英文:
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
}
}
示例代码在这里。
如果在动物园中有越来越多的动物怎么办?
好问题 上述解决方案的问题在于它不够可扩展。如果我们有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 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:
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论