简体   繁体   中英

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
    }

To avoid copying Contact fields, use object embedding to shadow the fields that require special handling.

Use the factory pattern to eliminate code duplication across the address types.

See commentary for more info:

var addressFactories = map[string]func() AddressIf{
    "home": func() AddressIf { return &AddressHome{} },
    "work": func() AddressIf { return &AddressWork{} },
}

func (self *Contact) UnmarshalJSON(b []byte) error {

    // Declare type with same base type as Contact. This
    // avoids recursion below because this type does not
    // have Contact's UnmarshalJSON method.
    type contactNoMethods Contact

    // Unmarshal to this value. The Contact.AddressMap
    // field is shadowed so we can handle unmarshal of
    // each value in the map.
    var data = struct {
        *contactNoMethods
        AddressMap map[string]json.RawMessage `json:"address_map"`
    }{
        // Note that all fields except AddressMap are unmarshaled
        // directly to Contact.
        (*contactNoMethods)(self),
        nil,
    }

    if err := json.Unmarshal(b, &data); err != nil {
        return err
    }

    // Unmarshal each addresss...

    self.AddressMap = make(map[string]AddressIf, len(data.AddressMap))
    for k, raw := range data.AddressMap {
        factory := addressFactories[k]
        if factory == nil {
            return fmt.Errorf("unknown key %s", k)
        }
        address := factory()
        if err := json.Unmarshal(raw, address); err != nil {
            return err
        }
        self.AddressMap[k] = address
    }
    return nil
}
 

Run it on the playground .

In a comment on the question, OP says that no additional types should be used. This answer uses two additional types ( contactNoMethods and the anonymous type for data ).

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM