Skip to content

Commit 98687a0

Browse files
authored
Feat: Add support for custom validation error handler in OApiApp configuration (#20)
* Add support for custom validation error handler in OApiApp configuration * Add example and tests for custom validation error handler * Add test for custom validation error handler with disabled OpenAPI docs * Refactor comments and import order in custom validation error example * Add test to ensure validation is enabled when only custom error handler is set * Enhance validation logic to ensure validation remains enabled when a ValidationErrorHandler is set without explicit disablement * Update README to clarify automatic validation enablement with ValidationErrorHandler * Refactor validation logic to restore defaults when ValidationErrorHandler is set without explicit boolean configurations * Add test to verify OpenAPI docs are enabled by default with only ValidationErrorHandler configured
1 parent d370e89 commit 98687a0

7 files changed

Lines changed: 604 additions & 10 deletions

File tree

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# Custom Validation Error Handler Example
2+
3+
This example demonstrates how to use a custom validation error handler in fiber-oapi to return your own error structure when validation fails.
4+
5+
## Problem
6+
7+
By default, fiber-oapi returns a standard `ErrorResponse` structure when validation fails:
8+
9+
```json
10+
{
11+
"code": 400,
12+
"details": "validation error message",
13+
"type": "validation_error"
14+
}
15+
```
16+
17+
However, you might want all errors in your API to follow the same structure, including validation errors.
18+
19+
## Solution
20+
21+
Use the `ValidationErrorHandler` field in the `Config` to provide a custom function that handles validation errors:
22+
23+
**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.
24+
25+
```go
26+
// Minimal configuration - validation is automatically enabled
27+
oapi := fiberoapi.New(app, fiberoapi.Config{
28+
ValidationErrorHandler: func(c *fiber.Ctx, err error) error {
29+
// Return your custom error structure
30+
return c.Status(fiber.StatusBadRequest).JSON(CustomErrorResponse{
31+
Success: false,
32+
Message: err.Error(),
33+
Code: "VALIDATION_ERROR",
34+
})
35+
},
36+
})
37+
```
38+
39+
```go
40+
// Or with explicit configuration
41+
oapi := fiberoapi.New(app, fiberoapi.Config{
42+
EnableValidation: true,
43+
EnableOpenAPIDocs: true,
44+
ValidationErrorHandler: func(c *fiber.Ctx, err error) error {
45+
return c.Status(fiber.StatusBadRequest).JSON(CustomErrorResponse{
46+
Success: false,
47+
Message: err.Error(),
48+
Code: "VALIDATION_ERROR",
49+
})
50+
},
51+
})
52+
```
53+
54+
## Running the Example
55+
56+
```bash
57+
go run main.go
58+
```
59+
60+
## Testing
61+
62+
Try sending an invalid request:
63+
64+
```bash
65+
# Missing required fields
66+
curl -X POST http://localhost:3000/users \
67+
-H "Content-Type: application/json" \
68+
-d '{}'
69+
70+
# Response:
71+
# {
72+
# "success": false,
73+
# "message": "Key: 'CreateUserInput.Name' Error:Field validation for 'Name' failed on the 'required' tag...",
74+
# "code": "VALIDATION_ERROR"
75+
# }
76+
```
77+
78+
```bash
79+
# Invalid email format
80+
curl -X POST http://localhost:3000/users \
81+
-H "Content-Type: application/json" \
82+
-d '{
83+
"name": "John",
84+
"email": "invalid-email",
85+
"age": 25
86+
}'
87+
88+
# Response:
89+
# {
90+
# "success": false,
91+
# "message": "Key: 'CreateUserInput.Email' Error:Field validation for 'Email' failed on the 'email' tag",
92+
# "code": "VALIDATION_ERROR"
93+
# }
94+
```
95+
96+
```bash
97+
# Valid request
98+
curl -X POST http://localhost:3000/users \
99+
-H "Content-Type: application/json" \
100+
-d '{
101+
"name": "John Doe",
102+
"email": "john@example.com",
103+
"age": 25
104+
}'
105+
106+
# Response:
107+
# {
108+
# "id": 1,
109+
# "name": "John Doe",
110+
# "email": "john@example.com",
111+
# "age": 25,
112+
# "message": "User created successfully"
113+
# }
114+
```
115+
116+
## Advanced Usage
117+
118+
You can also parse the validation error to extract detailed information:
119+
120+
```go
121+
ValidationErrorHandler: func(c *fiber.Ctx, err error) error {
122+
// Parse validator errors for more details
123+
if validationErrs, ok := err.(validator.ValidationErrors); ok {
124+
errors := make([]map[string]string, 0)
125+
for _, fieldErr := range validationErrs {
126+
errors = append(errors, map[string]string{
127+
"field": fieldErr.Field(),
128+
"tag": fieldErr.Tag(),
129+
"value": fmt.Sprintf("%v", fieldErr.Value()),
130+
"message": fieldErr.Error(),
131+
})
132+
}
133+
return c.Status(fiber.StatusBadRequest).JSON(map[string]interface{}{
134+
"success": false,
135+
"errors": errors,
136+
})
137+
}
138+
139+
// Fallback for other errors
140+
return c.Status(fiber.StatusBadRequest).JSON(CustomErrorResponse{
141+
Success: false,
142+
Message: err.Error(),
143+
Code: "VALIDATION_ERROR",
144+
})
145+
},
146+
```
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
module custom_validation_error_example
2+
3+
go 1.25.1
4+
5+
replace github.com/labbs/fiber-oapi => ../..
6+
7+
require (
8+
github.com/gofiber/fiber/v2 v2.52.10
9+
github.com/labbs/fiber-oapi v0.0.0-00010101000000-000000000000
10+
)
11+
12+
require (
13+
github.com/andybalholm/brotli v1.2.0 // indirect
14+
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
15+
github.com/go-playground/locales v0.14.1 // indirect
16+
github.com/go-playground/universal-translator v0.18.1 // indirect
17+
github.com/go-playground/validator/v10 v10.28.0 // indirect
18+
github.com/google/uuid v1.6.0 // indirect
19+
github.com/klauspost/compress v1.18.0 // indirect
20+
github.com/leodido/go-urn v1.4.0 // indirect
21+
github.com/mattn/go-colorable v0.1.14 // indirect
22+
github.com/mattn/go-isatty v0.0.20 // indirect
23+
github.com/mattn/go-runewidth v0.0.17 // indirect
24+
github.com/rivo/uniseg v0.4.7 // indirect
25+
github.com/valyala/bytebufferpool v1.0.0 // indirect
26+
github.com/valyala/fasthttp v1.66.0 // indirect
27+
golang.org/x/crypto v0.45.0 // indirect
28+
golang.org/x/sys v0.38.0 // indirect
29+
golang.org/x/text v0.31.0 // indirect
30+
gopkg.in/yaml.v3 v3.0.1 // indirect
31+
)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
2+
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
3+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5+
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
6+
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
7+
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
8+
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
9+
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
10+
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
11+
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
12+
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
13+
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
14+
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
15+
github.com/gofiber/fiber/v2 v2.52.10 h1:jRHROi2BuNti6NYXmZ6gbNSfT3zj/8c0xy94GOU5elY=
16+
github.com/gofiber/fiber/v2 v2.52.10/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
17+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
18+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
19+
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
20+
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
21+
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
22+
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
23+
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
24+
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
25+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
26+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
27+
github.com/mattn/go-runewidth v0.0.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkENfrjQ=
28+
github.com/mattn/go-runewidth v0.0.17/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
29+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
30+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
31+
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
32+
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
33+
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
34+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
35+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
36+
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
37+
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
38+
github.com/valyala/fasthttp v1.66.0 h1:M87A0Z7EayeyNaV6pfO3tUTUiYO0dZfEJnRGXTVNuyU=
39+
github.com/valyala/fasthttp v1.66.0/go.mod h1:Y4eC+zwoocmXSVCB1JmhNbYtS7tZPRI2ztPB72EVObs=
40+
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
41+
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
42+
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
43+
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
44+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
45+
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
46+
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
47+
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
48+
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
49+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
50+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
51+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
52+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package main
2+
3+
import (
4+
"log"
5+
6+
"github.com/gofiber/fiber/v2"
7+
fiberoapi "github.com/labbs/fiber-oapi"
8+
)
9+
10+
// CustomErrorResponse is the structure for custom validation error responses
11+
type CustomErrorResponse struct {
12+
Success bool `json:"success"`
13+
Message string `json:"message"`
14+
Code string `json:"code"`
15+
}
16+
17+
// CreateUserInput is the input structure for creating a user
18+
type CreateUserInput struct {
19+
Name string `json:"name" validate:"required,min=3"`
20+
Email string `json:"email" validate:"required,email"`
21+
Age int `json:"age" validate:"required,min=18,max=100"`
22+
}
23+
24+
// CreateUserOutput is the output structure for creating a user
25+
type CreateUserOutput struct {
26+
ID int `json:"id"`
27+
Name string `json:"name"`
28+
Email string `json:"email"`
29+
Age int `json:"age"`
30+
Message string `json:"message"`
31+
}
32+
33+
func main() {
34+
app := fiber.New()
35+
36+
// Configure fiber-oapi with a custom validation error handler
37+
oapi := fiberoapi.New(app, fiberoapi.Config{
38+
EnableValidation: true,
39+
EnableOpenAPIDocs: true,
40+
// Define your custom handler for validation errors
41+
ValidationErrorHandler: func(c *fiber.Ctx, err error) error {
42+
// You can parse the validation error to extract more details
43+
// or simply return your custom structure
44+
return c.Status(fiber.StatusBadRequest).JSON(CustomErrorResponse{
45+
Success: false,
46+
Message: err.Error(),
47+
Code: "VALIDATION_ERROR",
48+
})
49+
},
50+
})
51+
52+
// Define your endpoint
53+
fiberoapi.Post[CreateUserInput, CreateUserOutput, struct{}](
54+
oapi,
55+
"/users",
56+
func(c *fiber.Ctx, input CreateUserInput) (CreateUserOutput, struct{}) {
57+
// User creation logic goes here
58+
return CreateUserOutput{
59+
ID: 1,
60+
Name: input.Name,
61+
Email: input.Email,
62+
Age: input.Age,
63+
Message: "User created successfully",
64+
}, struct{}{}
65+
},
66+
fiberoapi.OpenAPIOptions{
67+
Summary: "Create a new user",
68+
Description: "Creates a new user with validation",
69+
Tags: []string{"users"},
70+
},
71+
)
72+
73+
log.Println("Server starting on :3000")
74+
log.Println("OpenAPI docs available at http://localhost:3000/docs")
75+
log.Fatal(oapi.Listen(":3000"))
76+
}

0 commit comments

Comments
 (0)