DTOs & Validation

Data Transfer Objects (DTOs) and validation ensure data integrity in your Goose applications.

Data Transfer Objects

DTOs define the shape of data for requests and responses:

// Request DTO
type CreateUserDTO struct {
    Name     string `json:"name" validate:"required"`
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"password" validate:"required,min=8"`
}

// Response DTO
type UserResponseDTO struct {
    ID    string `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
    // Password excluded from response
}

Validation Tags

Use struct tags for declarative validation:

Required Fields

type CreateUserDTO struct {
    Name  string `json:"name" validate:"required"`
    Email string `json:"email" validate:"required"`
}

String Validation

type ProfileDTO struct {
    Username    string `json:"username" validate:"required,min=3,max=20"`
    Bio         string `json:"bio" validate:"max=500"`
    Website     string `json:"website" validate:"omitempty,url"`
    TwitterHandle string `json:"twitter" validate:"omitempty,startswith=@"`
}

Numeric Validation

type ProductDTO struct {
    Price    float64 `json:"price" validate:"required,gt=0"`
    Quantity int     `json:"quantity" validate:"required,gte=0,lte=1000"`
    Rating   float64 `json:"rating" validate:"gte=0,lte=5"`
}

Email & URL

type ContactDTO struct {
    Email   string `json:"email" validate:"required,email"`
    Website string `json:"website" validate:"omitempty,url"`
}

Enum/OneOf

type OrderDTO struct {
    Status   string `json:"status" validate:"oneof=pending processing shipped delivered"`
    Priority string `json:"priority" validate:"oneof=low medium high"`
}

Nested Validation

type OrderDTO struct {
    Customer CustomerDTO   `json:"customer" validate:"required"`
    Items    []OrderItemDTO `json:"items" validate:"required,min=1,dive"`
}

type CustomerDTO struct {
    Name  string `json:"name" validate:"required"`
    Email string `json:"email" validate:"required,email"`
}

type OrderItemDTO struct {
    ProductID string `json:"product_id" validate:"required,uuid"`
    Quantity  int    `json:"quantity" validate:"required,gt=0"`
}

Common Validation Tags

Tag Description Example
required Field must be present validate:"required"
email Valid email format validate:"email"
url Valid URL format validate:"url"
uuid Valid UUID format validate:"uuid"
min Minimum length/value validate:"min=3"
max Maximum length/value validate:"max=100"
len Exact length validate:"len=10"
gt Greater than validate:"gt=0"
gte Greater than or equal validate:"gte=0"
lt Less than validate:"lt=100"
lte Less than or equal validate:"lte=100"
oneof Must be one of values validate:"oneof=a b c"
contains Must contain substring validate:"contains=@"
startswith Must start with validate:"startswith=http"
endswith Must end with validate:"endswith=.com"
omitempty Skip if empty validate:"omitempty,email"
dive Validate array elements validate:"dive,required"

Using Validation in Controllers

func (c *UserController) Create(ctx types.Context) any {
    var dto CreateUserDTO

    // Bind request body
    if err := ctx.Bind(&dto); err != nil {
        return ctx.Error(400, "Invalid JSON format")
    }

    // Validate DTO
    if err := ctx.Validate(&dto); err != nil {
        return ctx.Error(422, map[string]any{
            "error": "validation_failed",
            "details": formatValidationErrors(err),
        })
    }

    return c.service.CreateUser(dto)
}

Custom Validation

Custom Validate Method

Implement complex validation logic:

type CreateOrderDTO struct {
    CustomerID string    `json:"customer_id" validate:"required"`
    Items      []ItemDTO `json:"items" validate:"required,min=1,dive"`
    ScheduledAt *time.Time `json:"scheduled_at"`
}

// Custom validation method
func (dto *CreateOrderDTO) Validate() error {
    // Check scheduled date is in the future
    if dto.ScheduledAt != nil && dto.ScheduledAt.Before(time.Now()) {
        return fmt.Errorf("scheduled_at must be in the future")
    }

    // Check for duplicate items
    seen := make(map[string]bool)
    for _, item := range dto.Items {
        if seen[item.ProductID] {
            return fmt.Errorf("duplicate product: %s", item.ProductID)
        }
        seen[item.ProductID] = true
    }

    return nil
}

Using Custom Validation

func (c *OrderController) Create(ctx types.Context) any {
    var dto CreateOrderDTO
    ctx.Bind(&dto)

    // Tag-based validation
    if err := ctx.Validate(&dto); err != nil {
        return ctx.Error(422, err.Error())
    }

    // Custom validation
    if err := dto.Validate(); err != nil {
        return ctx.Error(422, err.Error())
    }

    return c.service.CreateOrder(dto)
}

Validation Error Formatting

Format validation errors for API responses:

func formatValidationErrors(err error) []map[string]string {
    var errors []map[string]string

    for _, e := range err.(validator.ValidationErrors) {
        errors = append(errors, map[string]string{
            "field":   e.Field(),
            "tag":     e.Tag(),
            "message": getErrorMessage(e),
        })
    }

    return errors
}

func getErrorMessage(e validator.FieldError) string {
    switch e.Tag() {
    case "required":
        return fmt.Sprintf("%s is required", e.Field())
    case "email":
        return fmt.Sprintf("%s must be a valid email", e.Field())
    case "min":
        return fmt.Sprintf("%s must be at least %s", e.Field(), e.Param())
    case "max":
        return fmt.Sprintf("%s must be at most %s", e.Field(), e.Param())
    default:
        return fmt.Sprintf("%s is invalid", e.Field())
    }
}

DTO Patterns

Request/Response DTOs

// Request DTOs (input)
type CreateUserRequest struct {
    Name     string `json:"name" validate:"required"`
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"password" validate:"required,min=8"`
}

// Response DTOs (output)
type UserResponse struct {
    ID        string    `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    CreatedAt time.Time `json:"created_at"`
    // Password excluded
}

// Conversion
func ToUserResponse(user *User) *UserResponse {
    return &UserResponse{
        ID:        user.ID,
        Name:      user.Name,
        Email:     user.Email,
        CreatedAt: user.CreatedAt,
    }
}

Query DTOs

type ListUsersQuery struct {
    Page    int    `query:"page" validate:"gte=1"`
    Limit   int    `query:"limit" validate:"gte=1,lte=100"`
    Search  string `query:"search" validate:"max=100"`
    SortBy  string `query:"sort_by" validate:"oneof=name email created_at"`
    Order   string `query:"order" validate:"oneof=asc desc"`
}

func (c *UserController) List(ctx types.Context) any {
    query := ListUsersQuery{
        Page:  1,
        Limit: 10,
        Order: "asc",
    }
    ctx.BindQuery(&query)
    ctx.Validate(&query)

    return c.service.ListUsers(query)
}

Best Practices

1. Separate Request and Response DTOs

// โœ… Good: Separate DTOs
type CreateUserRequest struct { ... }  // Input
type UserResponse struct { ... }       // Output

// โŒ Bad: Same DTO for both
type UserDTO struct { ... }  // Used for input and output

2. Keep DTOs Simple

// โœ… Good: Flat structure
type CreateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

// โŒ Bad: Unnecessary nesting
type CreateUserRequest struct {
    User struct {
        Info struct {
            Name string `json:"name"`
        } `json:"info"`
    } `json:"user"`
}

3. Use Specific Types

// โœ… Good: Specific types
type UpdateOrderRequest struct {
    Status OrderStatus `json:"status" validate:"required"`
}

type OrderStatus string
const (
    OrderStatusPending OrderStatus = "pending"
    OrderStatusShipped OrderStatus = "shipped"
)

// โŒ Bad: Generic string
type UpdateOrderRequest struct {
    Status string `json:"status"`
}

Next Steps