Search code examples
gopolymorphismmarshalling

What is the equivalence of Type Name Handling with encoding/json in Go?


Short Description in Prose

I have a situation where I want to unmarshal JSON data into an array of structs (either Foo or Bar and many more) that all implement a common interface MyInterface. Also all of the eligble struct types that implement the interface have a common field which I named Discrimininator in the example below. The Discriminator¹ allows to bi-uniquely find the correct struct type for each value of Discriminator.

The Problem and Error Message

But during unmarshling the code does not "know" which is the correct "target" type. The unmarshaling fails.

cannot unmarshal object into Go value of type main.MyInterface

MWE in

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

package main

import (
    "encoding/json"
    "fmt"
)

type MyInterface interface {
    // some other business logic methods go here!
    GetDiscriminator() string // GetDiscriminator returns something like a key that is unique per struct type implementing the interface
}

type BaseStruct struct {
    Discriminator string // will always be "Foo" for all Foos, will always be "Bar" for all Bars
}

type Foo struct {
    BaseStruct
    // actual fields of the struct don't matter. it's just important that they're different from Bar
    FooField string
}

func (foo *Foo) GetDiscriminator() string {
    return foo.Discriminator
}

type Bar struct {
    BaseStruct
    // actual fields of the struct don't matter. it's just important that they're different from Foo
    BarField int
}

func (bar *Bar) GetDiscriminator() string {
    return bar.Discriminator
}

// Foo and Bar both implement the interface.
// Foo and Bars are always distinguishible if we check the value of Discriminator

func main() {
    list := []MyInterface{
        &Bar{
            BaseStruct: BaseStruct{Discriminator: "Bar"},
            BarField:   42,
        },
        &Foo{
            BaseStruct: BaseStruct{Discriminator: "Foo"},
            FooField:   "hello",
        },
    }
    jsonBytes, _ := json.Marshal(list)
    jsonString := string(jsonBytes)
    fmt.Println(jsonString)
    // [{"Discriminator":"Bar","BarField":42},{"Discriminator":"Foo","FooField":"hello"}]
    var unmarshaledList []MyInterface
    err := json.Unmarshal(jsonBytes, &unmarshaledList)
    if err != nil {
        // Unmarshaling failed: json: cannot unmarshal object into Go value of type main.MyInterface
        fmt.Printf("Unmarshaling failed: %v", err)
    }
}

In other languages

TypeNameHandling as known from .NET

In Newtonsoft, a popular .NET JSON Framework, this is solved by a something called "TypeNameHandling" or can be solved with a custom JsonConverter . The framework will add something like a magic "$type" key on root level to the serialized/marshaled JSON which is then used to determine the original type on deserialization/unmarshaling.

Polymorphism in ORMs

¹A similar situation occurs under the term "polymorphism" in ORMs when instances of multiple types having the same base are saved in the same table. One typically introduces a discriminator column, hence the name in above example.


Solution

  • You can implement a custom json.Unmarshaler. For that you'll need to use a named slice type instead of the unnamed []MyInterface.

    Within the custom unmarshaler implementation you can unmarshal the JSON array into a slice where each element of the slice is a json.RawMessage representing the corresponding JSON object. After that you can iterate over the slice of raw messages. In the loop unmarshal from each raw message only the Discriminator field, then use the Discriminator field's value to determine what the correct type is into which the full raw message can be unmarshaled, finally unmarshal the full message and add the result to the receiver.

    type MyInterfaceSlice []MyInterface
    
    func (s *MyInterfaceSlice) UnmarshalJSON(data []byte) error {
        array := []json.RawMessage{}
        if err := json.Unmarshal(data, &array); err != nil {
            return err
        }
    
        *s = make(MyInterfaceSlice, len(array))
        for i := range array {
            base := BaseStruct{}
            data := []byte(array[i])
            if err := json.Unmarshal(data, &base); err != nil {
                return err
            }
    
            var elem MyInterface
            switch base.Discriminator {
            case "Foo":
                elem = new(Foo)
            case "Bar":
                elem = new(Bar)
            }
            if elem == nil {
                panic("whoops")
            }
    
            if err := json.Unmarshal(data, elem); err != nil {
                return err
            }
            (*s)[i] = elem
        }
        return nil
    }
    

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