Unmarshal JSON Array of arrays in Go

18,411

Solution 1

Do I need to implement my own Unmarshaller for this?

Yes.

You're trying to unmarshal an array into a struct (Point), which means you need to tell the JSON unmarshaler how the array values map to the struct values.

Also note that your tags are incorrect in your Point definition. json tags refer to the key names, but arrays don't have keys (in JavaScript they can be accessed as if they do, but this isn't JavaScript). In other words, json:"0" will only work if your JSON looks like {"0":123}. If you implement your own unmarshaler, you can just get rid of those json tags.

Solution 2

The JSON:

[
  {
    "type": "Car",
    "wheels": 4
  },
  {
    "type": "Motorcycle",
    "wheels": 2
  }
]

The Struct:

type Vehicle struct {
  Type   string
  Wheels int
}

The Unmarshaller:

func TestVehicleUnmarshal(t *testing.T) {
    response := `[{"type": "Car","wheels": 4},{"type": "Motorcycle","wheels": 2}]`

    var vehicles []Vehicle
    json.Unmarshal([]byte(response), &vehicles)

    assert.IsType(t, Vehicle{}, vehicles[0])
    assert.EqualValues(t, "Car", vehicles[0].Type)
    assert.EqualValues(t, 4, vehicles[0].Wheels)
    assert.EqualValues(t, "Motorcycle", vehicles[1].Type)
    assert.EqualValues(t, 2, vehicles[1].Wheels)
}

https://play.golang.org/p/5SfDH-XZt9J

Share:
18,411
chrisdo
Author by

chrisdo

Updated on June 20, 2022

Comments

  • chrisdo
    chrisdo almost 2 years

    I want to parse some json data in go. The data looks like this:

    {"id":"someId","key_1":"value_1","key_2":"value_2","key_3":"value_3","points":[[1487100466412,"50.032178","8.526018",300,0.0,26,0],[1487100471563,"50.030869","8.525949",300,0.0,38,0],[1487100475722,"50.028514","8.525959",225,0.0,69,-900],[1487100480834,"50.025827","8.525793",275,0.0,92,-262],...]}

    I built a go struct:

    type SomeStruct struct {
       ID   string `json:"id"`
       Key1 string `json:"key_1"`
       Key2 string `json:"key_2"`
       Key3 string `json:"key_3"`
       Points []Point `json:"points"`
    }
    
    type Point struct {
       Timestamp int64 `json:"0"`
       Latitude float64 `json:"1,string"`
       Longitude float64 `json:"2,string"`
       Altitude int `json:"3"` 
       Value1 float64 `json:"4"`
       Value2 int `json:"5"`
       Value3 int `json:"6"`      
    }
    

    I unmarshal the json data

    var track SomeStruct
    error := json.Unmarshal(data,&track)
    if(error != nil){
        fmt.Printf("Error while parsing data: %s", error)
    }
    

    json: cannot unmarshal array into Go value of type Point{someId value_1 value_2 value_3 [{0 0 0 0 0 0 0} {0 0 0 0 0 0 0} {0 0 0 0 0 0 0}...]}

    So the first json keys are parsed correctly, but I cannot figure out how to get the point data, which is an array of arrays.

    The generate struct is also the suggest one from here, except I don't use a nested struct but a separate type. Using the suggested nested struct does not make a difference: JSON-to-Go

    Do I need to implement my own Unmarshaller for this?

    ======= UPDATE SOLUTION ============

    It is enough to implement the UnmarshalJSON interface for the Point struct. The example below does not contain proper error handling but it show the direction.

    Playground example

    package main
    
    import (
        "encoding/json"
        "fmt"
        "strconv"
    )
    
    type SomeStruct struct {
        ID     string  `json:"id"`
        Key1   string  `json:"key_1"`
        Key2   string  `json:"key_2"`
        Key3   string  `json:"key_3"`
        Points []Point `json:"points"`
    }
    
    type Point struct {
        Timestamp int64
        Latitude  float64
        Longitude float64
        Altitude  int
        Value1    float64
        Value2    int16
        Value3    int16
    }
    
    func (tp *Point) UnmarshalJSON(data []byte) error {
        var v []interface{}
        if err := json.Unmarshal(data, &v); err != nil {
            fmt.Printf("Error whilde decoding %v\n", err)
            return err
        }
        tp.Timestamp = int64(v[0].(float64))
        tp.Latitude, _ = strconv.ParseFloat(v[1].(string), 64)
        tp.Longitude, _ = strconv.ParseFloat(v[2].(string), 64)
        tp.Altitude = int(v[3].(float64))
        tp.Value1 = v[4].(float64)
        tp.Value2 = int16(v[5].(float64))
        tp.Value3 = int16(v[6].(float64))
    
        return nil
    }
    
    func main() {
    
        const data =    `{"id":"someId","key_1":"value_1","key_2":"value_2","key_3":"value_3","points":[[1487100466412,"50.032178","8.526018",300,0.0,26,0],[1487100471563,"50.030869","8.525949",300,0.0,38,0],[1487100475722,"50.028514","8.525959",225,0.0,69,-900],[1487100480834,"50.025827","8.525793",275,0.0,92,-262]]}`
    
    var something SomeStruct
    json.Unmarshal([]byte(data), &something)
    
    fmt.Printf("%v", something)
    }
    
  • RayfenWindspear
    RayfenWindspear about 7 years
    Just a note. Only an Unmarshaller for the array itself needs to be written. The array will unmarshal into a [][]interface{} of points. That is the portion that will need to be handled manually.
  • chrisdo
    chrisdo about 7 years
    Thanks a lot, i will play with this. I managed to use type assertion to get the values from the Points, but using a custom unmarshaller for the array sounds cleaner to me. I will update Post as soon as I got it right.
  • Flimzy
    Flimzy about 7 years
    @RayfenWindspear: I think the proper approach is to unmarshal the pointer data to []interface{}. The standard JSON marshaler will already handle the []Point part.
  • Arun
    Arun over 3 years
    Your example is really helpful !!
  • james-see
    james-see about 3 years
    I keep forgetting I can do a slice of structs! Thanks!