skip to Main Content

Assume I have a JSON message that looks like this:

{
    "type": string
    "data": list of something that is based on above type
}

two examples might be

{
    "type": "car"
    "data": [{"color": "red", "mpg": 16.4}]
}

and

{
    "type": "house"
    "data": [{"color": "blue", "state": "CA"}]
}

I want to define a struct so I can basically only decode the type and then use that to properly unmarshal the data field. I tried the following

type message struct {
    Type string `json:"type"`
    Data []byte `json:"data"`
}

type car struct {
    Color string
    MPG   float64
}

type house struct {
    Color string
    State string
}

erroneously thinking that it would just leave the Data field raw for me to unmarshal later into either the car or house struct. I know I can define Data as a []interface{} and do some other work to get what I want, but I was wondering if this is (currently) the best way in Go? In case it comes up, assume I cannot change the JSON definitions – I’m just the consumer of a service here.

2

Answers


  1. This is a perfect use-case for json.RawMessage. Check out the Unmarshal example there.

    For your example, this would look something like:

    type message struct {
        Type string            `json:"type"`
        Data []json.RawMessage `json:"data"`
    }
    
    type car struct {
        Color string
        MPG   float64
    }
    
    type house struct {
        Color string
        State string
    }
    
    func main() {
        if err := parseAndPrint(carJSON); err != nil {
            panic(err)
        }
        if err := parseAndPrint(houseJSON); err != nil {
            panic(err)
        }
    }
    
    func parseAndPrint(b []byte) error {
        msg := new(message)
        if err := json.Unmarshal(b, msg); err != nil {
            panic(err)
        }
    
        switch msg.Type {
        case "car":
            for _, data := range msg.Data {
                c := new(car)
                if err := json.Unmarshal(data, c); err != nil {
                    return err
                }
                fmt.Println(c)
            }
        case "house":
            for _, data := range msg.Data {
                h := new(house)
                if err := json.Unmarshal(data, h); err != nil {
                    return err
                }
                fmt.Println(h)
            }
        }
        return nil
    }
    
    
    // Tucked here to get out of the way of the example
    var carJSON = []byte(`
    {
        "type": "car",
        "data": [{"color": "red", "mpg": 16.4}]
    }
    `)
    
    var houseJSON = []byte(`{
        "type": "house",
        "data": [{"color": "blue", "state": "CA"}]
    }
    `)
    

    Now what you do with the parsed result is kinda up to you and your program’s needs. Parse at the edge and only pass down the fully-typed message, add a Parsed any field to the outer struct, etc.

    Login or Signup to reply.
  2. type message struct {
        Type string
        Data any
    }
    
    func (m *message) UnmarshalJSON(data []byte) error {
        var obj struct {
            Type string          `json:"type"`
            Data json.RawMessage `json:"data"`
        }
        if err := json.Unmarshal(data, &obj); err != nil {
            return err
        }
    
        var val any
        var err error
        switch obj.Type {
        case "car":
            val = decodeArrayOf[car](obj.Data, &err)
    
        case "house":
            val = decodeArrayOf[house](obj.Data, &err)
        }
        if err != nil {
            return err
        }
    
        m.Type = obj.Type
        m.Data = val
        return nil
    }
    
    func decodeArrayOf[T any](data []byte, err *error) []*T {
        v := []*T{}
        if e := json.Unmarshal(data, &v); e != nil {
            *err = e
            return nil
        }
        return v
    }
    

    https://go.dev/play/p/5UPrW27LWlX

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search