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.