Skip to content

Commit a78b685

Browse files
feat: implement structured validation error responses (#38)
* feat(errors): add structured validation error proto definitions - Add ValidationError and FieldViolation messages - Support field-level validation error details - Enable JSON/protobuf serialization for error responses 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * feat(httpgen): implement structured validation error responses - Add writeValidationErrorResponse() for content-type aware error serialization - Add writeValidationError() to convert protovalidate errors to ValidationError - Update validateHeaders() to return ValidationError directly - Replace plain text http.Error() calls with structured error responses - Support both JSON and protobuf error response formats based on content-type - Extract field paths from protovalidate violations for detailed error reporting 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * build(proto): add errors.proto to proto generation target - Include errors.proto in protoc compilation - Add proper go_opt mapping for errors.proto 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor(examples): simplify simple-api by removing admin service - Remove admin.proto and admin_service.proto - Focus example on core user service functionality - Reduce complexity for testing and demonstration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * build(examples): use local sebuf module with replace directive - Add replace directive to use local sebuf for testing - Ensure examples use latest local changes during development 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * chore(proto): regenerate proto files with latest protoc - Update annotations.pb.go and headers.pb.go - Regenerated with protoc v6.32.0 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * docs(validation): update error handling for structured responses - Document ValidationError message structure with field violations - Update error examples to show JSON response format - Add examples of multiple validation failures - Update features list to include structured errors and content-type awareness - Show both header and body validation error responses 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * docs(http-generation): update for structured validation errors - Update generated validation code examples to show ValidationError returns - Modify header validation examples to show JSON error responses - Add structured errors to features list - Update curl examples with proper JSON error format 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * docs(examples): update simple-api README for structured errors - Update validation error examples to show JSON response format - Add examples of multiple validation failures - Remove references to deleted AdminService - Simplify OpenAPI documentation references - Add structured error responses to features list 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * docs: add structured error responses to main feature list 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * feat(openapiv3): add ValidationError schemas and 400 responses - Add ValidationError and FieldViolation schemas to OpenAPI components - Include 400 validation error responses in all method specifications - Provide comprehensive error documentation for API consumers - Support both request body and header validation error reporting 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * style(httpgen): improve function signature formatting - Format long function signature lines for better readability - Maintain consistent code style across generator functions 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor(httpgen): break down long functions to fix linting issues - Split generateErrorResponseFunctions into smaller helper functions - Split generateValidateHeadersFunction into smaller helper functions - Fix funlen linting violations (functions > 50 statements) - Maintain same generated code behavior with improved readability 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * test(openapiv3): update golden files for ValidationError schemas - Add FieldViolation and ValidationError schemas to all golden files - Include 400 validation error responses in method specifications - Update both YAML and JSON golden file formats - Reflect new OpenAPI generator behavior for validation errors 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * chore(test): remove temporary golden file artifacts - Remove .generated files created during golden file testing - Clean up test artifacts that shouldn't be committed - These files are created during UPDATE_GOLDEN test runs 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Revert "chore(test): remove temporary golden file artifacts" This reverts commit d6db39d. * chore(test): remove .generated test artifacts from git history - Remove temporary .generated files created during golden file testing - These files are test artifacts and shouldn't be committed - They are created during UPDATE_GOLDEN test runs for comparison 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * chore: add .generated files to .gitignore - Prevent temporary .generated test files from being committed - These files are created during golden file testing for comparison - Should be automatically cleaned up after test runs 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 724ab8a commit a78b685

40 files changed

+1040
-158
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,6 @@ internal/generated/proto/
6262

6363
# Generated sqlc files
6464
internal/modules/*/internal/infrastructure/persistence/sqlc/
65+
66+
# Temporary test files
67+
*.generated

Makefile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,11 @@ proto:
110110
@protoc --go_out=. --go_opt=module=github.com/SebastienMelki/sebuf \
111111
--go_opt=Msebuf/http/annotations.proto=github.com/SebastienMelki/sebuf/http \
112112
--go_opt=Msebuf/http/headers.proto=github.com/SebastienMelki/sebuf/http \
113+
--go_opt=Msebuf/http/errors.proto=github.com/SebastienMelki/sebuf/http \
113114
--proto_path=. \
114115
proto/sebuf/http/annotations.proto \
115-
proto/sebuf/http/headers.proto
116+
proto/sebuf/http/headers.proto \
117+
proto/sebuf/http/errors.proto
116118

117119
# Publish annotations to Buf Schema Registry
118120
.PHONY: publish

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ This starts a working HTTP API with JSON endpoints, OpenAPI docs, and helper fun
3333
- **Mock server generation** with realistic field examples for rapid prototyping
3434
- **Automatic request validation** using protovalidate with buf.validate annotations
3535
- **HTTP header validation** with type checking and format validation (UUID, email, datetime)
36+
- **Structured error responses** with field-level validation details in JSON or protobuf
3637
- **OpenAPI v3.1 docs** that stay in sync with your code, one file per service for better organization
3738
- **Helper functions** that eliminate protobuf boilerplate
3839
- **Zero runtime dependencies** - works with any Go HTTP framework

docs/http-generation.md

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -608,33 +608,39 @@ service UserService {
608608

609609
### Generated Validation Code
610610

611-
The plugin generates header validation middleware that's automatically applied:
611+
The plugin generates header validation that returns structured errors:
612612

613613
```go
614-
// Generated middleware validates headers before processing requests
615-
func validateHeaders(requiredHeaders []HeaderConfig) func(http.Handler) http.Handler {
616-
return func(next http.Handler) http.Handler {
617-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
618-
// Validate each required header
619-
for _, header := range requiredHeaders {
620-
value := r.Header.Get(header.Name)
621-
622-
// Check required headers
623-
if header.Required && value == "" {
624-
http.Error(w, fmt.Sprintf("Missing required header: %s", header.Name), 400)
625-
return
626-
}
627-
628-
// Validate type and format
629-
if err := validateHeaderValue(value, header.Type, header.Format); err != nil {
630-
http.Error(w, fmt.Sprintf("Invalid header %s: %v", header.Name, err), 400)
631-
return
632-
}
633-
}
634-
635-
next.ServeHTTP(w, r)
636-
})
614+
// Generated validation returns ValidationError with field-level violations
615+
func validateHeaders(r *http.Request, serviceHeaders, methodHeaders []*Header) *ValidationError {
616+
var violations []*FieldViolation
617+
allHeaders := mergeHeaders(serviceHeaders, methodHeaders)
618+
619+
for _, header := range allHeaders {
620+
value := r.Header.Get(header.Name)
621+
622+
// Check required headers
623+
if header.Required && value == "" {
624+
violations = append(violations, &FieldViolation{
625+
Field: header.Name,
626+
Description: fmt.Sprintf("required header '%s' is missing", header.Name),
627+
})
628+
continue
629+
}
630+
631+
// Validate type and format
632+
if err := validateHeaderValue(header, value); err != nil {
633+
violations = append(violations, &FieldViolation{
634+
Field: header.Name,
635+
Description: fmt.Sprintf("header '%s' validation failed: %v", header.Name, err),
636+
})
637+
}
638+
}
639+
640+
if len(violations) > 0 {
641+
return &ValidationError{Violations: violations}
637642
}
643+
return nil
638644
}
639645
```
640646

@@ -697,15 +703,29 @@ curl -X POST http://localhost:8080/api/v1/users \
697703
curl -X POST http://localhost:8080/api/v1/users \
698704
-H "Content-Type: application/json" \
699705
-d '{"name": "John", "email": "john@example.com"}'
700-
# Response: 400 Bad Request - Missing required header: X-API-Key
706+
# Response: 400 Bad Request
707+
# Body:
708+
{
709+
"violations": [{
710+
"field": "X-API-Key",
711+
"description": "required header 'X-API-Key' is missing"
712+
}]
713+
}
701714
702715
# Invalid header format (returns 400)
703716
curl -X POST http://localhost:8080/api/v1/users \
704717
-H "Content-Type: application/json" \
705718
-H "X-API-Key: not-a-uuid" \
706719
-H "X-Tenant-ID: 42" \
707720
-d '{"name": "John", "email": "john@example.com"}'
708-
# Response: 400 Bad Request - Invalid header X-API-Key: invalid UUID format
721+
# Response: 400 Bad Request
722+
# Body:
723+
{
724+
"violations": [{
725+
"field": "X-API-Key",
726+
"description": "header 'X-API-Key' validation failed: invalid UUID format"
727+
}]
728+
}
709729
```
710730

711731
## Generated Code Structure

docs/validation.md

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ That's it! Both header and body validation happen automatically in your HTTP han
6565
-**Header type validation** - Support for string, integer, number, boolean, array types
6666
-**Header format validation** - Built-in validators for UUID, email, datetime formats
6767
-**Performance optimized** - Cached validator instances
68-
-**Clear error messages** - HTTP 400 with detailed validation errors
68+
-**Structured error responses** - JSON or protobuf ValidationError with field-level details
69+
-**Content-type aware** - Error format matches client's requested content type
6970
-**Fail-fast validation** - Headers validated before body for efficiency
7071

7172
## Request Body Validation Rules
@@ -291,48 +292,121 @@ service SecureAPI {
291292

292293
## Error Handling
293294

294-
When validation fails, sebuf returns an HTTP 400 Bad Request with the validation error message. Headers are validated before the request body.
295+
When validation fails, sebuf returns an HTTP 400 Bad Request with a structured error response. The response format respects the client's `Content-Type` header, returning either JSON or protobuf. Headers are validated before the request body.
296+
297+
### Structured Error Response Format
298+
299+
Validation errors are returned as a `ValidationError` message containing field-level violations:
300+
301+
```protobuf
302+
message ValidationError {
303+
repeated FieldViolation violations = 1;
304+
}
305+
306+
message FieldViolation {
307+
string field = 1; // Field that failed validation
308+
string description = 2; // Description of the violation
309+
}
310+
```
295311

296312
### Header Validation Errors
297313

298314
```bash
299315
# Missing required header
300-
curl -X POST /api/users -d '{"name": "John"}'
316+
curl -X POST /api/users \
317+
-H "Content-Type: application/json" \
318+
-d '{"name": "John"}'
301319
# Returns: 400 Bad Request
302-
# Body: "Missing required header: X-API-Key"
320+
# Body:
321+
{
322+
"violations": [{
323+
"field": "X-API-Key",
324+
"description": "required header 'X-API-Key' is missing"
325+
}]
326+
}
303327

304328
# Invalid header format (UUID)
305329
curl -X POST /api/users \
330+
-H "Content-Type: application/json" \
306331
-H "X-API-Key: not-a-uuid" \
307332
-d '{"name": "John"}'
308333
# Returns: 400 Bad Request
309-
# Body: "Invalid header X-API-Key: invalid UUID format"
334+
# Body:
335+
{
336+
"violations": [{
337+
"field": "X-API-Key",
338+
"description": "header 'X-API-Key' validation failed: invalid UUID format"
339+
}]
340+
}
310341

311342
# Invalid header type (expecting integer)
312343
curl -X POST /api/users \
344+
-H "Content-Type: application/json" \
313345
-H "X-API-Key: 123e4567-e89b-12d3-a456-426614174000" \
314346
-H "X-Tenant-ID: abc" \
315347
-d '{"name": "John"}'
316348
# Returns: 400 Bad Request
317-
# Body: "Invalid header X-Tenant-ID: must be an integer"
349+
# Body:
350+
{
351+
"violations": [{
352+
"field": "X-Tenant-ID",
353+
"description": "header 'X-Tenant-ID' validation failed: value is not a valid integer"
354+
}]
355+
}
318356
```
319357

320358
### Body Validation Errors
321359

322360
```bash
323361
# Invalid email
324362
curl -X POST /api/users \
363+
-H "Content-Type: application/json" \
325364
-H "X-API-Key: 123e4567-e89b-12d3-a456-426614174000" \
326365
-d '{"email": "invalid"}'
327366
# Returns: 400 Bad Request
328-
# Body: "validation error: field 'email' with value 'invalid' failed rule 'string.email'"
367+
# Body:
368+
{
369+
"violations": [{
370+
"field": "email",
371+
"description": "value must be a valid email address"
372+
}]
373+
}
329374

330-
# Name too short
375+
# Multiple validation failures
331376
curl -X POST /api/users \
377+
-H "Content-Type: application/json" \
332378
-H "X-API-Key: 123e4567-e89b-12d3-a456-426614174000" \
333-
-d '{"name": "J"}'
379+
-d '{"name": "J", "email": "invalid", "age": 200}'
334380
# Returns: 400 Bad Request
335-
# Body: "validation error: field 'name' with value 'J' failed rule 'string.min_len'"
381+
# Body:
382+
{
383+
"violations": [
384+
{
385+
"field": "name",
386+
"description": "value length must be at least 2 runes"
387+
},
388+
{
389+
"field": "email",
390+
"description": "value must be a valid email address"
391+
},
392+
{
393+
"field": "age",
394+
"description": "value must be less than or equal to 120"
395+
}
396+
]
397+
}
398+
```
399+
400+
### Binary Response Format
401+
402+
When using protobuf content type, errors are returned as binary protobuf:
403+
404+
```bash
405+
curl -X POST /api/users \
406+
-H "Content-Type: application/x-protobuf" \
407+
-H "X-API-Key: invalid" \
408+
--data-binary @request.pb
409+
# Returns: 400 Bad Request with binary ValidationError protobuf
336410
```
337411

338412
## Advanced Usage

examples/simple-api/Makefile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ clean:
4040
@rm -f *.pb.go
4141
@rm -f *_helpers.pb.go
4242
@rm -f *_http*.pb.go
43-
@rm -f *.yaml *.json
4443
@echo "✅ Cleaned generated files"
4544

4645
# Test the API endpoints

0 commit comments

Comments
 (0)