Search code examples
jsondictionarygounmarshalling

Custom unmarshaling a struct into a map of slices


I thought I understood unmarshalling by now, but I guess not. I'm having a little bit of trouble unmarshalling a map in go. Here is the code that I have so far

type OHLC_RESS struct {
    Pair map[string][]Candles
    Last int64 `json:"last"`
}

type Candles struct {
    Time   uint64
    Open   string
    High   string
    Low    string
    Close  string
    VWAP   string
    Volume string
    Count  int
}

func (c *Candles) UnmarshalJSON(d []byte) error {
    tmp := []interface{}{&c.Time, &c.Open, &c.High, &c.Low, &c.Close, &c.VWAP, &c.Volume, &c.Count}
    length := len(tmp)
    err := json.Unmarshal(d, &tmp)
    if err != nil {
        return err
    }
    g := len(tmp)
    if g != length {
        return fmt.Errorf("Lengths don't match: %d != %d", g, length)
    }
    return nil
}

func main() {
    response := []byte(`{"XXBTZUSD":[[1616662740,"52591.9","52599.9","52591.8","52599.9","52599.1","0.11091626",5],[1616662740,"52591.9","52599.9","52591.8","52599.9","52599.1","0.11091626",5]],"last":15}`)
    var resp OHLC_RESS
    err := json.Unmarshal(response, &resp)
    fmt.Println("resp: ", resp)
}

after running the code, the last field will unmarshal fine, but for whatever reason, the map is left without any value. Any help?


Solution

  • The expedient solution, for the specific example JSON, would be to NOT use a map at all but instead change the structure of OHLC_RESS so that it matches the structure of the JSON, i.e.

    type OHLC_RESS struct {
        Pair []Candles `json:"XXBTZUSD"`
        Last int64     `json:"last"`
    }
    

    https://go.dev/play/p/Z9PhJt3wX33


    However it's safe to assume, I think, that the reason you've opted to use a map is because the JSON object's key(s) that hold the "pairs" can vary and so hardcoding them into the field's tag is out of the question.

    To understand why your code doesn't produce the desired result, you have to realize two things. First, the order of a struct's fields has no bearing on how the keys of a JSON object will be decoded. Second, the name Pair holds no special meaning for the unmarshaler. Therefore, by default, the unmarshaler has no way of knowing that your wish is to decode the "XXBTZUSD": [ ... ] element into the Pair map.

    So, to get your desired result, you can have the OHLC_RESS implement the json.Unmarshaler interface and do the following:

    func (r *OHLC_RESS) UnmarshalJSON(d []byte) error {
        // first, decode just the object's keys and leave
        // the values as raw, non-decoded JSON
        var obj map[string]json.RawMessage
        if err := json.Unmarshal(d, &obj); err != nil {
            return err
        }
    
        // next, look up the "last" element's raw, non-decoded value
        // and, if it is present, then decode it into the Last field
        if last, ok := obj["last"]; ok {
            if err := json.Unmarshal(last, &r.Last); err != nil {
                return err
            }
    
            // remove the element so it's not in
            // the way when decoding the rest below
            delete(obj, "last")
        }
    
        // finally, decode the rest of the element values
        // in the object and store them in the Pair field
        r.Pair = make(map[string][]Candles, len(obj))
        for key, val := range obj {
            cc := []Candles{}
            if err := json.Unmarshal(val, &cc); err != nil {
                return err
            }
            r.Pair[key] = cc
        }
        return nil
    }
    

    https://go.dev/play/p/Lj8a8Gx9fWH