mapstructure icon indicating copy to clipboard operation
mapstructure copied to clipboard

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

Open DonDebonair opened this issue 3 years ago • 2 comments
trafficstars

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.

DonDebonair avatar Jun 24 '22 15:06 DonDebonair

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

gtors avatar Nov 22 '22 08:11 gtors

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)
	}
}

DonDebonair avatar Nov 23 '22 13:11 DonDebonair