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
- Requests - Handling requests
- Responses - Sending responses
- Error Handling - Managing errors