Go – How to deal with JSON response that has attribute that can be of different types

Issue

Let’s say I have the following struct

type Response struct {
    ID string  `json:"id"`
    Edited int `json:"edited"`
}

type Responses []Response

Then lets say I send a request to an API, but my issue is the API docs and from testing have told me that the edited value can come back as a bool or as a int. This obviously upsets Go and it throws an error when decoding the response body into the struct.

// ... http GET
// response [{"edited": true, id: 1}, {"edited": 1683248234.0, id: 2}]

r := Responses{}
err := json.NewDecoder(resp.Body).Decode(&r)
if err != nil {
    return nil, err
}
defer resp.Body.Close()

How can I handle the above situation where I can’t automatically load it into a struct? I’m assuming I’d need to do it into an interface first then filter the slice and handle the two different Response types in their own structs? But then I can’t combine them!

So I’m thinking of conforming the field to one or the other, bool or int.

n.b. this relates to the Reddit API where some of the fields such as edited, created, created_utc don’t have conforming types.

Solution

As told @mkopriva, the simplest way to handle different type of variable is use interface{} type:

const resp = `[{"edited": true, "id": 1}, {"edited": 1683248234.0, "id": 2}, {"id": 3}]`

type Response struct {
    ID     int         `json:"id"`
    Edited interface{} `json:"edited"`
}

type Responses []Response
r := Responses{}
err := json.Unmarshal([]byte(resp), &r)
if err != nil {
    log.Fatal(err)
}
for _, response := range r {
    switch response.Edited.(type) {
    case float64:
        fmt.Println("float64")
    case bool:
        fmt.Println("bool")
    default:
        fmt.Println("invalid")
    }
}

PLAYGROUND

Also you can define new type with custom json.Unmarshaler implementation:

type Response struct {
    ID     int    `json:"id"`
    Edited Edited `json:"edited"`
}

type Edited struct {
    BoolVal  *bool
    FloatVal *float64
}

func (e *Edited) UnmarshalJSON(data []byte) error {
    boolVal, err := strconv.ParseBool(string(data))
    if err == nil {
        e.BoolVal = &boolVal
        return nil
    }
    floatVal, err := strconv.ParseFloat(string(data), 64)
    if err == nil {
        e.FloatVal = &floatVal
        return nil
    }
    return errors.New("undefined type")
}
r := Responses{}
err := json.Unmarshal([]byte(resp), &r)
if err != nil {
    log.Fatal(err)
}
for _, response := range r {
    edited := response.Edited
    switch {
    case edited.FloatVal != nil:
        fmt.Println("float64")
    case edited.BoolVal != nil:
        fmt.Println("bool")
    default:
        fmt.Println("invalid")
    }
}

PLAYGROUND

Answered By – kozmo

Answer Checked By – David Goodson (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.