Go 中类型名称处理与 encoding/json 的等效性是什么?

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

散文的简短描述

我有一种情况,我想将 JSON 数据解组为一个结构数组(FooBar 等等),这些结构都实现了一个通用接口 MyInterface。此外,所有实现该接口的合格结构类型都有一个公共字段,我在下面的示例中将其命名为 Discrimininator。 Discriminator¹ 允许为 Discriminator 的每个值双唯一地找到正确的结构类型。

问题和错误信息

但是在解组期间,代码并不“知道”哪个是正确的“目标”类型。解组失败。

cannot unmarshal object into Go value of type main.MyInterface

MWE 在

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

其他语言

.NET 中已知的 TypeNameHandling

在流行的 .NET JSON 框架 Newtonsoft 中,这可以通过名为“TypeNameHandling”的东西解决,也可以通过自定义 JsonConverter 解决。该框架将在 serialized/marshaled JSON 的根级别添加类似魔术 "$type" 键的内容,然后用于确定 deserialization/unmarshaling.

上的原始类型

ORM 中的多态性

¹当具有相同基础的多个类型的实例保存在同一个 table 中时,在 ORM 中的术语“多态性”下会出现类似的情况。通常会引入一个鉴别器列,因此在上面的示例中使用了这个名称。

您可以实施自定义 json.Unmarshaler。为此,您需要使用命名切片类型而不是未命名的 []MyInterface.

在自定义解组器实现中,您可以将 JSON 数组解组为一个切片,其中切片的每个元素都是一个 json.RawMessage,表示对应的 JSON 对象。之后,您可以迭代原始消息片段。在循环中,仅从每个原始消息中解组 Discriminator 字段,然后使用 Discriminator 字段的值来确定可以解组完整原始消息的正确类型,最后解组完整消息和将结果添加到接收器。

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