diff --git a/problem_details.go b/problem_details.go index a744de3..82783f6 100644 --- a/problem_details.go +++ b/problem_details.go @@ -47,6 +47,42 @@ func (p ProblemDetails) MarshalJSON() ([]byte, error) { return json.Marshal(payload) } +// UnmarshalJSON reads the standard RFC 9457 members and collects every other +// top-level member into Extensions, mirroring MarshalJSON. +func (p *ProblemDetails) UnmarshalJSON(data []byte) error { + var known struct { + Type string `json:"type"` + Title string `json:"title"` + Status int `json:"status"` + Detail string `json:"detail"` + Instance string `json:"instance"` + } + if err := json.Unmarshal(data, &known); err != nil { + return err + } + + var members map[string]any + if err := json.Unmarshal(data, &members); err != nil { + return err + } + for _, key := range []string{"type", "title", "status", "detail", "instance"} { + delete(members, key) + } + + decoded := ProblemDetails{ + Type: known.Type, + Title: known.Title, + Status: known.Status, + Detail: known.Detail, + Instance: known.Instance, + } + if len(members) > 0 { + decoded.Extensions = members + } + *p = decoded + return nil +} + // NewProblemDetails creates a ProblemDetails instance with standard fields. func NewProblemDetails(status int, problemType, title, detail string) *ProblemDetails { if status < 100 || status > 599 { diff --git a/problem_details_test.go b/problem_details_test.go index ac86921..2078519 100644 --- a/problem_details_test.go +++ b/problem_details_test.go @@ -2,6 +2,7 @@ package httpsuite import ( "encoding/json" + "reflect" "testing" ) @@ -20,6 +21,60 @@ func TestNewProblemDetailsDefaults(t *testing.T) { } } +func TestProblemDetailsJSONRoundTrip(t *testing.T) { + t.Parallel() + + original := ProblemDetails{ + Type: "https://example.com/errors/bad", + Title: "Bad Request", + Status: 400, + Detail: "broken", + Instance: "/things/1", + Extensions: map[string]interface{}{ + "messageKey": "errors.bad", + "correlationId": "abc-123", + }, + } + + body, err := json.Marshal(original) + if err != nil { + t.Fatalf("marshal: %v", err) + } + + var decoded ProblemDetails + if err := json.Unmarshal(body, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + + if !reflect.DeepEqual(original, decoded) { + t.Fatalf("round trip mismatch:\nwant %#v\ngot %#v", original, decoded) + } +} + +func TestProblemDetailsUnmarshalJSONNoExtensions(t *testing.T) { + t.Parallel() + + decoded := ProblemDetails{Extensions: map[string]interface{}{"stale": true}} + if err := json.Unmarshal([]byte(`{"type":"about:blank","title":"T","status":404}`), &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if decoded.Extensions != nil { + t.Fatalf("expected nil extensions, got %#v", decoded.Extensions) + } + if decoded.Status != 404 || decoded.Title != "T" { + t.Fatalf("unexpected decode: %#v", decoded) + } +} + +func TestProblemDetailsUnmarshalJSONInvalid(t *testing.T) { + t.Parallel() + + var decoded ProblemDetails + if err := json.Unmarshal([]byte(`{"status":"nope"}`), &decoded); err == nil { + t.Fatal("expected error for wrong status type") + } +} + func TestProblemDetailsMarshalJSONFlattensExtensions(t *testing.T) { t.Parallel()