# Request/Response Validation Guide

## INT-012: Add request/response validation

**Date**: 2025-12-25  
**Status**: Completed

## Overview

This guide provides comprehensive documentation for request and response validation in the Veza Backend API. It covers validation strategies, best practices, and implementation guidelines.

## Current Implementation

The Veza API uses a centralized validation system built on:
- **go-playground/validator**: Core validation library
- **Gin binding**: Request binding and basic validation
- **Custom validators**: Domain-specific validation rules
- **Centralized helpers**: `BindAndValidateJSON` for consistent validation

## Validation Architecture

### Components

1. **Validator** (`internal/validators/validator.go`)
   - Centralized validator instance
   - Custom validation rules
   - Error message formatting

2. **Common Helpers** (`internal/common/validation.go`)
   - `BindAndValidateJSON`: Main validation helper
   - Error handling
   - Body size limits

3. **Middleware** (`internal/middleware/validation.go`)
   - Query parameter validation
   - Request-level validation

4. **DTOs** (Data Transfer Objects)
   - Request structures with validation tags
   - Response structures (when needed)

## Request Validation

### Using BindAndValidateJSON

The recommended way to validate requests:

```go
func (h *Handler) CreateResource(c *gin.Context) {
    var req CreateResourceRequest
    
    // Validate request
    if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
        RespondWithAppError(c, appErr)
        return
    }
    
    // Process validated request
    // ...
}
```

### Validation Tags

#### Standard Tags (go-playground/validator)

```go
type CreateUserRequest struct {
    // Required fields
    Email    string `json:"email" binding:"required,email" validate:"required,email"`
    Username string `json:"username" binding:"required,min=3,max=30" validate:"required,min=3,max=30,username"`
    
    // Optional fields
    Bio      string `json:"bio" binding:"omitempty,max=500" validate:"omitempty,max=500"`
    
    // Numeric validation
    Age      int    `json:"age" binding:"omitempty,min=18,max=120" validate:"omitempty,min=18,max=120"`
    
    // UUID validation
    UserID   string `json:"user_id" binding:"required,uuid" validate:"required,uuid"`
    
    // Enum validation
    Role     string `json:"role" binding:"required,oneof=user admin moderator" validate:"required,oneof=user admin moderator"`
    
    // URL validation
    Website  string `json:"website" binding:"omitempty,url" validate:"omitempty,url"`
}
```

#### Custom Tags

The API includes custom validation tags:

- **`username`**: Alphanumeric + underscore, 3-30 characters
- **`uuid_string`**: UUID format validation
- **`slug`**: URL-friendly slug format
- **`phone`**: International phone number format
- **`date_iso`**: ISO 8601 date format (YYYY-MM-DD)
- **`not_empty`**: Non-empty string after trim

```go
type UpdateProfileRequest struct {
    Username string `json:"username" validate:"required,username"`
    Slug     string `json:"slug" validate:"omitempty,slug"`
    Phone    string `json:"phone" validate:"omitempty,phone"`
    BirthDate string `json:"birth_date" validate:"omitempty,date_iso"`
}
```

### Common Validation Patterns

#### Required Fields

```go
Field string `json:"field" binding:"required" validate:"required"`
```

#### String Length

```go
Title string `json:"title" binding:"required,min=1,max=200" validate:"required,min=1,max=200"`
```

#### Numeric Ranges

```go
Price float64 `json:"price" binding:"required,min=0,gt=0" validate:"required,min=0,gt=0"`
Page  int     `json:"page" binding:"omitempty,min=1" validate:"omitempty,min=1"`
```

#### Enums

```go
Status string `json:"status" binding:"required,oneof=active inactive pending" validate:"required,oneof=active inactive pending"`
```

#### UUIDs

```go
TrackID string `json:"track_id" binding:"required,uuid" validate:"required,uuid"`
```

#### Optional Fields

```go
Description string `json:"description" binding:"omitempty,max=1000" validate:"omitempty,max=1000"`
```

### Query Parameter Validation

Use the query parameter validation middleware:

```go
queryValidation := middleware.NewQueryParamValidation(logger)

router.GET("/tracks", 
    queryValidation.ValidateQueryParams(map[string]string{
        "page":  "numeric,min=1",
        "limit": "numeric,min=1,max=100",
        "sort":  "oneof=asc,desc",
    }),
    handler.ListTracks,
)
```

### Path Parameter Validation

Validate path parameters manually:

```go
func (h *Handler) GetTrack(c *gin.Context) {
    trackID := c.Param("id")
    
    // Validate UUID format
    if _, err := uuid.Parse(trackID); err != nil {
        RespondWithAppError(c, apperrors.NewValidationError("Invalid track ID format"))
        return
    }
    
    // Continue processing
}
```

## Response Validation

### When to Validate Responses

Response validation is typically not needed in production, but can be useful for:
- Development/testing
- API contract testing
- Ensuring consistency

### Response Structure Validation

Ensure responses follow the standard format:

```go
// Standard success response
RespondSuccess(c, http.StatusOK, gin.H{
    "data": result,
})

// Standard error response
RespondWithAppError(c, apperrors.NewValidationError("Validation failed"))
```

### Response Schema Validation (Optional)

For strict response validation, you can validate response structures:

```go
type TrackResponse struct {
    ID       string `json:"id" validate:"required,uuid"`
    Title    string `json:"title" validate:"required,min=1,max=200"`
    Duration int    `json:"duration" validate:"required,min=0"`
}

func validateResponse(data interface{}) error {
    validator := validators.NewValidator()
    errors := validator.Validate(data)
    if len(errors) > 0 {
        return fmt.Errorf("response validation failed: %v", errors)
    }
    return nil
}
```

## Validation Error Format

### Standard Error Response

All validation errors follow the standard API response format:

```json
{
  "success": false,
  "error": {
    "code": 1000,
    "message": "Validation failed",
    "details": [
      {
        "field": "email",
        "message": "The field 'email' must be a valid email address",
        "value": "invalid-email"
      },
      {
        "field": "username",
        "message": "The field 'username' must be at least 3 characters long",
        "value": "ab"
      }
    ],
    "timestamp": "2025-12-25T10:30:00Z",
    "request_id": "550e8400-e29b-41d4-a716-446655440000"
  }
}
```

### Error Codes

- **1000** (`ErrCodeValidation`): General validation error
- **1001**: Required field missing
- **1002**: Invalid format
- **1003**: Value out of range

## Best Practices

### 1. Always Validate Input

```go
// ✅ Good
var req CreateRequest
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
    RespondWithAppError(c, appErr)
    return
}

// ❌ Bad
var req CreateRequest
c.ShouldBindJSON(&req) // No validation, no error handling
```

### 2. Use Both Binding and Validation Tags

```go
// ✅ Good
Email string `json:"email" binding:"required,email" validate:"required,email"`

// ⚠️ Acceptable (but less robust)
Email string `json:"email" binding:"required,email"`
```

### 3. Validate Path Parameters

```go
// ✅ Good
trackID := c.Param("id")
if _, err := uuid.Parse(trackID); err != nil {
    RespondWithAppError(c, apperrors.NewValidationError("Invalid track ID"))
    return
}

// ❌ Bad
trackID := c.Param("id") // No validation
```

### 4. Validate Query Parameters

```go
// ✅ Good
page, err := strconv.Atoi(c.DefaultQuery("page", "1"))
if err != nil || page < 1 {
    RespondWithAppError(c, apperrors.NewValidationError("Invalid page number"))
    return
}

// ✅ Better (using middleware)
queryValidation.ValidateQueryParams(map[string]string{
    "page": "numeric,min=1",
})
```

### 5. Provide Clear Error Messages

```go
// ✅ Good - Custom validator with clear messages
Username string `json:"username" validate:"required,username"`

// ❌ Bad - Generic error
Username string `json:"username" validate:"required"`
```

### 6. Set Appropriate Limits

```go
// ✅ Good - Reasonable limits
Title string `json:"title" validate:"required,min=1,max=200"`

// ❌ Bad - No limits or unrealistic limits
Title string `json:"title" validate:"required"`
```

### 7. Validate Nested Structures

```go
type CreatePlaylistRequest struct {
    Title  string              `json:"title" validate:"required,min=1,max=200"`
    Tracks []TrackReference    `json:"tracks" validate:"omitempty,dive"`
}

type TrackReference struct {
    ID     string `json:"id" validate:"required,uuid"`
    Position int  `json:"position" validate:"omitempty,min=0"`
}
```

## Common Validation Scenarios

### User Registration

```go
type RegisterRequest struct {
    Email           string `json:"email" binding:"required,email" validate:"required,email"`
    Username        string `json:"username" binding:"required,min=3,max=30" validate:"required,min=3,max=30,username"`
    Password        string `json:"password" binding:"required,min=12" validate:"required,min=12"`
    PasswordConfirm string `json:"password_confirm" binding:"required,eqfield=Password" validate:"required,eqfield=Password"`
}
```

