使用自定义 MarshalJSON() 方法嵌入结构的惯用方式
Posted
技术标签:
【中文标题】使用自定义 MarshalJSON() 方法嵌入结构的惯用方式【英文标题】:Idiomatic way to embed struct with custom MarshalJSON() method 【发布时间】:2016-11-24 04:37:45 【问题描述】:给定以下结构:
type Person
Name string `json:"name"`
type Employee
Person
JobRole string `json:"jobRole"`
我可以按预期轻松地将 Employee 编组为 JSON:
p := Person"Bob"
e := Employee&p, "Sales"
output, _ := json.Marshal(e)
fmt.Printf("%s\n", string(output))
输出:
"name":"Bob","jobRole":"Sales"
但是当嵌入的结构有一个自定义的MarshalJSON()
方法时...
func (p *Person) MarshalJSON() ([]byte,error)
return json.Marshal(struct
Name string `json:"name"`
Name: strings.ToUpper(p.Name),
)
它完全崩溃了:
p := Person"Bob"
e := Employee&p, "Sales"
output, _ := json.Marshal(e)
fmt.Printf("%s\n", string(output))
现在结果:
"name":"BOB"
(注意明显缺少jobRole
字段)
这很容易预料到...嵌入的Person
结构实现了正在调用的MarshalJSON()
函数。
问题是,这不是我想要的。我想要的是:
"name":"BOB","jobRole":"Sales"
也就是说,对Employee
的字段进行正常编码,并按照Person
的MarshalJSON()
方法对其字段进行编组,并交回一些整洁的JSON。
现在我也可以将MarshalJSON()
方法添加到Employee
,但这要求我知道嵌入式类型也实现MarshalJSON()
,并且要么(a)复制其逻辑,要么(b)调用Person
的 MarshalJSON()
并以某种方式操纵其输出以适合我想要的位置。任何一种方法都显得草率,而且不是很有未来的证明(如果有一天我无法控制的嵌入式类型添加了自定义 MarshalJSON()
方法怎么办?)
这里有没有我没有考虑过的替代方案?
【问题讨论】:
如果 Person 的MarshalJSON
返回一个数组怎么办?没有办法将它合并到一个对象中。作曲很难。
@AlexGuerra:相当。为了一致性起见,这足以让我希望 MarshalJSON 总是跳过嵌入式类型。呵呵。我想在我的应用程序中可能需要一种完全不同的方法。
【参考方案1】:
我在父结构上使用了这种方法来防止嵌入的结构覆盖封送处理:
func (e Employee) MarshalJSON() ([]byte, error)
v := reflect.ValueOf(e)
result := make(map[string]interface)
for i := 0; i < v.NumField(); i++
fieldName := v.Type().Field(i).Name
result[fieldName] = v.Field(i).Interface()
return json.Marshal(result)
它很方便,但在输出中嵌套了嵌入式结构::
"JobRole":"Sales","Person":"name":"Bob"
对于像问题中这样的小结构,@Flimzy 的回答很好,但可以做得更简洁:
func (e Employee) MarshalJSON() ([]byte, error)
return json.Marshal(map[string]interface
"jobRole": e.JobRole,
"name": e.Name,
)
【讨论】:
【参考方案2】:近 4 年后,我提出了一个与 @jcbwlkr 基本相似的答案,但不需要中间解组/重新编组步骤,通过使用一点字节切片操作来连接两个JSON 段。
func (e *Employee) MarshalJSON() ([]byte, error)
pJSON, err := e.Person.MarshalJSON()
if err != nil
return nil, err
eJSON, err := json.Marshal(map[string]interface
"jobRole": e.JobRole,
)
if err != nil
return nil, err
eJSON[0] = ','
return append(pJSON[:len(pJSON)-1], eJSON...), nil
此方法的更多详细信息和讨论here。
【讨论】:
这个很有用,谢谢。如果你也有一个自定义的 UnmarshalJSON 呢? :) 这实际上是迄今为止我发现的最好的答案,它确保如果嵌入式结构具有自定义 MarshalJSON 实现,它仍然可以访问。【参考方案3】:虽然这会产生与 OP 想要的不同的输出,但我认为它仍然是一种有用的技术,可以防止 MarshalJSON
的嵌入式结构破坏包含它们的结构的编组。
有a proposal for encoding/json
to recognize an inline
option in struct tags。如果实现了,那么我认为避免在 OP 示例中的情况下嵌入结构可能是最好的选择。
目前,a comment on the Go issue tracker 中描述了一种合理的解决方法,并且是此答案的基础。它包括定义一个新类型,该类型将具有与嵌入的原始结构相同的内存布局,但没有任何方法:
https://play.golang.org/p/BCwcyIqv0F7
package main
import (
"encoding/json"
"fmt"
"strings"
)
type Person struct
Name string `json:"name"`
func (p *Person) MarshalJSON() ([]byte, error)
return json.Marshal(struct
Name string `json:"name"`
Name: strings.ToUpper(p.Name),
)
// person has all the fields of Person, but none of the methods.
// It exists to be embedded in other structs.
type person Person
type EmployeeBroken struct
*Person
JobRole string `json:"jobRole"`
type EmployeeGood struct
*person
JobRole string `json:"jobRole"`
func main()
p := Person"Bob"
e := EmployeeBroken&p, "Sales"
output, _ := json.Marshal(e)
fmt.Printf("%s\n", string(output))
p := Person"Bob"
e := EmployeeGood(*person)(&p), "Sales"
output, _ := json.Marshal(e)
fmt.Printf("%s\n", string(output))
输出:
"name":"BOB"
"name":"Bob","jobRole":"Sales"
OP 想要"name":"BOB","jobRole":"Sales"
。为此,需要将Person.MarshalJSON
返回的对象“内联”到Employee.MashalJSON
生成的对象中,不包括Person
中定义的字段。
【讨论】:
这太棒了。【参考方案4】:一种更通用的方式来支持内部和外部字段中的大量字段。
副作用是你需要为每个外部结构都写这个。
示例:https://play.golang.org/p/iexkUYFJV9K
package main
import (
"encoding/json"
"fmt"
"log"
"strings"
)
func Struct2Json2Map(obj interface) (map[string]interface, error)
data, err := json.Marshal(obj)
if err != nil
return nil, err
var kvs map[string]interface
err = json.Unmarshal(data, &kvs)
if err != nil
return nil, err
return kvs, nil
type Person struct
Name string `json:"-"`
func (p Person) MarshalJSONHelper() (map[string]interface, error)
return Struct2Json2Map(struct
Name string `json:"name"`
Name: strings.ToUpper(p.Name),
)
type Employee struct
Person
JobRole string `json:"jobRole"`
func (e Employee) MarshalJSON() ([]byte, error)
personKvs, err := e.Person.MarshalJSONHelper()
if err != nil
return nil, err
type AliasEmployee Employee
kvs, err := Struct2Json2Map(struct
AliasEmployee
AliasEmployee(e),
)
for k,v := range personKvs
kvs[k] = v
return json.Marshal(kvs)
func main()
bob := Employee
Person: Person
Name: "Bob",
,
JobRole: "Sales",
output, err := json.Marshal(bob)
if err != nil
log.Fatal(err)
fmt.Println(string(output))
【讨论】:
【参考方案5】:不要将MarshalJSON
放在Person
上,因为它被提升为外部类型。而是创建一个type Name string
并让Name
实现MarshalJSON
。然后把Person
改成
type Person struct
Name Name `json:"name"`
示例:https://play.golang.org/p/u96T4C6PaY
更新
为了更通用地解决这个问题,您将不得不在外部类型上实现MarshalJSON
。内部类型的方法被提升为外部类型,所以你不会绕过它。您可以让外部类型调用内部类型的MarshalJSON
,然后将其解组为map[string]interface
等通用结构并添加您自己的字段。这个例子是这样做的,但它有一个副作用是改变最终输出字段的顺序
https://play.golang.org/p/ut3e21oRdj
【讨论】:
这适用于我的具体示例,并且很有帮助(所以+1),但我认为它错过了我试图问的要点。如果 Person 的 MarshalJSON 添加了新的字段,或者其他不透明的数据怎么办? @Flimzy 好点。我用另一个例子更新了我的答案以上是关于使用自定义 MarshalJSON() 方法嵌入结构的惯用方式的主要内容,如果未能解决你的问题,请参考以下文章
json.Marshal(): json: 为 msgraph.Application 类型调用 MarshalJSON 时出错