Problem
Same field returns different JSON types depending on data:
"min_price": 0
"min_price": {"excl_vat": 50.5, "incl_vat": 60.63}
"min_price": nullSolution: custom UnmarshalJSON, try simplest format first
func (p *PriceInput) UnmarshalJSON(data []byte) error {
if string(data) == "null" {
p.ExclVAT, p.InclVAT = 0, 0
return nil
}
var num float64
if err := sonic.Unmarshal(data, &num); err == nil {
p.ExclVAT, p.InclVAT = num, num
return nil
}
type Alias PriceInput
aux := &struct{ *Alias }{Alias: (*Alias)(p)}
return sonic.Unmarshal(data, aux)
}Type alias pattern prevents infinite recursion
Without the alias, calling sonic.Unmarshal(data, p) inside UnmarshalJSON calls itself forever. The type Alias creates a new type with no custom unmarshaler, so the fallback uses the default behavior.
MongoDB ObjectId: string or populated object
Same pattern — try string first, fall back to object:
func (u *InternalUserIDInput) UnmarshalJSON(data []byte) error {
var id string
if err := sonic.Unmarshal(data, &id); err == nil {
u.ID = id
return nil
}
type Alias InternalUserIDInput
aux := &struct{ *Alias }{Alias: (*Alias)(u)}
return sonic.Unmarshal(data, aux)
}Schema vs reality
Always test against the actual API response, not the schema docs. A serialization layer (e.g. Mongoose’s toJSON()) may convert the type before it leaves the server — schema says number, wire sends ISO 8601 string.