### Pagination

```go
type PaginationParams struct {
    Page  int `json:"page" binding:"omitempty,min=1" validate:"omitempty,min=1"`
    Limit int `json:"limit" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"`
}
```

### File Upload

```go
type UploadRequest struct {
    Filename string `json:"filename" binding:"required,min=1,max=255" validate:"required,min=1,max=255"`
    FileSize int64  `json:"file_size" binding:"required,min=1,max=10737418240" validate:"required,min=1,max=10737418240"` // 10GB max
    MimeType string `json:"mime_type" binding:"required" validate:"required"`
}
```

### Date Ranges

```go
type DateRangeRequest struct {
    StartDate string `json:"start_date" binding:"omitempty,date_iso" validate:"omitempty,date_iso"`
    EndDate   string `json:"end_date" binding:"omitempty,date_iso" validate:"omitempty,date_iso"`
}
```

## Testing Validation

### Unit Tests

```go
func TestCreateUserRequest_Validation(t *testing.T) {
    validator := validators.NewValidator()
    
    tests := []struct {
        name    string
        request CreateUserRequest
        wantErr bool
    }{
        {
            name: "valid request",
            request: CreateUserRequest{
                Email:    "user@example.com",
                Username: "testuser",
            },
            wantErr: false,
        },
        {
            name: "invalid email",
            request: CreateUserRequest{
                Email:    "invalid-email",
                Username: "testuser",
            },
            wantErr: true,
        },
        {
            name: "username too short",
            request: CreateUserRequest{
                Email:    "user@example.com",
                Username: "ab",
            },
            wantErr: true,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            errors := validator.Validate(tt.request)
            if (len(errors) > 0) != tt.wantErr {
                t.Errorf("Validation error mismatch: got %v, want %v", errors, tt.wantErr)
            }
        })
    }
}
```

### Integration Tests

```go
func TestCreateUser_Validation(t *testing.T) {
    router := setupTestRouter()
    
    tests := []struct {
        name           string
        body           string
        expectedStatus int
    }{
        {
            name:           "missing email",
            body:           `{"username": "testuser"}`,
            expectedStatus: http.StatusBadRequest,
        },
        {
            name:           "invalid email format",
            body:           `{"email": "invalid", "username": "testuser"}`,
            expectedStatus: http.StatusBadRequest,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            w := httptest.NewRecorder()
            req := httptest.NewRequest("POST", "/api/v1/users", strings.NewReader(tt.body))
            router.ServeHTTP(w, req)
            
            assert.Equal(t, tt.expectedStatus, w.Code)
        })
    }
}
```

## Validation Checklist

When creating a new endpoint:

- [ ] Define request DTO with validation tags
- [ ] Use `BindAndValidateJSON` for request validation
- [ ] Validate path parameters (UUIDs, IDs)
- [ ] Validate query parameters (pagination, filters)
- [ ] Test validation with invalid inputs
- [ ] Test validation with edge cases
- [ ] Ensure error messages are clear
- [ ] Document validation rules in Swagger annotations

## Common Issues and Solutions

### Issue: Validation not working

**Solution**: Ensure both `binding` and `validate` tags are present:

```go
// ✅ Correct
Email string `json:"email" binding:"required,email" validate:"required,email"`
```

### Issue: Custom validator not recognized

**Solution**: Ensure custom validators are registered in `registerCustomValidations`:

```go
v.RegisterValidation("custom_tag", func(fl validator.FieldLevel) bool {
    // Validation logic
    return true
})
```

### Issue: Nested struct validation

**Solution**: Use `dive` tag for nested structures:

```go
Tracks []TrackReference `json:"tracks" validate:"omitempty,dive"`
```

### Issue: Conditional validation

**Solution**: Use custom validator or validate in handler:

```go
// In handler
if req.Type == "premium" && req.Price < 10 {
    RespondWithAppError(c, apperrors.NewValidationError("Premium products must cost at least $10"))
    return
}
```

## References

- [go-playground/validator Documentation](https://pkg.go.dev/github.com/go-playground/validator/v10)
- [Gin Binding Documentation](https://gin-gonic.com/docs/examples/binding-and-validation/)
- `internal/validators/validator.go` - Custom validators
- `internal/common/validation.go` - Validation helpers
- `ERROR_RESPONSE_STANDARD.md` - Error response format

---

**Last Updated**: 2025-12-25  
**Maintained By**: Veza Backend Team

