使用自定义 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的字段进行正常编码,并按照PersonMarshalJSON()方法对其字段进行编组,并交回一些整洁的JSON。

现在我也可以将MarshalJSON() 方法添加到Employee,但这要求我知道嵌入式类型也实现MarshalJSON(),并且要么(a)复制其逻辑,要么(b)调用PersonMarshalJSON() 并以某种方式操纵其输出以适合我想要的位置。任何一种方法都显得草率,而且不是很有未来的证明(如果有一天我无法控制的嵌入式类型添加了自定义 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 中自定义编组映射键

嵌入式:ARM数据定义伪操作全总结

网页嵌入自定义字体方法

将自定义键盘嵌入uiview

json.Marshal(): json: 为 msgraph.Application 类型调用 MarshalJSON 时出错

如何使用 Entity Framework Core 5.0 将复杂 SQL 查询的结果行映射到自定义 DTO?