用自定义MarshalJSON()方法嵌入struct的惯用法

时间:2016-07-20 19:59:58

标签: json go embedding

鉴于以下结构:

type Person {
    Name string `json:"name"`
}

type Employee {
    Person
    JobRole string `json:"jobRole"`
}

我可以像预期的那样轻松地将员工编组为JSON:

p := Person{"Bob"}
e := Employee{&p, "Sales"}
output, _ := json.Marshal(e)
fmt.Printf("%s\n", string(output))

输出:

  

{"名称":"鲍勃"" jobRole":"销售"}

但是当嵌入式结构具有自定义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))

现在导致:

  

{"名称":" BOB"}

(请注意明显缺少jobRole字段)

这很容易预料......嵌入式Person结构实现了被调用的MarshalJSON()函数。

麻烦的是,它不是我想要的。我想要的是:

  

{"名称":" BOB"" jobRole":"销售"}

即,正常编码Employee的字段,并按照Person的{​​{1}}方法编组其字段,并交回一些整齐的JSON

现在我也可以向MarshalJSON()添加MarshalJSON()方法,但这要求我知道嵌入式类型也实现Employee,并且(a)复制其逻辑,或(b)致电MarshalJSON()' Person并以某种方式操纵其输出以适应我想要的地方。这两种方法看起来都很邋and,而且不是很有前途的证明(如果某天我不控制的嵌入式类型会添加自定义MarshalJSON()方法会怎么样?)

这里有没有其他选择我还没考虑过?

5 个答案:

答案 0 :(得分:6)

不要将MarshalJSON放在Person上,因为它被提升为外部类型。而是制作type Name stringName实施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 :(得分:1)

尽管这会产生与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中定义的字段。

答案 2 :(得分:0)

近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的其他详细信息和讨论。

答案 3 :(得分:0)

我在父结构上使用了这种方法,以防止嵌入的结构被覆盖封送处理:

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

答案 4 :(得分:-1)

在内部和外部字段中支持大量字段的更通用的方法。

副作用是您需要为每个外部结构编写此代码。

示例: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))
}