Type Conversion Guide¶
This guide explains how Schema handles type conversion and the type system.
Automatic Type Conversion¶
Schema uses the mapstructure library for automatic type conversion. This provides flexible conversion between types with sensible defaults.
Supported Conversions¶
String Conversions¶
The most common conversion - from string parameters to typed fields:
| Target Type | Accepted Input | Example Conversion |
|---|---|---|
string |
string, int, bool, float | 42 → "42" |
bool |
bool, int, string ("true", "false", "1", "0") |
"true" → true |
int, int8-int64 |
int, uint, float, bool, string | "42" → 42 |
uint, uint8-uint64 |
int, uint, float, bool, string | "42" → uint(42) |
float32, float64 |
int, uint, float, bool, string | "3.14" → 3.14 |
[]byte |
string, []byte | "hello" → []byte("hello") |
Example¶
type ConfigRequest struct {
Host string `schema:"host"` // "localhost"
Port int `schema:"port"` // "8080" → 8080
Enabled bool `schema:"enabled"` // "true" → true
Timeout float64 `schema:"timeout"` // "30.5" → 30.5
Replicas uint `schema:"replicas"` // "3" → 3
}
// Query: ?host=localhost&port=8080&enabled=true&timeout=30.5&replicas=3
Test:
curl "http://localhost:8080/config?host=localhost&port=8080&enabled=true&timeout=30.5&replicas=3"
Boolean Conversions¶
Multiple representations of boolean values are supported:
| Input | Result |
|---|---|
"true", "True", "TRUE", "1" |
true |
"false", "False", "FALSE", "0", "" |
false |
type FeatureFlags struct {
Debug bool `schema:"debug"` // ?debug=true
Profiling bool `schema:"profiling"` // ?profiling=1
Logging bool `schema:"logging"` // ?logging=false
}
// All of these work:
// ?debug=true
// ?debug=True
// ?debug=TRUE
// ?debug=1
// ?profiling=0
// ?profiling=false
// ?profiling= (empty = false)
Numeric Conversions¶
Integer Types¶
type RangeRequest struct {
Min int `schema:"min"` // ?min=0
Max int `schema:"max"` // ?max=100
Offset int64 `schema:"offset"` // ?offset=1000
Limit uint `schema:"limit"` // ?limit=50
}
// Conversions:
// "42" → 42
// "3.14" → 3 (truncates)
// "0" → 0
// "-10" → -10
Floating Point Types¶
type MathRequest struct {
Latitude float64 `schema:"lat"` // ?lat=40.7128
Longitude float64 `schema:"lon"` // ?lon=-74.0060
Precision float32 `schema:"prec"` // ?prec=0.001
}
// Conversions:
// "3.14" → 3.14
// "42" → 42.0
// "1e-6" → 0.000001
Collection Types¶
Slices and Arrays¶
type FilterRequest struct {
// String slice
Tags []string `schema:"tags"` // ?tags=go&tags=api
// Integer slice
IDs []int `schema:"ids"` // ?ids=1&ids=2&ids=3
// Fixed-size array
Coords [2]float64 `schema:"coords"` // ?coords=40.7&coords=-74.0
}
// Result:
// req.Tags = ["go", "api"]
// req.IDs = [1, 2, 3]
// req.Coords = [40.7, -74.0]
Maps¶
type MetadataRequest struct {
// Map from string to string
Labels map[string]string `schema:"labels,style=deepObject"`
// Map with typed values
Counts map[string]int `schema:"counts,style=deepObject"`
}
// Query: ?labels[env]=prod&labels[region]=us&counts[users]=100&counts[posts]=50
// Result:
// req.Labels = {"env": "prod", "region": "us"}
// req.Counts = {"users": 100, "posts": 50}
Struct Types¶
Nested Structs¶
Nested structs are handled automatically:
type Address struct {
Street string `schema:"street"`
City string `schema:"city"`
ZipCode string `schema:"zip_code"`
}
type UserRequest struct {
Name string `schema:"name"`
Address Address `schema:"address,style=deepObject"`
}
// Query: ?name=Alice&address[street]=123 Main St&address[city]=NYC&address[zip_code]=10001
JSON Body Example:
type CreateUserRequest struct {
Body struct {
Name string `schema:"name"`
Address struct {
Street string `schema:"street"`
City string `schema:"city"`
} `schema:"address"`
} `body:"structured"`
}
// POST with JSON body:
// {
// "name": "Alice",
// "address": {
// "street": "123 Main St",
// "city": "NYC"
// }
// }
Embedded Structs¶
Embedded structs promote their fields to the parent:
type Timestamps struct {
CreatedAt string `schema:"created_at"`
UpdatedAt string `schema:"updated_at"`
}
type Article struct {
Timestamps // Fields promoted to Article
Title string `schema:"title"`
Content string `schema:"content"`
}
// JSON body (promoted fields at top level):
// {
// "title": "My Article",
// "content": "Article content...",
// "created_at": "2024-01-01T00:00:00Z",
// "updated_at": "2024-01-02T00:00:00Z"
// }
// Alternative (named embedded access):
// {
// "title": "My Article",
// "Timestamps": {
// "created_at": "2024-01-01T00:00:00Z",
// "updated_at": "2024-01-02T00:00:00Z"
// }
// }
// Both work!
Pointer Types¶
Detecting Missing vs Zero Values¶
Use pointers to distinguish between missing parameters and zero values:
type UpdateRequest struct {
// Without pointer: 0 could be missing OR explicitly set to 0
Count int `schema:"count"`
// With pointer: nil = missing, *0 = explicitly set to 0
Limit *int `schema:"limit"`
// Pointer to bool: nil = missing
Enabled *bool `schema:"enabled"`
// Pointer to string: nil = missing, *"" = empty string
Name *string `schema:"name"`
}
func handler(w http.ResponseWriter, r *http.Request) {
var req UpdateRequest
codec.DecodeRequest(r, nil, &req)
// Check if Limit was provided
if req.Limit == nil {
// Not provided - use default or skip update
fmt.Println("Limit not provided")
} else {
// Provided - use the value (could be 0)
fmt.Printf("Limit set to %d\n", *req.Limit)
}
// Check Enabled flag
if req.Enabled != nil && *req.Enabled {
fmt.Println("Explicitly enabled")
}
}
Example Scenarios:
| Query String | Count | Limit |
|---|---|---|
?count=0&limit=0 |
0 |
*0 |
?count=5&limit=10 |
5 |
*10 |
| (no params) | 0 |
nil |
?count=0 |
0 |
nil |
Optional Structs¶
type SearchRequest struct {
Query string `schema:"q"`
// Optional filter - nil if not provided
Filter *struct {
Status string `schema:"status"`
Category string `schema:"category"`
} `schema:"filter,style=deepObject"`
}
func handler(w http.ResponseWriter, r *http.Request) {
var req SearchRequest
codec.DecodeRequest(r, nil, &req)
if req.Filter != nil {
// Filter was provided
fmt.Printf("Status: %s, Category: %s\n",
req.Filter.Status, req.Filter.Category)
} else {
// No filter provided
fmt.Println("No filter")
}
}
Default Values¶
Use the default tag to provide fallback values:
type APIRequest struct {
Format string `schema:"format" default:"json"`
PageSize int `schema:"page_size" default:"20"`
Timeout int `schema:"timeout" default:"30"`
Debug bool `schema:"debug" default:"false"`
}
// Query: (empty)
// Result: Format="json", PageSize=20, Timeout=30, Debug=false
// Query: ?format=xml&page_size=50
// Result: Format="xml", PageSize=50, Timeout=30, Debug=false
Defaults with Pointers¶
Defaults work with pointers too:
type Request struct {
Count *int `schema:"count" default:"10"`
}
// Query: (empty)
// Result: req.Count = *10
// Query: ?count=5
// Result: req.Count = *5
Custom Types¶
Type Aliases¶
Type aliases work automatically:
type UserID string
type Count int
type Request struct {
User UserID `schema:"user_id"`
Total Count `schema:"total"`
}
// Query: ?user_id=user123&total=42
// Result: req.User = UserID("user123"), req.Total = Count(42)
Struct Types¶
Custom struct types require JSON body or deep object style:
type Coordinate struct {
Lat float64 `schema:"lat"`
Lon float64 `schema:"lon"`
}
type LocationRequest struct {
// Deep object in query
Location Coordinate `schema:"location,style=deepObject"`
}
// Query: ?location[lat]=40.7128&location[lon]=-74.0060
Time and Date Types¶
For time/date handling, use strings and parse manually:
import "time"
type EventRequest struct {
StartDate string `schema:"start_date"` // "2024-01-01"
EndDate string `schema:"end_date"` // "2024-12-31"
}
func handler(w http.ResponseWriter, r *http.Request) {
var req EventRequest
codec.DecodeRequest(r, nil, &req)
// Parse dates
start, err := time.Parse("2006-01-02", req.StartDate)
if err != nil {
http.Error(w, "Invalid start_date", http.StatusBadRequest)
return
}
end, err := time.Parse("2006-01-02", req.EndDate)
if err != nil {
http.Error(w, "Invalid end_date", http.StatusBadRequest)
return
}
// Use parsed times
fmt.Printf("Range: %s to %s\n", start, end)
}
Custom Type Conversion
For automatic parsing of custom types (like dates, enums, etc.), you can implement Go's encoding.TextUnmarshaler interface. See Extensibility for details on custom type handling.
Next Steps¶
- Parameters Guide - Learn about parameter locations
- Request Bodies Guide - Understand body types
- mapstructure docs - Deep dive into conversions