英文:
What is the equivalence of Type Name Handling with encoding/json in Go?
问题
简要描述
我有一个情况,我想将JSON数据解组成一个结构体数组(可以是Foo
或Bar
等等),它们都实现了一个公共接口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
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论