mitchellh / mapstructure

Go library for decoding generic map values into native Go structures and vice versa.
https://gist.github.com/mitchellh/90029601268e59a29e64e55bab1c5bdc
MIT License
7.93k stars 677 forks source link

How to unmarshal object to specific struct based on value in that object? #293

Open DonDebonair opened 2 years ago

DonDebonair commented 2 years ago

In the Buy Why?! section in the README is says:

For example, consider this JSON:

{
  "type": "person",
  "name": "Mitchell"
}

Perhaps we can't populate a specific structure without first reading the "type" field from the JSON. We could always do two passes over the decoding of the JSON (reading the "type" first, and the rest later). However, it is much simpler to just decode this into a map[string]interface{} structure, read the "type" key, then use something like this library to decode it into the proper structure.

How would you go about this, if the JSON looks like this?:

{
  "animals": [
    {
      "type": "cat",
      "name": "Ginny",
      "purrFactor": 3
    },
    {
      "type": "dog",
      "name": "Scooby",
      "boopNose": true
    }
  ]
}

In this case, each of the items in the animals array would adhere to the same interface (for example something that implements Name()) but each different type would correspond to a different struct type that implements said interface.

The README suggests that this is what mapstructure is practically made for, but I don't understand how to implement this.

gtors commented 2 years ago

I also don't figure out how to implement deserialization based on the discriminator field. @DonDebonair have you found a solution?

DonDebonair commented 2 years ago

Yeah, I did. I'm using YAML, not JSON. But the principles are the same. This is roughly how I approached it:

  1. Load YAML file
  2. Unmarshal YAML as a struct with a slice of maps in it:
type Animals struct {
    Animals []map[string]any `yaml:"animals"`
}

func myFunc() {
    animalsConfig := &Animals{}
    err = yaml.Unmarshal(b, animals)
}
  1. Loop over the the slice of animals and based on the type, use mapstruct to unmarshal into actual structs
func myFunc() {
...
for _, animal := range animalsConfig.Animals {
        var animalThing Animal // Animal interface is defined somewhere
        if animal["type] == "dog" {
            animalThing = Dog{}
        }
        if animal["type"] == "cat" {
            animalThing = Cat{}
        }
        // Etc., you should probably use a switch/case here? ^
        err = mapstructure.Decode(deployment, animalThing)
        if err != nil {
            return nil, err
        }
        animals = append(animals, animalThing)
    }
}