Skip to content

Commit 590f30d

Browse files
authored
Fix JSON unmarshal error handling and tests (#22)
* Enhance JSON unmarshal error handling to provide clearer validation messages * Refactor JSON type mismatch error handling tests for clarity and reliability
1 parent 868d736 commit 590f30d

File tree

2 files changed

+32
-52
lines changed

2 files changed

+32
-52
lines changed

common.go

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package fiberoapi
22

33
import (
4-
"encoding/json"
5-
"errors"
64
"fmt"
75
"reflect"
86
"strconv"
@@ -52,12 +50,33 @@ func parseInput[TInput any](app *OApiApp, c *fiber.Ctx, path string, options *Op
5250
// It's OK, the POST has no body - ignore the error
5351
} else {
5452
// Transform JSON unmarshal type errors into readable validation errors
55-
// Using errors.As for more robust error handling (handles wrapped errors)
56-
var unmarshalErr *json.UnmarshalTypeError
57-
if errors.As(err, &unmarshalErr) {
58-
return input, fmt.Errorf("invalid type for field '%s': expected %s but got %s",
59-
unmarshalErr.Field, unmarshalErr.Type.String(), unmarshalErr.Value)
53+
// Check if error message contains unmarshal type error pattern
54+
errMsg := err.Error()
55+
if strings.Contains(errMsg, "json: cannot unmarshal") && strings.Contains(errMsg, "into Go struct field") {
56+
// Parse the error message to extract field name and type info
57+
// Format: "json: cannot unmarshal <type> into Go struct field <StructName>.<Field> of type <GoType>"
58+
parts := strings.Split(errMsg, "into Go struct field ")
59+
if len(parts) == 2 {
60+
afterField := parts[1]
61+
fieldParts := strings.Split(afterField, " of type ")
62+
if len(fieldParts) == 2 {
63+
// Extract field name (after the last dot)
64+
fullFieldName := fieldParts[0]
65+
fieldNameParts := strings.Split(fullFieldName, ".")
66+
fieldName := fieldNameParts[len(fieldNameParts)-1]
67+
68+
// Extract expected type
69+
expectedType := fieldParts[1]
70+
71+
// Extract actual type from the first part
72+
typePart := strings.TrimPrefix(parts[0], "json: cannot unmarshal ")
73+
74+
return input, fmt.Errorf("invalid type for field '%s': expected %s but got %s",
75+
fieldName, expectedType, typePart)
76+
}
77+
}
6078
}
79+
6180
return input, err
6281
}
6382
}

json_type_error_test.go

Lines changed: 6 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@ import (
1818
// the raw Go error message is not user-friendly:
1919
// "json: cannot unmarshal number into Go struct field Request.Description of type string"
2020
//
21-
// Solution: Detect json.UnmarshalTypeError and transform it into a readable message:
21+
// Solution: Parse the error message to extract field name and type information,
22+
// then transform it into a readable message:
2223
// "invalid type for field 'description': expected string but got number"
2324
//
24-
// Implementation: Uses errors.As (not type assertion) to handle wrapped errors correctly.
25-
// This ensures the error detection works even if the error is wrapped by Fiber or middleware.
25+
// Implementation: The error type from c.BodyParser() is *errors.UnmarshalTypeError
26+
// (not *json.UnmarshalTypeError), so we parse the error message string to extract
27+
// the field name, expected type, and actual type. This approach works reliably
28+
// across different Fiber versions and handles all JSON unmarshal type errors.
2629

2730
// Test for JSON type mismatch errors
2831
func TestJSONTypeMismatchErrors(t *testing.T) {
@@ -115,48 +118,6 @@ func TestJSONTypeMismatchErrors(t *testing.T) {
115118
}
116119
}
117120

118-
// Test that errors.As correctly handles wrapped errors
119-
func TestJSONTypeMismatchWithWrappedError(t *testing.T) {
120-
app := fiber.New()
121-
oapi := New(app)
122-
123-
type TestRequest struct {
124-
Value string `json:"value"`
125-
}
126-
127-
type TestResponse struct {
128-
Result string `json:"result"`
129-
}
130-
131-
Post(oapi, "/test", func(c *fiber.Ctx, input TestRequest) (TestResponse, TestError) {
132-
return TestResponse{Result: "OK"}, TestError{}
133-
}, OpenAPIOptions{})
134-
135-
// Test with wrong type - even if the error is wrapped, errors.As should detect it
136-
req := httptest.NewRequest("POST", "/test", strings.NewReader(`{"value": 123}`))
137-
req.Header.Set("Content-Type", "application/json")
138-
resp, err := app.Test(req)
139-
if err != nil {
140-
t.Fatalf("Expected no error, got %v", err)
141-
}
142-
143-
if resp.StatusCode != 400 {
144-
t.Errorf("Expected status 400, got %d", resp.StatusCode)
145-
}
146-
147-
body, _ := io.ReadAll(resp.Body)
148-
bodyStr := string(body)
149-
150-
// Should contain our custom error message
151-
if !strings.Contains(bodyStr, "invalid type for field 'value'") {
152-
t.Errorf("Expected 'invalid type for field' in error message, got %s", bodyStr)
153-
}
154-
155-
if !strings.Contains(bodyStr, "expected string but got number") {
156-
t.Errorf("Expected 'expected string but got number' in error message, got %s", bodyStr)
157-
}
158-
}
159-
160121
// Test with custom validation error handler
161122
func TestJSONTypeMismatchWithCustomHandler(t *testing.T) {
162123
app := fiber.New()

0 commit comments

Comments
 (0)