Handling odd JSON decoding in Go

I was recently writing a client for a JSON-based API that had some unique behavior. The API lets you look up ham radio callsigns and their associated information. If the callsign exists you get back a JSON object with those details.
The JSON of a valid callsign is as follows (redacted for privacy):
{
"hamdb":{
"version":"1",
"callsign":{
"call":"redacted",
"class":"E",
"expires":"09/25/2025",
"status":"A",
"grid":"redacted",
"lat":"redacted",
"lon":"redacted",
"fname":"redacted",
"mi":"redacted",
"name":"redacted",
"suffix":"redacted",
"addr1":"redacted",
"addr2":"redacted",
"state":"redacted",
"zip":"redacted",
"country":"United States"
},
"messages":{
"status":"OK"
}
}
}
But look what happens when you query for a callsign that can't be found:
{
"hamdb":{
"version":"1",
"callsign":{
"call":"NOT_FOUND",
"class":"NOT_FOUND",
"expires":"NOT_FOUND",
"status":"NOT_FOUND",
"grid":"NOT_FOUND",
"lat":"NOT_FOUND",
"lon":"NOT_FOUND",
"fname":"NOT_FOUND",
"mi":"NOT_FOUND",
"name":"NOT_FOUND",
"suffix":"NOT_FOUND",
"addr1":"NOT_FOUND",
"addr2":"NOT_FOUND",
"state":"NOT_FOUND",
"zip":"NOT_FOUND",
"country":"NOT_FOUND"
},
"messages":{
"status":"NOT_FOUND"
}
}
}
Every field is NOT_FOUND. This makes unmarshalling that data into a struct a real challenge. The expires field can be either a parseable date or a string!
The solution I came up with was to introduce a new generic type that can handle this behavior:
type Unknowable[K comparable] struct {
Value K
Known bool
}
func (u *Unknowable[K]) UnmarshalJSON(b []byte) error {
s := strings.Replace(string(b), `"`, ``, -1)
switch s {
case "NOT_FOUND":
u.Known = false
return nil
default:
u.Known = true
var v K
switch any(v).(type) {
case float64:
f, err := strconv.ParseFloat(s, 64)
if err != nil {
return err
}
v = any(f).(K)
case int:
f, err := strconv.Atoi(s)
if err != nil {
return err
}
v = any(f).(K)
default:
if err := json.Unmarshal(b, &v); err != nil {
return err
}
}
u.Value = v
}
return nil
}
The goofy any(v).(type) switch is because I couldn't quite figure out how to get the concrete types to parse correctly without doing something different to each of them. This is partly because I wanted to have real types for things like the lat and lon, not just strings.
Now I can represent the types I want to unmarshal within the destination struct directly.
type Callsign struct {
Call string `json:"call"`
Class LicenseClass `json:"class"`
Expires Unknowable[time.Time] `json:"expires"`
Status string `json:"status"`
Grid string `json:"grid"`
Lat Unknowable[float64] `json:"lat"`
Lon Unknowable[float64] `json:"lon"`
FirstName string `json:"fname"`
MiddleInitial string `json:"mi"`
LastName string `json:"name"`
Suffix string `json:"suffix"`
Address string `json:"addr1"`
City string `json:"addr2"`
State string `json:"state"`
Zip Unknowable[int] `json:"zip"`
Country string `json:"country"`
}
This is one of those times where generics in Go has been super helpful. Granted, I'd still like a way to type-switch on a constraint, but this is still much cleaner than having multiple types with nearly identical implementations and names.
