From 9e9144bfe077e1fddfb0dc815aa6ab825f93dcb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Mouton?= Date: Tue, 2 Dec 2025 15:48:23 +0100 Subject: [PATCH 1/9] Add support for custom validation error handler in OApiApp configuration --- fiberoapi.go | 8 ++++++++ types.go | 23 ++++++++++++++--------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/fiberoapi.go b/fiberoapi.go index d507dda..983106c 100644 --- a/fiberoapi.go +++ b/fiberoapi.go @@ -73,6 +73,9 @@ func New(app *fiber.App, config ...Config) *OApiApp { if provided.DefaultSecurity != nil { cfg.DefaultSecurity = provided.DefaultSecurity } + if provided.ValidationErrorHandler != nil { + cfg.ValidationErrorHandler = provided.ValidationErrorHandler + } } oapi := &OApiApp{ @@ -821,6 +824,11 @@ func Method[TInput any, TOutput any, TError any]( fiberHandler := func(c *fiber.Ctx) error { input, err := parseInput[TInput](app, c, fullPath, &options) if err != nil { + // Use custom validation error handler if configured + if app.config.ValidationErrorHandler != nil { + return app.config.ValidationErrorHandler(c, err) + } + // Default validation error response return c.Status(400).JSON(ErrorResponse{ Code: 400, Details: err.Error(), diff --git a/types.go b/types.go index 0bfc141..c4ac3fe 100644 --- a/types.go +++ b/types.go @@ -53,17 +53,22 @@ type PathInfo struct { Index int // Position in the path for validation } +// ValidationErrorHandler is a function type for handling validation errors +// It receives the fiber context and the validation error, and returns a fiber error response +type ValidationErrorHandler func(c *fiber.Ctx, err error) error + // Config represents configuration for the OApi wrapper type Config struct { - EnableValidation bool // Enable request validation (default: true) - EnableOpenAPIDocs bool // Enable automatic docs setup (default: true) - EnableAuthorization bool // Enable authorization validation (default: false) - OpenAPIDocsPath string // Path for documentation UI (default: "/docs") - OpenAPIJSONPath string // Path for OpenAPI JSON spec (default: "/openapi.json") - OpenAPIYamlPath string // Path for OpenAPI YAML spec (default: "/openapi.yaml") - AuthService AuthorizationService // Service for handling authentication and authorization - SecuritySchemes map[string]SecurityScheme // OpenAPI security schemes - DefaultSecurity []map[string][]string // Default security requirements + EnableValidation bool // Enable request validation (default: true) + EnableOpenAPIDocs bool // Enable automatic docs setup (default: true) + EnableAuthorization bool // Enable authorization validation (default: false) + OpenAPIDocsPath string // Path for documentation UI (default: "/docs") + OpenAPIJSONPath string // Path for OpenAPI JSON spec (default: "/openapi.json") + OpenAPIYamlPath string // Path for OpenAPI YAML spec (default: "/openapi.yaml") + AuthService AuthorizationService // Service for handling authentication and authorization + SecuritySchemes map[string]SecurityScheme // OpenAPI security schemes + DefaultSecurity []map[string][]string // Default security requirements + ValidationErrorHandler ValidationErrorHandler // Custom handler for validation errors } // OpenAPIOptions represents options for OpenAPI operations From 6f8fa8bd447e27199f1747082013f2559ee70db3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Mouton?= Date: Tue, 2 Dec 2025 15:48:31 +0100 Subject: [PATCH 2/9] Add example and tests for custom validation error handler --- _examples/custom_validation_error/README.md | 129 ++++++++++++++++++++ _examples/custom_validation_error/go.mod | 31 +++++ _examples/custom_validation_error/go.sum | 52 ++++++++ _examples/custom_validation_error/main.go | 76 ++++++++++++ custom_validation_error_test.go | 124 +++++++++++++++++++ 5 files changed, 412 insertions(+) create mode 100644 _examples/custom_validation_error/README.md create mode 100644 _examples/custom_validation_error/go.mod create mode 100644 _examples/custom_validation_error/go.sum create mode 100644 _examples/custom_validation_error/main.go create mode 100644 custom_validation_error_test.go diff --git a/_examples/custom_validation_error/README.md b/_examples/custom_validation_error/README.md new file mode 100644 index 0000000..810ecbb --- /dev/null +++ b/_examples/custom_validation_error/README.md @@ -0,0 +1,129 @@ +# Custom Validation Error Handler Example + +This example demonstrates how to use a custom validation error handler in fiber-oapi to return your own error structure when validation fails. + +## Problem + +By default, fiber-oapi returns a standard `ErrorResponse` structure when validation fails: + +```json +{ + "code": 400, + "details": "validation error message", + "type": "validation_error" +} +``` + +However, you might want all errors in your API to follow the same structure, including validation errors. + +## Solution + +Use the `ValidationErrorHandler` field in the `Config` to provide a custom function that handles validation errors: + +```go +oapi := fiberoapi.New(app, fiberoapi.Config{ + EnableValidation: true, + ValidationErrorHandler: func(c *fiber.Ctx, err error) error { + // Return your custom error structure + return c.Status(fiber.StatusBadRequest).JSON(CustomErrorResponse{ + Success: false, + Message: err.Error(), + Code: "VALIDATION_ERROR", + }) + }, +}) +``` + +## Running the Example + +```bash +go run main.go +``` + +## Testing + +Try sending an invalid request: + +```bash +# Missing required fields +curl -X POST http://localhost:3000/users \ + -H "Content-Type: application/json" \ + -d '{}' + +# Response: +# { +# "success": false, +# "message": "Key: 'CreateUserInput.Name' Error:Field validation for 'Name' failed on the 'required' tag...", +# "code": "VALIDATION_ERROR" +# } +``` + +```bash +# Invalid email format +curl -X POST http://localhost:3000/users \ + -H "Content-Type: application/json" \ + -d '{ + "name": "John", + "email": "invalid-email", + "age": 25 + }' + +# Response: +# { +# "success": false, +# "message": "Key: 'CreateUserInput.Email' Error:Field validation for 'Email' failed on the 'email' tag", +# "code": "VALIDATION_ERROR" +# } +``` + +```bash +# Valid request +curl -X POST http://localhost:3000/users \ + -H "Content-Type: application/json" \ + -d '{ + "name": "John Doe", + "email": "john@example.com", + "age": 25 + }' + +# Response: +# { +# "id": 1, +# "name": "John Doe", +# "email": "john@example.com", +# "age": 25, +# "message": "User created successfully" +# } +``` + +## Advanced Usage + +You can also parse the validation error to extract detailed information: + +```go +ValidationErrorHandler: func(c *fiber.Ctx, err error) error { + // Parse validator errors for more details + if validationErrs, ok := err.(validator.ValidationErrors); ok { + errors := make([]map[string]string, 0) + for _, fieldErr := range validationErrs { + errors = append(errors, map[string]string{ + "field": fieldErr.Field(), + "tag": fieldErr.Tag(), + "value": fmt.Sprintf("%v", fieldErr.Value()), + "message": fieldErr.Error(), + }) + } + return c.Status(fiber.StatusBadRequest).JSON(map[string]interface{}{ + "success": false, + "errors": errors, + }) + } + + // Fallback for other errors + return c.Status(fiber.StatusBadRequest).JSON(CustomErrorResponse{ + Success: false, + Message: err.Error(), + Code: "VALIDATION_ERROR", + }) +}, +``` diff --git a/_examples/custom_validation_error/go.mod b/_examples/custom_validation_error/go.mod new file mode 100644 index 0000000..61b3c37 --- /dev/null +++ b/_examples/custom_validation_error/go.mod @@ -0,0 +1,31 @@ +module custom_validation_error_example + +go 1.25.1 + +replace github.com/labbs/fiber-oapi => ../.. + +require ( + github.com/gofiber/fiber/v2 v2.52.10 + github.com/labbs/fiber-oapi v0.0.0-00010101000000-000000000000 +) + +require ( + github.com/andybalholm/brotli v1.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.10 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.28.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.17 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.66.0 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/_examples/custom_validation_error/go.sum b/_examples/custom_validation_error/go.sum new file mode 100644 index 0000000..4b834d8 --- /dev/null +++ b/_examples/custom_validation_error/go.sum @@ -0,0 +1,52 @@ +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= +github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= +github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= +github.com/gofiber/fiber/v2 v2.52.10 h1:jRHROi2BuNti6NYXmZ6gbNSfT3zj/8c0xy94GOU5elY= +github.com/gofiber/fiber/v2 v2.52.10/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkENfrjQ= +github.com/mattn/go-runewidth v0.0.17/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.66.0 h1:M87A0Z7EayeyNaV6pfO3tUTUiYO0dZfEJnRGXTVNuyU= +github.com/valyala/fasthttp v1.66.0/go.mod h1:Y4eC+zwoocmXSVCB1JmhNbYtS7tZPRI2ztPB72EVObs= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/_examples/custom_validation_error/main.go b/_examples/custom_validation_error/main.go new file mode 100644 index 0000000..1d9d307 --- /dev/null +++ b/_examples/custom_validation_error/main.go @@ -0,0 +1,76 @@ +package main + +import ( + "log" + + fiberoapi "github.com/labbs/fiber-oapi" + "github.com/gofiber/fiber/v2" +) + +// CustomErrorResponse représente votre structure d'erreur personnalisée +type CustomErrorResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Code string `json:"code"` +} + +// CreateUserInput représente l'entrée pour créer un utilisateur +type CreateUserInput struct { + Name string `json:"name" validate:"required,min=3"` + Email string `json:"email" validate:"required,email"` + Age int `json:"age" validate:"required,min=18,max=100"` +} + +// CreateUserOutput représente la sortie +type CreateUserOutput struct { + ID int `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + Age int `json:"age"` + Message string `json:"message"` +} + +func main() { + app := fiber.New() + + // Configurer fiber-oapi avec un gestionnaire d'erreur de validation personnalisé + oapi := fiberoapi.New(app, fiberoapi.Config{ + EnableValidation: true, + EnableOpenAPIDocs: true, + // Définir votre handler personnalisé pour les erreurs de validation + ValidationErrorHandler: func(c *fiber.Ctx, err error) error { + // Vous pouvez parser l'erreur de validation pour extraire plus de détails + // ou simplement retourner votre structure personnalisée + return c.Status(fiber.StatusBadRequest).JSON(CustomErrorResponse{ + Success: false, + Message: err.Error(), + Code: "VALIDATION_ERROR", + }) + }, + }) + + // Définir votre endpoint + fiberoapi.Post[CreateUserInput, CreateUserOutput, struct{}]( + oapi, + "/users", + func(c *fiber.Ctx, input CreateUserInput) (CreateUserOutput, struct{}) { + // Logique de création d'utilisateur + return CreateUserOutput{ + ID: 1, + Name: input.Name, + Email: input.Email, + Age: input.Age, + Message: "User created successfully", + }, struct{}{} + }, + fiberoapi.OpenAPIOptions{ + Summary: "Create a new user", + Description: "Creates a new user with validation", + Tags: []string{"users"}, + }, + ) + + log.Println("Server starting on :3000") + log.Println("OpenAPI docs available at http://localhost:3000/docs") + log.Fatal(oapi.Listen(":3000")) +} diff --git a/custom_validation_error_test.go b/custom_validation_error_test.go new file mode 100644 index 0000000..ba5224a --- /dev/null +++ b/custom_validation_error_test.go @@ -0,0 +1,124 @@ +package fiberoapi + +import ( + "bytes" + "encoding/json" + "io" + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/stretchr/testify/assert" +) + +// CustomValidationError represents a custom error structure +type CustomValidationError struct { + Success bool `json:"success"` + Message string `json:"message"` + Code string `json:"code"` +} + +func TestCustomValidationErrorHandler(t *testing.T) { + app := fiber.New() + + // Configure with custom validation error handler + oapi := New(app, Config{ + EnableValidation: true, + EnableOpenAPIDocs: false, + ValidationErrorHandler: func(c *fiber.Ctx, err error) error { + return c.Status(fiber.StatusBadRequest).JSON(CustomValidationError{ + Success: false, + Message: err.Error(), + Code: "CUSTOM_VALIDATION_ERROR", + }) + }, + }) + + type TestInput struct { + Name string `json:"name" validate:"required,min=3"` + Email string `json:"email" validate:"required,email"` + } + + type TestOutput struct { + Message string `json:"message"` + } + + Post[TestInput, TestOutput, struct{}]( + oapi, + "/test", + func(c *fiber.Ctx, input TestInput) (TestOutput, struct{}) { + return TestOutput{Message: "success"}, struct{}{} + }, + OpenAPIOptions{}, + ) + + // Test with invalid input (missing required field) + reqBody := map[string]interface{}{ + "name": "ab", // Too short (min=3) + } + bodyBytes, _ := json.Marshal(reqBody) + + req := httptest.NewRequest("POST", "/test", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + + resp, err := app.Test(req) + assert.NoError(t, err) + assert.Equal(t, fiber.StatusBadRequest, resp.StatusCode) + + // Verify custom error structure + body, _ := io.ReadAll(resp.Body) + t.Logf("Response body: %s", string(body)) + var customErr CustomValidationError + err = json.Unmarshal(body, &customErr) + assert.NoError(t, err) + assert.False(t, customErr.Success) + assert.Equal(t, "CUSTOM_VALIDATION_ERROR", customErr.Code) + assert.NotEmpty(t, customErr.Message) +} + +func TestDefaultValidationErrorWhenNoCustomHandler(t *testing.T) { + app := fiber.New() + + // Configure without custom validation error handler + oapi := New(app, Config{ + EnableValidation: true, + EnableOpenAPIDocs: false, + }) + + type TestInput struct { + Name string `json:"name" validate:"required"` + } + + type TestOutput struct { + Message string `json:"message"` + } + + Post[TestInput, TestOutput, struct{}]( + oapi, + "/test", + func(c *fiber.Ctx, input TestInput) (TestOutput, struct{}) { + return TestOutput{Message: "success"}, struct{}{} + }, + OpenAPIOptions{}, + ) + + // Test with invalid input + reqBody := map[string]interface{}{} + bodyBytes, _ := json.Marshal(reqBody) + + req := httptest.NewRequest("POST", "/test", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + + resp, err := app.Test(req) + assert.NoError(t, err) + assert.Equal(t, fiber.StatusBadRequest, resp.StatusCode) + + // Verify default error structure (ErrorResponse) + body, _ := io.ReadAll(resp.Body) + var defaultErr ErrorResponse + err = json.Unmarshal(body, &defaultErr) + assert.NoError(t, err) + assert.Equal(t, 400, defaultErr.Code) + assert.Equal(t, "validation_error", defaultErr.Type) + assert.NotEmpty(t, defaultErr.Details) +} From ea3a227c194fc87d0d48ad329903ea0b0a90ddd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Mouton?= Date: Tue, 2 Dec 2025 15:56:37 +0100 Subject: [PATCH 3/9] Add test for custom validation error handler with disabled OpenAPI docs --- custom_validation_error_test.go | 60 +++++++++++++++++++++++++++++++++ fiberoapi.go | 3 +- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/custom_validation_error_test.go b/custom_validation_error_test.go index ba5224a..21e61ec 100644 --- a/custom_validation_error_test.go +++ b/custom_validation_error_test.go @@ -122,3 +122,63 @@ func TestDefaultValidationErrorWhenNoCustomHandler(t *testing.T) { assert.Equal(t, "validation_error", defaultErr.Type) assert.NotEmpty(t, defaultErr.Details) } + +func TestCustomValidationErrorHandlerWithDisabledDocs(t *testing.T) { + app := fiber.New() + + // Configure with custom validation error handler AND EnableOpenAPIDocs: false + // This tests that boolean config is properly respected when ValidationErrorHandler is set + oapi := New(app, Config{ + EnableValidation: true, + EnableOpenAPIDocs: false, // This should be respected + ValidationErrorHandler: func(c *fiber.Ctx, err error) error { + return c.Status(fiber.StatusBadRequest).JSON(CustomValidationError{ + Success: false, + Message: err.Error(), + Code: "CUSTOM_ERROR", + }) + }, + }) + + type TestInput struct { + Name string `json:"name" validate:"required"` + } + + type TestOutput struct { + Message string `json:"message"` + } + + Post[TestInput, TestOutput, struct{}]( + oapi, + "/test", + func(c *fiber.Ctx, input TestInput) (TestOutput, struct{}) { + return TestOutput{Message: "success"}, struct{}{} + }, + OpenAPIOptions{}, + ) + + // Test validation error uses custom handler + reqBody := map[string]interface{}{} + bodyBytes, _ := json.Marshal(reqBody) + + req := httptest.NewRequest("POST", "/test", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + + resp, err := app.Test(req) + assert.NoError(t, err) + assert.Equal(t, fiber.StatusBadRequest, resp.StatusCode) + + // Verify custom error structure + body, _ := io.ReadAll(resp.Body) + var customErr CustomValidationError + err = json.Unmarshal(body, &customErr) + assert.NoError(t, err) + assert.Equal(t, "CUSTOM_ERROR", customErr.Code) + + // Verify that EnableOpenAPIDocs: false was respected + // The docs endpoint should not exist + req = httptest.NewRequest("GET", "/docs", nil) + resp, err = app.Test(req) + assert.NoError(t, err) + assert.Equal(t, fiber.StatusNotFound, resp.StatusCode) +} diff --git a/fiberoapi.go b/fiberoapi.go index 983106c..82a4303 100644 --- a/fiberoapi.go +++ b/fiberoapi.go @@ -40,7 +40,8 @@ func New(app *fiber.App, config ...Config) *OApiApp { provided.SecuritySchemes != nil || provided.OpenAPIDocsPath != "" || provided.OpenAPIJSONPath != "" || - provided.OpenAPIYamlPath != "" + provided.OpenAPIYamlPath != "" || + provided.ValidationErrorHandler != nil // Only override boolean defaults if the config appears to be explicitly set if hasExplicitConfig { From 48baaf592031243fe0ab125a22d70a222240e3e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Mouton?= Date: Tue, 2 Dec 2025 15:58:06 +0100 Subject: [PATCH 4/9] Refactor comments and import order in custom validation error example --- _examples/custom_validation_error/main.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/_examples/custom_validation_error/main.go b/_examples/custom_validation_error/main.go index 1d9d307..7c2ff56 100644 --- a/_examples/custom_validation_error/main.go +++ b/_examples/custom_validation_error/main.go @@ -3,25 +3,25 @@ package main import ( "log" - fiberoapi "github.com/labbs/fiber-oapi" "github.com/gofiber/fiber/v2" + fiberoapi "github.com/labbs/fiber-oapi" ) -// CustomErrorResponse représente votre structure d'erreur personnalisée +// CustomErrorResponse is the structure for custom validation error responses type CustomErrorResponse struct { Success bool `json:"success"` Message string `json:"message"` Code string `json:"code"` } -// CreateUserInput représente l'entrée pour créer un utilisateur +// CreateUserInput is the input structure for creating a user type CreateUserInput struct { Name string `json:"name" validate:"required,min=3"` Email string `json:"email" validate:"required,email"` Age int `json:"age" validate:"required,min=18,max=100"` } -// CreateUserOutput représente la sortie +// CreateUserOutput is the output structure for creating a user type CreateUserOutput struct { ID int `json:"id"` Name string `json:"name"` @@ -33,14 +33,14 @@ type CreateUserOutput struct { func main() { app := fiber.New() - // Configurer fiber-oapi avec un gestionnaire d'erreur de validation personnalisé + // Configure fiber-oapi with a custom validation error handler oapi := fiberoapi.New(app, fiberoapi.Config{ EnableValidation: true, EnableOpenAPIDocs: true, - // Définir votre handler personnalisé pour les erreurs de validation + // Define your custom handler for validation errors ValidationErrorHandler: func(c *fiber.Ctx, err error) error { - // Vous pouvez parser l'erreur de validation pour extraire plus de détails - // ou simplement retourner votre structure personnalisée + // You can parse the validation error to extract more details + // or simply return your custom structure return c.Status(fiber.StatusBadRequest).JSON(CustomErrorResponse{ Success: false, Message: err.Error(), @@ -49,12 +49,12 @@ func main() { }, }) - // Définir votre endpoint + // Define your endpoint fiberoapi.Post[CreateUserInput, CreateUserOutput, struct{}]( oapi, "/users", func(c *fiber.Ctx, input CreateUserInput) (CreateUserOutput, struct{}) { - // Logique de création d'utilisateur + // User creation logic goes here return CreateUserOutput{ ID: 1, Name: input.Name, From a5affc8de0c8eecca9caf19859dde264d0a9ffa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Mouton?= Date: Tue, 2 Dec 2025 16:16:43 +0100 Subject: [PATCH 5/9] Add test to ensure validation is enabled when only custom error handler is set --- custom_validation_error_test.go | 67 +++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/custom_validation_error_test.go b/custom_validation_error_test.go index 21e61ec..b6e12c9 100644 --- a/custom_validation_error_test.go +++ b/custom_validation_error_test.go @@ -182,3 +182,70 @@ func TestCustomValidationErrorHandlerWithDisabledDocs(t *testing.T) { assert.NoError(t, err) assert.Equal(t, fiber.StatusNotFound, resp.StatusCode) } + +func TestValidationErrorHandlerImpliesValidationEnabled(t *testing.T) { + app := fiber.New() + + // Configure ONLY ValidationErrorHandler without explicitly setting EnableValidation + // This should keep validation enabled by default since it makes sense + oapi := New(app, Config{ + ValidationErrorHandler: func(c *fiber.Ctx, err error) error { + return c.Status(fiber.StatusBadRequest).JSON(CustomValidationError{ + Success: false, + Message: err.Error(), + Code: "VALIDATION_HANDLER_ACTIVE", + }) + }, + }) + + type TestInput struct { + Name string `json:"name" validate:"required,min=3"` + } + + type TestOutput struct { + Message string `json:"message"` + } + + Post[TestInput, TestOutput, struct{}]( + oapi, + "/test", + func(c *fiber.Ctx, input TestInput) (TestOutput, struct{}) { + return TestOutput{Message: "success"}, struct{}{} + }, + OpenAPIOptions{}, + ) + + // Test that validation is still active and uses custom handler + reqBody := map[string]interface{}{ + "name": "ab", // Too short (min=3) + } + bodyBytes, _ := json.Marshal(reqBody) + + req := httptest.NewRequest("POST", "/test", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + + resp, err := app.Test(req) + assert.NoError(t, err) + assert.Equal(t, fiber.StatusBadRequest, resp.StatusCode) + + // Verify validation is active and custom handler was used + body, _ := io.ReadAll(resp.Body) + var customErr CustomValidationError + err = json.Unmarshal(body, &customErr) + assert.NoError(t, err) + assert.Equal(t, "VALIDATION_HANDLER_ACTIVE", customErr.Code) + assert.Contains(t, customErr.Message, "min") + + // Test with valid data to ensure endpoint works + reqBody = map[string]interface{}{ + "name": "John Doe", + } + bodyBytes, _ = json.Marshal(reqBody) + + req = httptest.NewRequest("POST", "/test", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + + resp, err = app.Test(req) + assert.NoError(t, err) + assert.Equal(t, fiber.StatusOK, resp.StatusCode) +} From 30e7c0ecc299a39ebcea2980dcf6af1442f7ce86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Mouton?= Date: Tue, 2 Dec 2025 16:16:48 +0100 Subject: [PATCH 6/9] Enhance validation logic to ensure validation remains enabled when a ValidationErrorHandler is set without explicit disablement --- fiberoapi.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/fiberoapi.go b/fiberoapi.go index 82a4303..35768c7 100644 --- a/fiberoapi.go +++ b/fiberoapi.go @@ -50,6 +50,24 @@ func New(app *fiber.App, config ...Config) *OApiApp { } // If no explicit config, keep the defaults (true, true, false) + // Special case: if ValidationErrorHandler is set but EnableValidation wasn't explicitly set to false, + // keep validation enabled (default: true) since it makes no sense to have a validation error handler without validation + if provided.ValidationErrorHandler != nil && !provided.EnableValidation { + // Check if EnableValidation was explicitly set to false or just using the zero value + // If hasExplicitConfig is true only because of ValidationErrorHandler, keep validation enabled + otherExplicitConfig := provided.EnableAuthorization || + provided.AuthService != nil || + provided.SecuritySchemes != nil || + provided.OpenAPIDocsPath != "" || + provided.OpenAPIJSONPath != "" || + provided.OpenAPIYamlPath != "" + + if !otherExplicitConfig { + // ValidationErrorHandler is the only explicit config, so keep validation enabled + cfg.EnableValidation = true + } + } + // For EnableAuthorization: only set to true if explicitly provided if provided.EnableAuthorization { cfg.EnableAuthorization = provided.EnableAuthorization From 05fab930ab061086600c188d37d755508da9a772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Mouton?= Date: Tue, 2 Dec 2025 16:16:52 +0100 Subject: [PATCH 7/9] Update README to clarify automatic validation enablement with ValidationErrorHandler --- _examples/custom_validation_error/README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/_examples/custom_validation_error/README.md b/_examples/custom_validation_error/README.md index 810ecbb..00bd13c 100644 --- a/_examples/custom_validation_error/README.md +++ b/_examples/custom_validation_error/README.md @@ -20,9 +20,11 @@ However, you might want all errors in your API to follow the same structure, inc Use the `ValidationErrorHandler` field in the `Config` to provide a custom function that handles validation errors: +**Note:** When you configure a `ValidationErrorHandler`, validation is automatically enabled (`EnableValidation: true` by default). You don't need to explicitly set `EnableValidation: true` unless you're also configuring other options. + ```go +// Minimal configuration - validation is automatically enabled oapi := fiberoapi.New(app, fiberoapi.Config{ - EnableValidation: true, ValidationErrorHandler: func(c *fiber.Ctx, err error) error { // Return your custom error structure return c.Status(fiber.StatusBadRequest).JSON(CustomErrorResponse{ @@ -34,6 +36,21 @@ oapi := fiberoapi.New(app, fiberoapi.Config{ }) ``` +```go +// Or with explicit configuration +oapi := fiberoapi.New(app, fiberoapi.Config{ + EnableValidation: true, + EnableOpenAPIDocs: true, + ValidationErrorHandler: func(c *fiber.Ctx, err error) error { + return c.Status(fiber.StatusBadRequest).JSON(CustomErrorResponse{ + Success: false, + Message: err.Error(), + Code: "VALIDATION_ERROR", + }) + }, +}) +``` + ## Running the Example ```bash From d067653b82abfd839b873b9bcb2101f90fbc4db1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Mouton?= Date: Tue, 2 Dec 2025 16:23:58 +0100 Subject: [PATCH 8/9] Refactor validation logic to restore defaults when ValidationErrorHandler is set without explicit boolean configurations --- fiberoapi.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/fiberoapi.go b/fiberoapi.go index 35768c7..503092e 100644 --- a/fiberoapi.go +++ b/fiberoapi.go @@ -50,22 +50,22 @@ func New(app *fiber.App, config ...Config) *OApiApp { } // If no explicit config, keep the defaults (true, true, false) - // Special case: if ValidationErrorHandler is set but EnableValidation wasn't explicitly set to false, - // keep validation enabled (default: true) since it makes no sense to have a validation error handler without validation - if provided.ValidationErrorHandler != nil && !provided.EnableValidation { - // Check if EnableValidation was explicitly set to false or just using the zero value - // If hasExplicitConfig is true only because of ValidationErrorHandler, keep validation enabled - otherExplicitConfig := provided.EnableAuthorization || - provided.AuthService != nil || - provided.SecuritySchemes != nil || - provided.OpenAPIDocsPath != "" || - provided.OpenAPIJSONPath != "" || - provided.OpenAPIYamlPath != "" - - if !otherExplicitConfig { - // ValidationErrorHandler is the only explicit config, so keep validation enabled - cfg.EnableValidation = true - } + // Special case: if ValidationErrorHandler is set and boolean fields seem to be using zero values, + // restore defaults since it makes no sense to have a validation error handler without validation + otherExplicitConfig := provided.EnableAuthorization || + provided.AuthService != nil || + provided.SecuritySchemes != nil || + provided.OpenAPIDocsPath != "" || + provided.OpenAPIJSONPath != "" || + provided.OpenAPIYamlPath != "" + + // Only restore defaults if ALL boolean fields are false (suggesting they weren't explicitly set) + allBooleansAreFalse := !provided.EnableValidation && !provided.EnableOpenAPIDocs && !provided.EnableAuthorization + + if provided.ValidationErrorHandler != nil && !otherExplicitConfig && allBooleansAreFalse { + // ValidationErrorHandler is the only explicit config, so restore defaults for boolean fields + cfg.EnableValidation = true // Keep validation enabled - the handler needs it + cfg.EnableOpenAPIDocs = true // Keep docs enabled - default behavior } // For EnableAuthorization: only set to true if explicitly provided From feac2932af2f4bd3d3786fcbf2379c9b867dba8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Mouton?= Date: Tue, 2 Dec 2025 16:24:02 +0100 Subject: [PATCH 9/9] Add test to verify OpenAPI docs are enabled by default with only ValidationErrorHandler configured --- custom_validation_error_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/custom_validation_error_test.go b/custom_validation_error_test.go index b6e12c9..0346a04 100644 --- a/custom_validation_error_test.go +++ b/custom_validation_error_test.go @@ -248,4 +248,10 @@ func TestValidationErrorHandlerImpliesValidationEnabled(t *testing.T) { resp, err = app.Test(req) assert.NoError(t, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) + + // Verify that OpenAPI docs are enabled by default (not silently disabled) + req = httptest.NewRequest("GET", "/docs", nil) + resp, err = app.Test(req) + assert.NoError(t, err) + assert.Equal(t, fiber.StatusOK, resp.StatusCode, "OpenAPI docs should be enabled by default when only ValidationErrorHandler is configured") }