From 82fc8696f75f97792138059c35344e130989adfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Mouton?= Date: Wed, 3 Dec 2025 11:26:32 +0100 Subject: [PATCH 1/5] Fix: Improve JSON unmarshal error handling in parseInput function --- common.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/common.go b/common.go index 900ae40..7f1be47 100644 --- a/common.go +++ b/common.go @@ -1,6 +1,7 @@ package fiberoapi import ( + "encoding/json" "fmt" "reflect" "strconv" @@ -49,6 +50,11 @@ func parseInput[TInput any](app *OApiApp, c *fiber.Ctx, path string, options *Op if bodyLength == 0 && method == "POST" { // It's OK, the POST has no body - ignore the error } else { + // Transform JSON unmarshal type errors into readable validation errors + if unmarshalErr, ok := err.(*json.UnmarshalTypeError); ok { + return input, fmt.Errorf("invalid type for field '%s': expected %s but got %s", + unmarshalErr.Field, unmarshalErr.Type.String(), unmarshalErr.Value) + } return input, err } } From c02ec07debca239378546a0fe10e630fb624aea0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Mouton?= Date: Wed, 3 Dec 2025 11:26:38 +0100 Subject: [PATCH 2/5] Test: Add JSON type mismatch error handling tests --- json_type_error_test.go | 156 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 json_type_error_test.go diff --git a/json_type_error_test.go b/json_type_error_test.go new file mode 100644 index 0000000..553551a --- /dev/null +++ b/json_type_error_test.go @@ -0,0 +1,156 @@ +package fiberoapi + +import ( + "io" + "net/http/httptest" + "strings" + "testing" + + "github.com/gofiber/fiber/v2" +) + +// Test for JSON type mismatch errors +func TestJSONTypeMismatchErrors(t *testing.T) { + app := fiber.New() + oapi := New(app) + + type CreateWorkspaceRequest struct { + Description string `json:"description,omitempty" validate:"omitempty,max=255"` + IpsWhitelist []string `json:"ips_whitelist,omitempty" validate:"dive,cidrv4|ip4_addr"` + } + + type CreateWorkspaceResponse struct { + Message string `json:"message"` + } + + Post(oapi, "/workspaces", func(c *fiber.Ctx, input CreateWorkspaceRequest) (CreateWorkspaceResponse, TestError) { + return CreateWorkspaceResponse{Message: "Workspace created"}, TestError{} + }, OpenAPIOptions{ + OperationID: "create-workspace", + Summary: "Create a new workspace", + }) + + tests := []struct { + name string + body string + expectedStatus int + errorContains string + }{ + { + name: "Valid request with string description", + body: `{"description": "A valid description"}`, + expectedStatus: 200, + }, + { + name: "Invalid request - description is a number", + body: `{"description": 0.0}`, + expectedStatus: 400, + errorContains: "invalid type for field 'description'", + }, + { + name: "Invalid request - description is an object", + body: `{"description": {"test": "test"}}`, + expectedStatus: 400, + errorContains: "invalid type for field 'description'", + }, + { + name: "Invalid request - ips_whitelist contains number", + body: `{"ips_whitelist": [123]}`, + expectedStatus: 400, + errorContains: "invalid type", + }, + { + name: "Valid request with empty body", + body: `{}`, + expectedStatus: 200, + }, + { + name: "Valid request with valid IPs", + body: `{"description": "Test", "ips_whitelist": ["192.168.1.0/24", "10.0.0.1"]}`, + expectedStatus: 200, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest("POST", "/workspaces", strings.NewReader(tt.body)) + req.Header.Set("Content-Type", "application/json") + resp, err := app.Test(req) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if resp.StatusCode != tt.expectedStatus { + body, _ := io.ReadAll(resp.Body) + t.Errorf("Expected status %d, got %d. Body: %s", tt.expectedStatus, resp.StatusCode, string(body)) + } + + if tt.errorContains != "" { + body, _ := io.ReadAll(resp.Body) + bodyStr := string(body) + if !strings.Contains(bodyStr, tt.errorContains) { + t.Errorf("Expected error to contain '%s', got %s", tt.errorContains, bodyStr) + } + // Ensure it returns validation_error type + if !strings.Contains(bodyStr, "validation_error") { + t.Errorf("Expected validation_error type, got %s", bodyStr) + } + } + }) + } +} + +// Test with custom validation error handler +func TestJSONTypeMismatchWithCustomHandler(t *testing.T) { + app := fiber.New() + + // Create a custom validation error handler + customHandler := func(c *fiber.Ctx, err error) error { + return c.Status(422).JSON(fiber.Map{ + "status": "error", + "message": err.Error(), + }) + } + + oapi := New(app, Config{ + ValidationErrorHandler: customHandler, + }) + + type TestRequest struct { + Value string `json:"value"` + } + + type TestResponse struct { + Result string `json:"result"` + } + + Post(oapi, "/test", func(c *fiber.Ctx, input TestRequest) (TestResponse, TestError) { + return TestResponse{Result: "OK"}, TestError{} + }, OpenAPIOptions{}) + + // Test with wrong type + req := httptest.NewRequest("POST", "/test", strings.NewReader(`{"value": 123}`)) + req.Header.Set("Content-Type", "application/json") + resp, err := app.Test(req) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + // Should use custom handler status code + if resp.StatusCode != 422 { + t.Errorf("Expected status 422, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + bodyStr := string(body) + + // Should contain custom error format + if !strings.Contains(bodyStr, "status") || !strings.Contains(bodyStr, "error") { + t.Errorf("Expected custom error format, got %s", bodyStr) + } + + // Should still contain the error message about invalid type + if !strings.Contains(bodyStr, "invalid type") { + t.Errorf("Expected 'invalid type' in error message, got %s", bodyStr) + } +} From d48a68cd415807a34c8d973df03ef6f715c1f104 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Mouton?= Date: Wed, 3 Dec 2025 11:32:55 +0100 Subject: [PATCH 3/5] Fix: Update JSON request and response types in TestJSONTypeMismatchErrors --- json_type_error_test.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/json_type_error_test.go b/json_type_error_test.go index 553551a..d6bedaf 100644 --- a/json_type_error_test.go +++ b/json_type_error_test.go @@ -14,20 +14,20 @@ func TestJSONTypeMismatchErrors(t *testing.T) { app := fiber.New() oapi := New(app) - type CreateWorkspaceRequest struct { - Description string `json:"description,omitempty" validate:"omitempty,max=255"` - IpsWhitelist []string `json:"ips_whitelist,omitempty" validate:"dive,cidrv4|ip4_addr"` + type CreateRequest struct { + Description string `json:"description,omitempty" validate:"omitempty,max=255"` + Ips []string `json:"ips,omitempty" validate:"dive,cidrv4|ip4_addr"` } - type CreateWorkspaceResponse struct { + type CreateResponse struct { Message string `json:"message"` } - Post(oapi, "/workspaces", func(c *fiber.Ctx, input CreateWorkspaceRequest) (CreateWorkspaceResponse, TestError) { - return CreateWorkspaceResponse{Message: "Workspace created"}, TestError{} + Post(oapi, "/test", func(c *fiber.Ctx, input CreateRequest) (CreateResponse, TestError) { + return CreateResponse{Message: "created"}, TestError{} }, OpenAPIOptions{ - OperationID: "create-workspace", - Summary: "Create a new workspace", + OperationID: "create", + Summary: "Create a new entry", }) tests := []struct { @@ -54,8 +54,8 @@ func TestJSONTypeMismatchErrors(t *testing.T) { errorContains: "invalid type for field 'description'", }, { - name: "Invalid request - ips_whitelist contains number", - body: `{"ips_whitelist": [123]}`, + name: "Invalid request - ips contains number", + body: `{"ips": [123]}`, expectedStatus: 400, errorContains: "invalid type", }, @@ -66,28 +66,28 @@ func TestJSONTypeMismatchErrors(t *testing.T) { }, { name: "Valid request with valid IPs", - body: `{"description": "Test", "ips_whitelist": ["192.168.1.0/24", "10.0.0.1"]}`, + body: `{"description": "Test", "ips": ["192.168.1.0/24", "10.0.0.1"]}`, expectedStatus: 200, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - req := httptest.NewRequest("POST", "/workspaces", strings.NewReader(tt.body)) + req := httptest.NewRequest("POST", "/test", strings.NewReader(tt.body)) req.Header.Set("Content-Type", "application/json") resp, err := app.Test(req) if err != nil { t.Fatalf("Expected no error, got %v", err) } + body, _ := io.ReadAll(resp.Body) + bodyStr := string(body) + if resp.StatusCode != tt.expectedStatus { - body, _ := io.ReadAll(resp.Body) - t.Errorf("Expected status %d, got %d. Body: %s", tt.expectedStatus, resp.StatusCode, string(body)) + t.Errorf("Expected status %d, got %d. Body: %s", tt.expectedStatus, resp.StatusCode, bodyStr) } if tt.errorContains != "" { - body, _ := io.ReadAll(resp.Body) - bodyStr := string(body) if !strings.Contains(bodyStr, tt.errorContains) { t.Errorf("Expected error to contain '%s', got %s", tt.errorContains, bodyStr) } From 37e329c1b8dcbcc683722e2feba70ed548c1c0a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Mouton?= Date: Wed, 3 Dec 2025 11:44:21 +0100 Subject: [PATCH 4/5] Fix: Enhance JSON unmarshal error handling in parseInput function --- common.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/common.go b/common.go index 7f1be47..9b56e98 100644 --- a/common.go +++ b/common.go @@ -2,6 +2,7 @@ package fiberoapi import ( "encoding/json" + "errors" "fmt" "reflect" "strconv" @@ -51,7 +52,9 @@ func parseInput[TInput any](app *OApiApp, c *fiber.Ctx, path string, options *Op // It's OK, the POST has no body - ignore the error } else { // Transform JSON unmarshal type errors into readable validation errors - if unmarshalErr, ok := err.(*json.UnmarshalTypeError); ok { + // Using errors.As for more robust error handling (handles wrapped errors) + var unmarshalErr *json.UnmarshalTypeError + if errors.As(err, &unmarshalErr) { return input, fmt.Errorf("invalid type for field '%s': expected %s but got %s", unmarshalErr.Field, unmarshalErr.Type.String(), unmarshalErr.Value) } From 96b1b3f497c0aaf7103209601448a743768cea7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Mouton?= Date: Wed, 3 Dec 2025 11:44:29 +0100 Subject: [PATCH 5/5] Fix: Add tests for JSON type mismatch error handling with wrapped errors --- json_type_error_test.go | 57 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/json_type_error_test.go b/json_type_error_test.go index d6bedaf..5e47be6 100644 --- a/json_type_error_test.go +++ b/json_type_error_test.go @@ -9,6 +9,21 @@ import ( "github.com/gofiber/fiber/v2" ) +// JSON Type Mismatch Error Handling Tests +// +// These tests verify that fiber-oapi correctly handles JSON type mismatch errors +// and transforms them into user-friendly validation error messages. +// +// Problem: When a client sends invalid JSON types (e.g., number instead of string), +// the raw Go error message is not user-friendly: +// "json: cannot unmarshal number into Go struct field Request.Description of type string" +// +// Solution: Detect json.UnmarshalTypeError and transform it into a readable message: +// "invalid type for field 'description': expected string but got number" +// +// Implementation: Uses errors.As (not type assertion) to handle wrapped errors correctly. +// This ensures the error detection works even if the error is wrapped by Fiber or middleware. + // Test for JSON type mismatch errors func TestJSONTypeMismatchErrors(t *testing.T) { app := fiber.New() @@ -100,6 +115,48 @@ func TestJSONTypeMismatchErrors(t *testing.T) { } } +// Test that errors.As correctly handles wrapped errors +func TestJSONTypeMismatchWithWrappedError(t *testing.T) { + app := fiber.New() + oapi := New(app) + + type TestRequest struct { + Value string `json:"value"` + } + + type TestResponse struct { + Result string `json:"result"` + } + + Post(oapi, "/test", func(c *fiber.Ctx, input TestRequest) (TestResponse, TestError) { + return TestResponse{Result: "OK"}, TestError{} + }, OpenAPIOptions{}) + + // Test with wrong type - even if the error is wrapped, errors.As should detect it + req := httptest.NewRequest("POST", "/test", strings.NewReader(`{"value": 123}`)) + req.Header.Set("Content-Type", "application/json") + resp, err := app.Test(req) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if resp.StatusCode != 400 { + t.Errorf("Expected status 400, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + bodyStr := string(body) + + // Should contain our custom error message + if !strings.Contains(bodyStr, "invalid type for field 'value'") { + t.Errorf("Expected 'invalid type for field' in error message, got %s", bodyStr) + } + + if !strings.Contains(bodyStr, "expected string but got number") { + t.Errorf("Expected 'expected string but got number' in error message, got %s", bodyStr) + } +} + // Test with custom validation error handler func TestJSONTypeMismatchWithCustomHandler(t *testing.T) { app := fiber.New()