为什么在存在Unmarshal实现时,嵌入结构体不会被解码?

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

Why embedded structs are not decoded when there is an Unmarshal implementation?

问题

我有一个类型为ServiceAccount的结构体,它嵌套了另外两个类型(User和Policy)。然而,由于Policy类型的Unmarshaler实现,似乎完全忽略了User类型的字段。
这种行为有什么合理的原因吗?对我来说,这看起来像是一个bug,因为json包可以通过反射看到我们嵌套了两个类型,而不仅仅是Policy类型。

我知道我可以通过在ServiceAccount类型上实现Unmarshaler接口来“修复”这个问题。

package main

import (
	"encoding/json"
	"fmt"
)

type ServiceAccount struct {
	User
	Policy
}

type User struct {
	UserID string `json:"userID"`
}

type Policy struct {
	Scopes string `json:"scopes,omitempty"`
}

// PolicyRaw is the Policy type as received from client.
type PolicyRaw struct {
	Scopes string `json:"scopes,omitempty"`
}

func main() {
	s := `{"userID":"xyz", "scopes":"some scopes"}`

	srvAcc := &ServiceAccount{}
	if err := json.Unmarshal([]byte(s), srvAcc); err != nil {
		panic(err)
	}
	fmt.Printf("srvAcc %v", *srvAcc)
}

func (p *Policy) UnmarshalJSON(b []byte) error {
	pr := new(PolicyRaw)
	if err := json.Unmarshal(b, pr); err != nil {
		return err
	}
	p.Scopes = pr.Scopes
	return nil
}

执行代码

英文:

I have a type ServiceAccount which embeds two other types (User and Policy). However it seems the fields of the User type are totally ignored due the Unmarshaler implementation of the Policy type.
Is there any good reason for this behaviour? It looks like a bug to me because the json package can see through reflection that we have two types embedded and not only the type Policy.

I'm aware that I can "fix" the issue by implementing the Unmarshaler interface on type ServiceAccount too.

package main

import (
	"encoding/json"
	"fmt"

) 
type ServiceAccount struct {
	User
	Policy
}
type User struct {
	UserID string `json:"userID"`
}

type Policy struct {
	Scopes string `json:"scopes,omitempty"`
}

// PolicyRaw is the Policy type as received from client.
type PolicyRaw struct {
	Scopes string `json:"scopes,omitempty"`
}

func main() {
	s := `{"userID":"xyz", "scopes":"some scopes"}`

	srvAcc := &ServiceAccount{}
	if err := json.Unmarshal([]byte(s), srvAcc); err != nil {
		panic(err)
	}
	fmt.Printf("srvAcc %v", *srvAcc)
}

func (p *Policy) UnmarshalJSON(b []byte) error {
	pr := new(PolicyRaw)
	if err := json.Unmarshal(b, pr); err != nil {
		return err
	}
	p.Scopes = pr.Scopes
	return nil
}

Execute

答案1

得分: 3

我不认为这是一个 bug,而只是接口和嵌入的工作方式。这只是恰好不符合你想要/期望的情况。

json.Unmarshal 通过这一行来确定使用 UnmarshalJSON 方法:

    if u, ok := v.Interface().(Unmarshaler); ok

正如你所知,如果某个东西具有正确的方法集,它就实现了一个接口,而 *Policy*ServiceAccount 就是这样。所以预期的是,外部类型的 JSON 解码将只调用适当的方法并认为完成了。

有趣的是,如果你尝试添加一个虚拟方法,例如:

func (u *User) UnmarshalJSON([]byte) error {return errors.New("not impl")}

那么尽管 *User*Policy 现在都实现了该接口,*ServiceAccount 将不再实现该接口。原因很明显,如果你尝试显式调用 srvAcc.UnmarshalJSON,编译器将报错“ambiguous selector srvAcc.UnmarshalJSON”。如果没有这样的调用,代码是合法的,并且该方法只是从方法集中排除。

所以我认为解决方案有以下几种:

  • 如果你想要编组结果,请不要嵌入实现了 json.Unmarshaller 的内容,而是使用一个命名字段。
  • 当进行这种嵌入时,确保你自己明确实现了外部类型的 json.Unmarshaller(例如,在 *ServiceAccount 中添加一个 UnmarshalJSON 方法)。
  • 确保至少有两个嵌入的内容实现了 json.Unmarshaller,然后它们将分别工作,但“外部”类型将获得默认行为。

最后一种选项对我来说似乎是一种 hack。(顺便说一下,可以通过以下方式故意实现这种效果:

    type ServiceAccount struct {
    	User
    	Policy
        dummyMarshaller
    }
    
    type dummyMarshaller struct{}
    func (dummyMarshaller) MarshalJSON([]byte) error {panic("ouch")}

但我觉得这看起来非常 hacky)。

另请参阅:

¹ 进一步测试显示,对于这种匿名的(即嵌入字段),它们的 UnmarshalJSON 方法不会被调用。

英文:

I don't think it's a bug but just just how interfaces and embedding works. It just happens not to be what you want/expect here.

json.Unmarshal figures out to use the UnmarshalJSON method via this line:

    if u, ok := v.Interface().(Unmarshaler); ok

As you know, something implements an interface if it has the right method set which *Policy and *ServiceAccount do. So it's expected that JSON decoding of the the outer type would just call the appropriate method and think it's done.

Interestingly, if you were to experiment and add a dummy method such as:

func (u *User) UnmarshalJSON([]byte) error {return errors.New("not impl")}

then although *User and *Policy would now both implement the interface,
*ServiceAccount will no longer implement that interface. The reason is clear if you try and explicitly call srvAcc.UnmarshalJSON which would then give a compiler error of "ambiguous selector srvAcc.UnmarshalJSON". Without such a call the code is legal and the method is just excluded from the method set.

So I think the solution is one of:

  • Just don't embed things that implement json.Unmarshaller if you want to marshal the result, e.g. use a named field instead.
  • Make sure you explicitly implement json.Unmarshaller for the outer type yourself when doing such embedding (e.g. add an UnmarshalJSON method to *ServiceAccount).
  • Make sure at least two embedded things implement json.Unmarshaller and then <strike>they'll work individually</strike>¹ but the "outer" type will get the default behaviour.

The last option seems like a hack to me. (And btw could be done on purpose with something like:

    type ServiceAccount struct {
    	User
    	Policy
        dummyMarshaller
    }
    
    type dummyMarshaller struct{}
    func (dummyMarshaller) MarshalJSON([]byte) error {panic(&quot;ouch&quot;)}

but that looks really hacky to me).

See also:

¹ Further testing shows that decoding such anonymous (i.e. embedded fields), that their UnmarshalJSON methods do not get called.

huangapple
  • 本文由 发表于 2015年3月11日 08:20:24
  • 转载请务必保留本文链接:https://go.coder-hub.com/28976458.html
匿名

发表评论

匿名网友

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

确定