目录
写在前面
本文介绍了一个不需要修改 struct 的定义,就可以让该 struct 序列化成 json 格式时,在序列化结果中“凭空”多出来额外字段的方法。
这个方法原本是我在开发过程中碰到类似问题拍脑袋想出来的,本以为是原创,但悲催的是,在撰写本文之前,无意中发现 stack overflow 上已经有人提出了这个问题,最佳答案与我想出的方法完全一样,见:Can I use MarshalJSON to add arbitrary fields to a json encoding in golang?
为避免学舌,原本打算不写这篇文章了,但考虑目前貌似搜不出来这种方法的中文介绍,且 stack overflow 上的问答只说明的问题和解决方法,并未讨论这种方法的实际用途。所以我打算结合我的实际遭遇,把这件事的来龙去脉细细讲明,而如果你赶时间,可直接跳到方法一节。
背景
你应当对下面的代码不陌生:
package main
import (
"encoding/json"
"fmt"
)
type ColorGroup struct {
ID int `json:"id"`
Name string `json:"name"`
Colors []string `json:"colors"`
}
func main() {
group := ColorGroup{
ID: 1,
Name: "Reds",
Colors: []string{"Crimson", "Red", "Ruby", "Maroon"},
}
b, _ := json.Marshal(group)
fmt.Printf("%s\n", b)
// 输出:
// {"id":1,"name":"Reds","colors":["Crimson","Red","Ruby","Maroon"]}
}
这是一个简单地将 golang struct 序列化成 json 的例子,我从官网文档上拷下来的,稍有改动。
需要说明的是,golang 为 json 操作提供了 encoding/json
这个内置包,但也有如 github.com/json-iterator/go
、github.com/tidwall/gjson
这样的三方包提供额外的功能特性,为了缩小讨论范围,这里只考虑使用内置包来解决问题。
问题
我打赌将 struct 序列化成 json,十有八九是为了调 HTTP 接口。这里要讨论的问题也是如此,我们假设有一个 POST 接口,接受的 json 描述了一个富文本内容,如同下面的示例:
{
"title": "A apple", // 标题
"content": [ // 内容,接受三种类型的元素:文本、图片、超链接
{
"type": "text", // 一个文本元素
"text": "There is a apple."
},
{
"type": "text", // 又一个文本元素
"text": "It is red."
},
{
"type": "image", // 一个图片元素
"src": "http://example.com/apple.png"
},
{
"type": "link", // 一个超链接元素
"text": "more info",
"href": "http://example.com/more.info.html"
}
]
}
为方便调用这个接口,或者为了将 API 封装成 SDK,我们可能需要构造这样 golang 代码:
package main
import (
"encoding/json"
"fmt"
)
type Message struct {
Title string `json:"title"`
Content []interface{} `json:"content"`
}
type TextContent struct {
Type string `json:"type"`
Text string `json:"text"`
}
type ImageContent struct {
Type string `json:"type"`
Src string `json:"src"`
}
type LinkContent struct {
Type string `json:"type"`
Text string `json:"text"`
Link string `json:"link"`
}
func main() {
msg := Message{
Title: "A apple",
Content: []interface{}{
TextContent{
Type: "text",
Text: "There is a apple.",
},
TextContent{
Type: "text",
Text: "It is red.",
},
ImageContent{
Type: "image",
Src: "http://example.com/apple.png",
},
LinkContent{
Type: "link",
Text: "more info",
Link: "http://example.com/more.info.html",
},
},
}
buffer, _ := json.Marshal(msg)
fmt.Printf("%s\n", buffer)
// 输出:
// {"title":"A apple","content":[{"type":"text","text":"There is a apple."},{"type":"text","text":"It is red."},{"type":"image","src":"http://example.com/apple.png"},{"type":"link","text":"more info","link":"http://example.com/more.info.html"}]}
}
貌似很完美,但有没有发现让人不舒服的地方?是的,在这里:
TextContent{
Type: "text",
Text: "It is red.",
}
这个 struct 已经是 TextContent
类型了,却还需要一个 Type
字段来表明它是 text
,且不说这样多此一举,如果其他人调用这段代码,并没有细读 API 文档(事实上,API 封装成 SDK 就是为了向使用者屏蔽接口细节),写出了这样的代码:
TextContent{
Type: "txt",
Text: "It is red.",
}
哦吼,编译发现不了错,运行发现不了错,只有 API 请求真正发出去了,才可能收到 API 返回的错误信息,这不给人添麻烦么?
思路
其实这里的主要矛盾,就是 TextContent
本身的类型已经说明它是文本元素了,并不需要一个 Type
字段来显性的地说明它是 text
。换句话说,所有的 TextContent
实例的 Type
字段的值都应该固定是 text
,那这个字段压根儿就没有存在的必要不是吗?
但 json 的数据类型系统并不买账,对于 json 来说,object 就是 object,没有“object 是什么类型”这么一说,如果要区分两个 object,只能通过设置不同的字段或不同的字段值。所以它并不认为 type
字段是可有可无的。
所以思路来了,能不能让 TextContent
没有 Type
字段,但是在序列化成 json 时自动添加上 "type": "text"
呢?如果能实现成这样,那使用时就相当优雅了:
msg := Message{
Title: "A apple",
Content: []interface{}{
TextContent{
Text: "There is a apple.",
},
TextContent{
Text: "It is red.",
},
ImageContent{
Src: "http://example.com/apple.png",
},
LinkContent{
Text: "more info",
Link: "http://example.com/more.info.html",
},
},
}
瞬间清爽了对吗?可惜 json 默认的序列化操作并不知道我的小心思,我们需要额外的代码。
方法
golang json 包在做序列化或反序列化时,会有默认的行为,并不需要我们操太多心。但如果我们非要操心,可以让目标数据类型实现特定的接口,来自定义序列化或反序列化行为,相应的接口定义可以在 encoding/json 的接口文档里看到:
type Marshaler interface {
MarshalJSON() ([]byte, error)
}
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}
所以我们可以让上文中的三种 Content 分别实现 Marshaler 接口,以 TextContent
为例:
type TextContent struct {
Text string `json:"text"`
}
func (c TextContent) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
TextContent
Type string `json:"type"`
}{
TextContent: c,
Type: "text",
})
}
但注意!这是一个错误的示范,这样的代码会出现无限递归直到栈溢出,因为 TextContent.MarshalJSON
函数里调用了 json.Marshal
,而 json.Marshal
会隐性地调用 TextContent.MarshalJSON
,这样一个死结就解不开了。
修补的方案是不让 json.Marshal
调用 TextContent.MarshalJSON
,给 TextContent
换个马甲让 json.Marshal
认不出来:
type jsonTextContent TextContent
type TextContent struct {
Text string `json:"text"`
}
func (c TextContent) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
jsonTextContent
Type string `json:"type"`
}{
jsonTextContent: jsonTextContent(c),
Type: "text",
})
}
这就是我想说的方法的全部内容了。
附
完整的示例代码:
package main
import (
"encoding/json"
"fmt"
)
type Message struct {
Title string `json:"title"`
Content []interface{} `json:"content"`
}
type jsonTextContent TextContent
type TextContent struct {
Text string `json:"text"`
}
func (c TextContent) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
jsonTextContent
Type string `json:"type"`
}{
jsonTextContent: jsonTextContent(c),
Type: "text",
})
}
type jsonImageContent ImageContent
type ImageContent struct {
Src string `json:"src"`
}
func (c ImageContent) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
jsonImageContent
Type string `json:"type"`
}{
jsonImageContent: jsonImageContent(c),
Type: "image",
})
}
type jsonLinkContent LinkContent
type LinkContent struct {
Text string `json:"text"`
Link string `json:"link"`
}
func (c LinkContent) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
jsonLinkContent
Type string `json:"type"`
}{
jsonLinkContent: jsonLinkContent(c),
Type: "link",
})
}
func main() {
msg := Message{
Title: "A apple",
Content: []interface{}{
TextContent{
Text: "There is a apple.",
},
TextContent{
Text: "It is red.",
},
ImageContent{
Src: "http://example.com/apple.png",
},
LinkContent{
Text: "more info",
Link: "http://example.com/more.info.html",
},
},
}
buffer, _ := json.Marshal(msg)
fmt.Printf("%s\n", buffer)
// 输出:
// {"title":"A apple","content":[{"text":"There is a apple.","type":"text"},{"text":"It is red.","type":"text"},{"src":"http://example.com/apple.png","type":"image"},{"text":"more info","link":"http://example.com/more.info.html","type":"link"}]}
}
评论加载中……
若长时间无法加载,请刷新页面重试,或直接访问。