Type Reference
Supported Types
| Type | Support | Coercion | Notes |
|---|---|---|---|
Primitives (string, int, float64, bool) |
Full | Yes | All validation tags work |
Slices & Arrays ([]T, [N]T) |
Full | Yes | Element validation supported |
| Nested Structs | Full | Yes | Recursive validation |
Pointers (*T) |
Full | Yes | Nil handling for optional fields |
time.Time |
Full | Yes | RFC3339, Unix timestamps |
json.RawMessage |
Full | Yes | Preserves raw JSON |
Maps (map[string]T) |
Partial | Limited | Structure only, no element validation |
interface{} |
Partial | Yes | Runtime detection, limited validation |
Custom types with UnmarshalJSON |
Compatible | N/A | Standard library patterns work |
Type Coercion
gopantic automatically converts between compatible types:
// String to number
type Product struct {
Price float64 `json:"price"`
}
// {"price": "19.99"} → Product{Price: 19.99}
// Number to string
type Request struct {
ID string `json:"id"`
}
// {"id": 12345} → Request{ID: "12345"}
// String to boolean
// "true", "false", "1", "0" all work
// Unix timestamp to time.Time
// 1704067200 → parsed time.Time
Performance: Coercion adds ~5-10% overhead. If input is already correctly typed:
json.RawMessage
Preserve raw JSON for flexible metadata or deferred parsing:
type Account struct {
ID string `json:"id" validate:"required"`
MetadataRaw json.RawMessage `json:"metadata,omitempty"`
}
// Parse with validation
account, err := model.ParseInto[Account](input)
// Later: parse metadata as needed
var metadata map[string]interface{}
json.Unmarshal(account.MetadataRaw, &metadata)
Common use cases:
- PostgreSQL JSONB columns
- Plugin/extension systems
- Multi-tenant configurations
- Event payloads with varying schemas
Nested Structs
Recursive validation works automatically:
type Address struct {
City string `json:"city" validate:"required"`
ZipCode string `json:"zip_code" validate:"len=5"`
}
type User struct {
Name string `json:"name" validate:"required"`
Address Address `json:"address" validate:"required"`
}
// Both User and Address are validated
user, err := model.ParseInto[User](input)
Optional nested structs:
type User struct {
Name string `json:"name" validate:"required"`
Address *Address `json:"address,omitempty"` // Optional
}
Slices and Arrays
type UserList struct {
Users []User `json:"users" validate:"required,min=1"`
}
// Each User element is validated
Primitive slices work too:
Pointers (Optional Fields)
Use pointers to distinguish missing from zero values:
type UpdateRequest struct {
Name *string `json:"name,omitempty"` // nil = not provided
Age *int `json:"age,omitempty"` // 0 vs nil
Email *string `json:"email,omitempty" validate:"email"`
}
// {"name": "Alice"} → Name="Alice", Age=nil, Email=nil
Custom Types
Implement UnmarshalJSON for custom parsing:
type CustomID string
func (c *CustomID) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
if !strings.HasPrefix(s, "id_") {
return errors.New("invalid ID format")
}
*c = CustomID(s)
return nil
}
type Request struct {
ID CustomID `json:"id" validate:"required"`
}
Time Handling
Automatic support for:
- RFC3339:
"2024-01-01T12:00:00Z" - Unix timestamps:
1704067200 - ISO 8601:
"2024-01-01T12:00:00+00:00"
For custom formats, implement UnmarshalJSON:
type CustomTime time.Time
func (ct *CustomTime) UnmarshalJSON(data []byte) error {
var s string
json.Unmarshal(data, &s)
t, err := time.Parse("2006-01-02", s)
if err != nil {
return err
}
*ct = CustomTime(t)
return nil
}
Limitations and Workarounds
Maps (Limited Validation)
Problem: Can't validate individual map values
type Config struct {
Settings map[string]string `json:"settings" validate:"required"`
// Only validates existence/length, not individual values
}
Workaround: Use json.RawMessage + manual validation
type Config struct {
SettingsRaw json.RawMessage `json:"settings,omitempty"`
}
func (c *Config) ValidateSettings() error {
var settings map[string]string
json.Unmarshal(c.SettingsRaw, &settings)
for key, value := range settings {
if len(value) < 3 {
return fmt.Errorf("setting %s: value too short", key)
}
}
return nil
}
interface{} (Limited Validation)
Problem: Can't validate beyond type checking
type Flexible struct {
Data interface{} `json:"data" validate:"required"`
// "required" works, but no deep validation
}
Workaround: Type assertions
func (f *Flexible) ValidateData() error {
switch v := f.Data.(type) {
case string:
if len(v) < 3 {
return errors.New("string too short")
}
case map[string]interface{}:
if len(v) == 0 {
return errors.New("map empty")
}
}
return nil
}
Circular References
Problem: Causes infinite loops
type Node struct {
Children []Node `json:"children"` // OK
Parent *Node `json:"parent"` // Circular!
}
Workaround: Use IDs
type Node struct {
ID string `json:"id"`
ParentID string `json:"parent_id,omitempty"`
Children []Node `json:"children"`
}
Non-String Map Keys
Problem: JSON limitation
type Data struct {
Counts map[int]string `json:"counts"`
// Keys serialized as strings: {"1": "one"}
}
Workaround: Use string keys or implement UnmarshalJSON
Function/Channel Fields
Must be skipped:
Edge Cases
Zero Values vs Missing Fields
JSON can't distinguish:
Use pointers for optional fields:
type Request struct {
Count *int `json:"count,omitempty"`
}
// {} → Count: nil
// {"count": 0} → Count: &0
Large json.RawMessage
json.RawMessage keeps entire JSON in memory. For large fields (>1MB), consider:
- External storage (S3, database BLOB)
- Streaming/chunked processing
- Compression
Array Length Validation
Fixed-length array mismatches may not be caught in all cases:
type Data struct {
Scores [3]float64 `json:"scores"`
}
// {"scores": [1.0]} may not error (known limitation)
Use slices with length validation:
Performance Tips
- Skip coercion if types are correct: Use
json.Unmarshal+model.Validate() - Use pointers sparingly: Only for truly optional fields
- Avoid deep nesting: Flatten structures when possible
- json.RawMessage for large/dynamic data: More efficient than map[string]interface{}
- Cache parsed metadata: Don't unmarshal RawMessage repeatedly
Design Philosophy
Type Coercion: gopantic prioritizes developer convenience over strict typing. This reduces boilerplate when dealing with APIs that send "123" instead of 123.
Validation Tags: Simple comma-separated syntax for common cases. Use custom validators for complex rules.
Compatibility: Works with standard library patterns (UnmarshalJSON, json.RawMessage) rather than replacing them.
See Also
- API Reference - Complete API documentation
- Migration Guide - Switching from other libraries
- Architecture - Implementation details