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

huangapple go评论74阅读模式
英文:

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

问题

简要描述

我有一个情况,我想将JSON数据解组成一个结构体数组(可以是FooBar等等),它们都实现了一个公共接口MyInterface。此外,所有实现该接口的结构体类型都有一个我在下面示例中命名为Discriminator的公共字段。
Discriminator允许通过唯一地找到每个Discriminator值的正确结构体类型。

问题和错误消息

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

无法将对象解组为类型为main.MyInterface的Go值

最小工作示例

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

package main

import (
	"encoding/json"
	"fmt"
)

type MyInterface interface {
	// 这里还有一些其他的业务逻辑方法!
	GetDiscriminator() string // GetDiscriminator返回类似于每个实现接口的结构体类型的唯一键
}

type BaseStruct struct {
	Discriminator string // 对于所有的Foo,它将始终是“Foo”,对于所有的Bar,它将始终是“Bar”
}

type Foo struct {
	BaseStruct
	// 结构体的实际字段并不重要。重要的是它们与Bar不同
	FooField string
}

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

type Bar struct {
	BaseStruct
	// 结构体的实际字段并不重要。重要的是它们与Foo不同
	BarField int
}

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

// Foo和Bar都实现了该接口。
// 如果我们检查Discriminator的值,Foo和Bar始终是可区分的

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 {
		// 解组失败:json: 无法将对象解组为类型为main.MyInterface的Go值
		fmt.Printf("解组失败:%v", err)
	}
}

其他语言中的解决方案

类似于.NET中的TypeNameHandling

在.NET中的Newtonsoft这个流行的JSON框架中,可以通过一种称为“TypeNameHandling”的方法或使用自定义的JsonConverter来解决这个问题。该框架会在序列化/编组的JSON中添加一个类似于魔术"$type"键,然后在反序列化/解组时使用它来确定原始类型。

ORM中的多态性

在ORM中,类似的情况被称为“多态性”,当多个类型的实例具有相同的基类并保存在同一张表中时,就会出现这种情况。通常会引入一个鉴别器列,因此在上面的示例中使用了这个名称。

英文:

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.

答案1

得分: 6

你可以实现一个自定义的json.Unmarshaler。为此,你需要使用一个命名的切片类型,而不是无名的[]MyInterface

在自定义的解组实现中,你可以将JSON数组解组成一个切片,其中切片的每个元素都是表示相应JSON对象的json.RawMessage。然后,你可以遍历原始消息的切片。在循环中,只从每个原始消息解组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

英文:

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

huangapple
  • 本文由 发表于 2021年10月13日 22:37:07
  • 转载请务必保留本文链接:https://go.coder-hub.com/69557474.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定