可以将JSON反序列化为接口的实现者吗?

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

Can I unmarshal JSON into implementers of an Interface?

问题

我有一个声明了一个方法的接口,以及一些实现该接口的结构体。现在我想将一些 JSON 解析为这些结构体的实例。具体如下所示:

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

但是当我运行时,我得到了"panic: json: cannot unmarshal object into Go value of type main.Animal"的错误。我能否得到一个动物园,其中的动物是一只名为 Fido 的狗和一条有 123 鳞片的鱼?

英文:

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?

答案1

得分: 8

根据你提供的当前条件,没有直接的方法来实现你想要的结果。@eduncan911提供了一个非常通用的方法,但是,如果你能稍微调整一下JSON输入,你就可以使用以下方法实现。

核心思想是使用json.RawMessage作为缓冲区,延迟解组直到知道要解组的类型为止。

首先,将JSON输入调整为以下形式:

{
    "Animals": [{
        "Type": "dog",
        "Property": {
            "Name": "Fido"
        }
    },{
        "Type": "fish",
        "Property": {
            "NumScales": 123
        }
    }]
}

从我所看到的,这个调整并没有使JSON变得更糟,而实际上在可读性方面更好了。

然后,创建一个新的结构体,比如AnimalCard

type AnimalCard struct {
    Type     string
    Property json.RawMessage
    Animal   Animal
}

并修改你的Zoo为:

type Zoo struct {
    Animals []*AnimalCard
}

现在将你的JSON解组到zoo,你将得到一个*AnimalCard数组。现在你可以遍历zoo数组,并根据类型进行解组:

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

示例代码在这里

如果在动物园中有越来越多的动物怎么办?

好问题 可以将JSON反序列化为接口的实现者吗? 上述解决方案的问题在于它不够可扩展。如果我们有20只动物,而不仅仅是2只呢?如果有200只?2000只?我们需要一种更通用的方法来解决。

这次的核心思想是使用reflect

首先,我们可以维护一个映射,将类型名称映射到接口实现:

mapper := map[string]Animal{}

然后,我们将动物的指针放入映射中:

mapper["dog"] = &Dog{}
mapper["fish"] = &Fish{}

现在,在我们将JSON解组为AnimalCard并开始迭代之后,我们使用反射来初始化一个新的实例指针并解组到它:

for _, card := range zoo.Animals {
    // 获取动物类型的指针
    animal := mapper[card.Type]
    // 获取指针的类型
    animalType := reflect.TypeOf(animal)
    // 创建相同类型的新实例指针
    newInstancePtr := reflect.New(animalType.Elem()).Interface().(Animal)
    // 解组到指针
    _ = json.Unmarshal(card.Property, newInstancePtr)
    // 将指针赋值回去
    card.Animal = newInstancePtr
}

示例代码在这里

英文:

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.

What if I got more and more Animals in the Zoo?

Good question 可以将JSON反序列化为接口的实现者吗? 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.

答案2

得分: 3

使用json.Unmarshaler接口创建自定义的UnmarshalJSON方法。然后在方法内部,测试类型转换以确定哪种类型适用,进行赋值并返回。

在这篇文章的结尾有一个很好的总结:

http://attilaolah.eu/2013/11/29/json-decoding-in-go/

英文:

Use the json.Unmarshaler interface to create a custom UnmarshalJSON method. 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:

http://attilaolah.eu/2013/11/29/json-decoding-in-go/

huangapple
  • 本文由 发表于 2016年4月10日 11:57:54
  • 转载请务必保留本文链接:https://go.coder-hub.com/36525602.html
匿名

发表评论

匿名网友

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

确定