Search code examples
goreddit

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


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