I have an interface that declares a method and some structs that implement that interface. Now I want to unmarshal some JSON into instances of these structs. To wit:
package main
import (
"encoding/json"
"fmt"
)
type Animal interface {
makeNoise() string
}
type Dog struct {
Name string
}
func (d Dog) makeNoise() string {
return "woof"
}
type Fish struct {
NumScales int
}
func (f Fish) makeNoise() string {
return "glub glub glub"
}
type Zoo struct {
Animals []Animal
}
func main() {
animals := `{"Animals": [{"Name": "Fido"}, {"NumScales": 123}]}`
animalBytes := []byte(animals)
var zoo Zoo
er := json.Unmarshal(animalBytes, &zoo)
if er != nil {
panic(er)
} else {
fmt.Println(zoo)
}
}
But when I run that, I get "panic: json: cannot unmarshal object into Go value of type main.Animal". Can I instead get a Zoo whose Animals are a Dog named Fido and a Fish with 123 scales?
There is no straight forward way to achieve what you want based on the current condition you gave us. @eduncan911 provided a very general method, but however, if you are able to tweak the JSON
input a bit, you are able to achieve it using the following method.
The core idea is to use json.RawMessage
as a buffer to delay the unmarshal until it knows the type it's gonna unmarshal to.
Firstly, tweak the JSON
input to something like below:
{
"Animals": [{
"Type": "dog",
"Property": {
"Name": "Fido"
}
},{
"Type": "fish",
"Property": {
"NumScales": 123
}
}]
}
From what I can see, this tweak does not make the JSON worse, but actually make it better in terms of readability.
Then, create a new struct, say AnimalCard
:
type AnimalCard struct {
Type string
Property json.RawMessage
Animal Animal
}
And modify your Zoo
to
type Zoo struct {
Animals []*AnimalCard
}
Now unmarshal your json to zoo, you will get an array of *AnimalCard
. Now you could iterate through zoo array and unmarshal it according to type:
for _, card := range zoo.Animals {
if card.Type == "dog" {
dog := Dog{}
_ = json.Unmarshal(card.Property, &dog)
card.Animal = dog
} else if card.Type == "fish" {
fish := Fish{}
_ = json.Unmarshal(card.Property, &fish)
card.Animal = fish
}
}
Playground Exmaple is here.
Good question :) The problem the above solution gave won't be that scalable. What if we have 20 animals, not only 2? What if 200? 2000? We need a more general way to do it.
The core idea this time is to use reflect
.
First, we could maintain a map, which maps a type name to an interface implementation:
mapper map[string]Animal{}
Then we put in our animals pointers:
mapper["dog"] = &Dog{}
mapper["fish"] = &Fish{}
Now, after we unmarshalled the JSON into AnimalCard
and start iterating, we use reflection to initialize a new instance pointer and unmarshal into it:
for _, card := range zoo.Animals {
// get the animal type pointer
animal := mapper[card.Type]
// get the pointer's type
animalType := reflect.TypeOf(animal)
// create a new instance pointer of the same type
newInstancePtr := reflect.New(animalType.Elem()).Interface().(Animal)
// unmarshal to the pointer
_ = json.Unmarshal(card.Property, newInstancePtr)
// assign the pointer back
card.Animal = newInstancePtr
}
Playground Example is here.
Use the json.Unmarshaler
interface to create a custom UnmarshalJSON
methods. Then within the method, test the type casting to see which type works, assign it, and return it.
Good summary at the end of this post: