Search code examples
jsongojson-deserialization

golang unmarshal (deserialize) variable type dictionary in struct


This is a contrived example of my issue, so please ignore that this is solved by using a singular struct w/ json optional param.

Given:

    {
        "name": "alice",
        "address_dict": {
                            "home": { "address": "123 st" },
                            "work": { "address": "456 rd", "suite": "123"}
                        }
    }
type AddressIf interface{}

type AddressHome {
    Address string `json:"address"`
}

type AddressWork {
    Address string `json:"address"`
    Suite string `json:"suite"`
}

type Contact struct {
    Name string `json:"name"`
    AddressMap map[string]AddressIf `json:"address_map"`
}
func(self *Contact) UnmarshalJSON(b []byte) error {
    var objMap map[string]*json.RawMessage
    err := json.Unmarshal(b, &objMap
    if err != nil {
        return err
    }

    var rawAddressMap map[string]*json.RawMessage
    err = json.Unmarshal(*objMap["address_map"], &rawAddressMap)
    if err != nil {
        return err
    }

    // how do we unmarshal everything else in the struct, and only override the handling of address map???
    // <failing code block is here - beg - just a tad, just a tad, just a tad - recursive>
    err = json.Unmarshal(b, self)
    if err != nil {
        return err
    }
    // <failing code block is here - end>

    if nil == self.AddressMap {
        self.AddressMap = make(map[string]AddressIf)
    }

    for key, value := range rawAddressMap {
        switch key {
            case "home":
                dst := &AddressHome{}
                err := json.Unmarshal(*value, dst)
                if err != nil {
                    return err
                } else {
                    self.AddressMap[key] = dst
                }
            case "work":
                dst := &AddressWork{}
                err := json.Unmarshal(*value, dst)
                if err != nil {
                    return err
                } else {
                    self.AddressMap[key] = dst
                }
            default:
                continue
        }
    }
}

I have just the name param in this sample bit of json, but let's assume I have more in my code. Is there a way to use the normal/default unmarshal for all the params, and then only manually take over for the address_dict, or is it the case that once I commit to implementing the interface for this object, I'm stuck manually deserializing each parameter?

I tried the following, and quickly realized why it didn't work.

    err = json.Unmarshal(b, self)
    if err != nil {
        return err
    }

Solution

  • An alternative to the other answer would be to declare a named map[string]AddressIf type and have it implement the json.Unmarshaler interface, then you do not have to do any of the temporary / anon type and conversion dance.

    type AddressMap map[string]AddressIf
    
    func (m *AddressMap) UnmarshalJSON(b []byte) error {
        if *m == nil {
            *m = make(AddressMap)
        }
    
        raw := make(map[string]json.RawMessage)
        if err := json.Unmarshal(b, &raw); err != nil {
            return err
        }
    
        for key, val := range raw {
            switch key {
            case "home":
                dst := new(AddressHome)
                if err := json.Unmarshal(val, dst); err != nil {
                    return err
                }
                (*m)[key] = dst
            case "work":
                dst := new(AddressWork)
                if err := json.Unmarshal(val, dst); err != nil {
                    return err
                }
                (*m)[key] = dst
            }
        }
        return nil
    }
    

    https://play.golang.org/p/o7zV8zu9g5X