Search code examples
jsongostructunmarshalling

Unmarshall JSON with multiple value types and arbitrary number of keys


I'm attempting to read a JSON with the following form

{
  string: int,
  string: string,
  string: MyStruct,
  string: MyStruct,
  ...
  string: MyStruct,
}

For example

{
  "status": 200,
  "message": "some cool text",
  "coolKeyA": {
    "name": "yoda",
    "age": 900
   },
  "CoolKeyB": {
    "name": "Mahalalel",
    "age": 895
   },
   "CoolKeyC": {
    "name": "Prince",
    "age": 57
   },
}

The desired outcome is to get a map of map[string]MyStruct. There are an elastic or arbitrary number of "CoolKeyX" keys but the other keys are static, e.g., status and message.

Since the values in the JSON are different types I tried to reach them in to a blank map[string]interface{}. Then the goal is to loop through the keys and pluck out they keys of interest and convert the keys of map[string]inferface{string: string, string: int} to MyStruct.

scaryAcceptAll := map[string]interface{}{}
  if err = json.Unmarshal(byteArray, &scaryAcceptAll); err != nil { 
    log.Printf("error: %v", err)
    return err 
  } 
  
  for k,v := range scaryAcceptAll { 
    if (k == "val0" ) || (k == "val1") {
        continue
    }   
    desiredMap[k] = models.MyStruct{Name: v["name"], Age: v["age"]}
  }

Which gives me the following error: NonIndexableOperand: invalid operation: cannot index v (variable of type interface{})

I know the basic idea of unmarshalling JSONs is to create a struct that looks like the json and use that but since I don't know the exact number of keys or what the "CoolKey" key really is (because it's a string containing a hash "000ab8f26d") I didn't know how. I know interfaces are sort of a catch all but then I'm not sure how to pull my desired data out of it.


Solution

  • One approach would be implement a custom json.Unmarshaler:

    type Obj struct {
        Status   int
        Message  string
        CoolKeys map[string]Person
    }
    
    type Person struct {
        Name string
        Age  int
    }
    
    func (o *Obj) UnmarshalJSON(data []byte) error {
        // first, unmarshal the object into a map of raw json
        var m map[string]json.RawMessage
        if err := json.Unmarshal(data, &m); err != nil {
            return err
        }
    
        // next, unmarshal the status and message fields, and any
        // other fields that don't belong to the "CoolKey" group
        if err := json.Unmarshal(m["status"], &o.Status); err != nil {
            return err
        }
        delete(m, "status") // make sure to delete it from the map
        if err := json.Unmarshal(m["message"], &o.Message); err != nil {
            return err
        }
        delete(m, "message") // make sure to delete it from the map
    
        // finally, unmarshal the rest of the map's content
        o.CoolKeys = make(map[string]Person)
        for k, v := range m {
            var p Person
            if err := json.Unmarshal(v, &p); err != nil {
                return err
            }
            o.CoolKeys[k] = p
        }
        return nil
    }
    

    https://go.dev/play/p/s4YCmve-pnz