Go 语言发布全新的 JSON API:encoding/json/v2 与 jsontext 正式稳定
A new Go API for JSON
Go 语言团队宣布全新的 `encoding/json/v2` 和 `encoding/json/jsontext` 软件包在 Go 1.27 中稳定并普遍可用。新版本底层采用基于语法和语义分离的设计,支持流式处理、一等公民选项配置及更严格的 RFC 8259 规范校验,同时在底层实现了与旧版 v1 的双向兼容。
新发布的 encoding/json/v2 和 jsontext 解决了旧版 API 在语法规范、流式处理和性能上的局限,并实现了与 v1 的双向兼容,为 Go 语言生态提供了更安全高效的 JSON 处理方案。
Michael Pratt, Joe Tsai, Daniel Martí, Johan Brandhorst-Satzkorn, Roger Peppe, Chris Hines 和 Damien Neil
2026年11月4日
引言
JavaScript 对象表示法 (JSON) 是一种简单的数据交换格式,是互联网上最流行的数据格式,并被 Go 程序广泛读取和编写。 早在 Go 1.0 发布之前,Go 的标准库就已经包含了对 JSON 的支持。 我们在 15 年多前就首次撰写了关于Go 对 JSON 的支持的文章!
随着时间推移,包会随着用户需求的发展而演进,encoding/json 也不例外。
去年,我们撰写了一篇关于用于 JSON 的新实验性 API 的文章,旨在带来期待已久的改进和修复。
在收集了更多反馈并进行改进后,全新的 encoding/json/v2 和 encoding/json/jsontext 包在 Go 1.27 中已经稳定并全面可用!
这篇文章是介绍该实验的文章的更新版本,描述了 encoding/json 存在的问题,并介绍了新的包。
encoding/json 的问题
总体而言,encoding/json 经受住了时间的考验。
将任意 Go 类型与 JSON 中的某些默认表示形式进行序列化(marshal)和反序列化(unmarshal)的思想,结合自定义表示形式的能力,已被证明具有高度的灵活性。
然而,自其推出以来的这些年里,各种用户已经发现了众多的缺点。
行为缺陷
encoding/json 中存在各种行为缺陷:
-
对 JSON 语法处理不够精确:多年来,JSON 的标准化程度不断提高,以便程序能够正确通信。 通常,解码器在拒绝模糊输入时会变得更加严格,以减少两个实现对特定 JSON 值产生不同(成功)解释的可能性。
-
encoding/json目前接受无效的 UTF-8, 而最新的互联网标准 (RFC 8259) 要求必须是有效的 UTF-8。 在存在无效 UTF-8 的情况下,默认行为应该报告错误, 而不是引入静默数据损坏,这可能会导致下游出现问题。 -
encoding/json目前接受带有重复成员名称的对象。 RFC 8259 没有规定如何处理重复名称, 因此实现可以自由选择任意值、合并这些值、丢弃这些值或报告错误。 存在重复名称会导致 JSON 值没有普遍共识的含义。 这可能会被安全应用中的攻击者利用,并且过去曾被利用过(例如 CVE-2017-12635)。 默认行为应倾向于安全性并拒绝重复名称。
-
-
泄漏切片和映射的 nil 性:JSON 常用于与以下程序的通信:这些程序使用的 JSON 实现不允许将
null反序列化为预期是 JSON 数组或对象的数据类型。 由于encoding/json会将 nil 切片或映射序列化为 JSONnull, 这可能会导致其他实现进行反序列化时出错。 一项调查 表明,大多数 Go 用户更倾向于默认将 nil 切片和映射序列化为空的 JSON 数组或对象。 -
不区分大小写的反序列化:在反序列化时,JSON 对象成员名称会通过不区分大小写的匹配解析为 Go 结构体字段名称。 这是一个令人惊讶的默认行为、潜在的安全漏洞以及性能限制。
-
方法调用不一致:由于实现细节,
MarshalJSON在指针接收器上声明的方法 被encoding/json不一致地调用。虽然被视为一个 Bug, 但由于有太多应用程序依赖当前行为,因此无法修复。
API 不足
encoding/json 的 API 可能很棘手或具有限制性:
-
从
io.Reader中正确反序列化很困难。 用户经常编写json.NewDecoder(r).Decode(v), 这无法拒绝输入末尾的多余垃圾数据。 -
选项可以在
Encoder和Decoder类型上设置, 但不能与Marshal和Unmarshal函数一起使用。 类似地,实现Marshaler和Unmarshaler接口的类型 无法使用这些选项,并且无法将选项传递到调用栈下方。 例如,当调用自定义Decoder.DisallowUnknownFields方法时,UnmarshalJSON选项会失效。 -
Compact、Indent和HTMLEscape函数写入bytes.Buffer, 而不是像[]byte或io.Writer这样更灵活的东西。 这限制了这些函数的可用性。
性能限制
抛开内部实现细节不谈, 公共 API 使其受到某些性能限制:
-
MarshalJSON:
MarshalJSON接口方法强制实现 分配返回的[]byte。此外,语义要求encoding/json验证结果是否为合法的 JSON, 并将其重新格式化以匹配指定的缩进。 -
UnmarshalJSON:
UnmarshalJSON接口方法要求 提供完整的 JSON 值(没有任何尾随数据)。 这迫使encoding/json完整解析要反序列化的 JSON 值, 以便在调用UnmarshalJSON之前确定其结束位置。 此后,UnmarshalJSON方法本身必须再次解析提供的 JSON 值。 -
缺乏流式处理:尽管
Encoder和Decoder类型操作 在io.Writer或io.Reader上,但它们会在内存中缓冲整个 JSON 值。 用于读取单个 Token 的Decoder.Token方法 分配开销很大,并且没有相应的 API 用于写入 Token。
此外,如果 MarshalJSON 或 UnmarshalJSON 方法的实现
递归调用 Marshal 或 Unmarshal 函数,
则性能将呈平方级下降。
直接尝试修复 encoding/json
引入包的新不兼容主要版本是一个需要权衡的重大决定。 如果可能,我们应该尝试修复现有的包。
虽然添加新功能相对容易, 但更改现有功能却很困难。 不幸的是,这些问题是现有 API 的固有后果, 使得在 Go 1 兼容性承诺 范围内几乎不可能修复它们。
我们原则上可以声明单独的名称,例如 MarshalV2 或 UnmarshalV2,
但这等同于在同一个包中创建一个平行的命名空间。
这引导我们走向 encoding/json/v2(此后称为 v2),
在这里我们可以在单独的 v2 命名空间中进行这些更改,
而与 encoding/json(此后称为 v1)形成对比。
构建在 encoding/json/jsontext 之上
在讨论 v2 API 之前,我们首先介绍
encoding/json/jsontext 包,
它为 Go 中 JSON 的改进奠定了基础。
Go 中的 JSON 序列化可以细分为两个主要组件:
- 语法功能,关注基于其语法处理 JSON;以及
- 语义功能,定义 JSON 值与 Go 值之间的关系。
我们使用术语“编码 (encode)”和“解码 (decode)”来描述语法功能, 并使用术语“序列化 (marshal)”和“反序列化 (unmarshal)”来描述语义功能。 我们的目标是明确区分 纯粹关注编码的功能与涉及序列化的功能。
此图概述了这种分离。
紫色区块代表类型,而蓝色区块代表函数或方法。
箭头的方向大致代表数据流向。
图表的下半部分由 jsontext 包实现,
包含仅关注语法的功用,
而上半部分由 json/v2 包实现,
包含为下半部分处理的语法数据赋予语义含义的功用。
jsontext 的基本 API 如下:
package jsontext
type Encoder struct { ... }
func NewEncoder(io.Writer, ...Options) *Encoder
func (*Encoder) WriteValue(Value) error
func (*Encoder) WriteToken(Token) error
type Decoder struct { ... }
func NewDecoder(io.Reader, ...Options) *Decoder
func (*Decoder) ReadValue() (Value, error)
func (*Decoder) ReadToken() (Token, error)
type Kind byte
type Value []byte
func (Value) Kind() Kind
type Token struct { ... }
func (Token) Kind() Kind
jsontext 包提供了在语法层级与 JSON 进行交互的功能,其名称源自
RFC 8259, section 2,
其中 JSON 数据的语法字面量被称为 JSON-text。
由于它仅在语法层级与 JSON 交互,
因此它不依赖 Go 反射。
Encoder 和
Decoder
提供了对编码和解码 JSON 值与 Token 的支持。
构造函数
接受可变参数选项,
这些选项会影响编码和解码的具体行为。
与 v1 中声明的 Encoder 和 Decoder 类型不同,
jsontext 中的新类型避免了混淆语法与语义的区别,并以真正的流式方式运行。
一个 JSON 值是一个完整的数据单元,在 Go 中表示为
一个命名的 []byte。
它与 v1 中的 RawMessage 相同,后者现在是 jsontext.Value 的类型别名。
一个 JSON 值在语法上由一个或多个 JSON Token 组成。
一个 JSON Token 在 Go 中表示为带有构造函数和访问器方法的
不透明的 Token 类型。
它类似于 v1 中的 Token,
但旨在表示任意的 JSON Token 而无需分配内存。
为了解决 MarshalJSON 和 UnmarshalJSON 接口方法的根本性能问题,
我们需要一种有效的方法来将 JSON 编码和解码为 Token 和值的流式序列。
在 v2 中,我们引入了 MarshalJSONTo 和 UnmarshalJSONFrom 接口方法,
它们对 Encoder 或 Decoder 进行操作,允许方法的实现
以纯流式方式处理 JSON。因此,json 包不需要
负责验证或格式化由 MarshalJSON 返回的 JSON 值,
也不需要负责确定提供给 UnmarshalJSON 的 JSON 值的边界。这些责任属于 Encoder 和 Decoder。
介绍 encoding/json/v2
在 jsontext 包的基础上,我们现在介绍
encoding/json/v2 包。
它旨在修复 v1 中的问题,
同时对 v1 包的用户保持熟悉。
我们的目标是,如果直接迁移到 v2,v1 的使用方式将大体上保持相同。
在此,我们主要介绍 v2 的高级 API。
有关如何使用它的示例,我们鼓励读者
研究 v2 包中的示例、
“处理 JSON”教程,或
阅读 Anton Zhiyanov 涵盖该主题的博客。
v2 的基本 API 如下:
package json
func Marshal(in any, opts ...Options) (out []byte, err error)
func MarshalWrite(out io.Writer, in any, opts ...Options) error
func MarshalEncode(out *jsontext.Encoder, in any, opts ...Options) error
func Unmarshal(in []byte, out any, opts ...Options) error
func UnmarshalRead(in io.Reader, out any, opts ...Options) error
func UnmarshalDecode(in *jsontext.Decoder, out any, opts ...Options) error
Marshal
和 Unmarshal 函数
的签名与 v1 类似,但接受用于配置其行为的选项。
MarshalWrite
和 UnmarshalRead 函数
直接操作 io.Writer 或 io.Reader,
从而避免了仅仅为了写入或读取此类类型
而临时构造 Encoder 或 Decoder。
MarshalEncode
和 UnmarshalDecode 函数
操作 jsontext.Encoder 和 jsontext.Decoder,
并且实际上是前面提到的函数的底层实现。
与 v1 不同,选项是每个序列化和反序列化函数的一等参数,
极大地扩展了 v2 的灵活性和可配置性。
本文未涵盖 v2 中可用的几个选项。
类型指定的自定义
与 v1 类似,v2 允许类型通过实现特定的接口来定义它们自己的 JSON 表示形式。
type Marshaler interface {
MarshalJSON() ([]byte, error)
}
type MarshalerTo interface {
MarshalJSONTo(*jsontext.Encoder) error
}
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}
type UnmarshalerFrom interface {
UnmarshalJSONFrom(*jsontext.Decoder) error
}
Marshaler 和 Unmarshaler 接口与 v1 中的接口完全相同。
新的 MarshalerTo 和 UnmarshalerFrom 接口允许类型使用 jsontext.Encoder 或 jsontext.Decoder 将自身表示为 JSON。
这允许选项向下传递到调用栈,因为可以通过 Encoder 或 Decoder 上的 Options 访问器方法来检索选项。
请参阅 OrderedObject 示例了解如何实现一个维护 JSON 对象成员顺序的自定义类型。
调用者指定的自定义
在 v2 中,Marshal 和 Unmarshal 的调用者还可以为任意类型指定自定义的 JSON 表示形式,
其中调用者指定的函数优先于类型定义的方法或特定类型的默认表示形式。
func WithMarshalers(*Marshalers) Options
type Marshalers struct { ... }
func MarshalFunc[T any](fn func(T) ([]byte, error)) *Marshalers
func MarshalToFunc[T any](fn func(*jsontext.Encoder, T) error) *Marshalers
func WithUnmarshalers(*Unmarshalers) Options
type Unmarshalers struct { ... }
func UnmarshalFunc[T any](fn func([]byte, T) error) *Unmarshalers
func UnmarshalFromFunc[T any](fn func(*jsontext.Decoder, T) error) *Unmarshalers
MarshalFunc 和 MarshalToFunc 构造一个自定义序列化器,可以将其传递给使用 WithMarshalers 的 Marshal 调用,以覆盖特定类型的序列化。
类似地,
UnmarshalFunc 和 UnmarshalFromFunc
支持对 Unmarshal 进行类似的自定义。
ProtoJSON 示例
展示了此功能如何允许所有 proto.Message 类型的序列化由 protojson 包处理。
行为差异
虽然 v2 旨在其行为与 v1 大部分相同,
但其行为已在某些方面发生了变化,以解决 v1 中的问题,最显著的是:
-
如果存在无效的 UTF-8,
v2会报告错误。 -
如果 JSON 对象包含重复的名称,
v2会报告错误。 -
v2分别将 nil Go 切片或 Go 映射序列化为空 JSON 数组或 JSON 对象。 -
v2使用从 JSON 成员名称到 Go 字段名称的区分大小写的匹配,将 JSON 对象反序列化为 Go 结构体。 -
v2重新定义了omitempty标签选项,以便在字段会被编码为“空”JSON 值(即null、""、[]和{})时省略该字段。 -
当尝试序列化目前没有默认表示形式的
time.Duration时,v2会报告错误, 但它提供了选项让调用者决定。
对于大多数行为更改,都有一个结构体标签选项或调用者指定的选项,
可以将行为配置为在 v1 或 v2 语义下运行,甚至可以采用其他调用者确定的行为。
有关更多信息,请参阅“迁移至 v2”。
性能优化
v2 的 Marshal 性能大致与 v1 持平。
有时它略快,但有时也略慢。
v2 的 Unmarshal 性能明显快于 v1,
基准测试显示性能提升高达 10 倍。
为了获得更大的性能提升,Marshaler 和 Unmarshaler 的现有实现应迁移至同时实现 MarshalerTo 和 UnmarshalerFrom,以便它们能从纯流式处理 JSON 中受益。例如,在 UnmarshalJSON 方法中递归解析 OpenAPI 规范严重损害了 Kubernetes 某项服务的性能(参见 kubernetes/kube-openapi#315),而切换到 UnmarshalJSONFrom 则使性能提升了几个数量级。
有关更多基准测试详情,请参阅 go-json-experiment/jsonbench 仓库。
除了结构上的改进,我们还在持续投入性能优化,针对 那些 v2 实现比 v1 更慢的场景进行改进。您可以期待 Go 1.28 版本带来更多提升。
兼容性
Go 1 兼容性承诺是 Go 项目的一项核心保证,它使开发者能够构建经得起时间考验的应用程序,并确保其在未来的 Go 版本中继续运行。
原有的 encoding/json 和新的 encoding/json/v2 软件包当然都遵循 Go 1 兼容性承诺。使用 v1 的应用程序将无限期地获得支持,且无需迁移到 v2。
话虽如此,在设计 v2 软件包时,我们希望做得比仅仅维持 v1 的支持更好。我们希望确保 v1 和 v2 彼此兼容,以便使用不同版本的软件包可以协同工作。这样做可以实现生态系统的平滑迁移,而不是因为依赖项的选择而强迫用户必须使用某个版本。
例如,假设您的软件包使用 v2 来编组一个结构体,其中包含一个在另一个软件包中定义类型的字段。该软件包可能使用原有的 v1 MarshalJSON 方法来定义其编组行为。没问题,v2 支持 MarshalJSON 方法,因此编组将按预期继续工作。
反之,假设您的软件包使用 v1 来编组同一个结构体。另一个软件包可能为了获得更好的性能而使用 v2 MarshalJSONTo 方法。这里也没有问题,因为 v1 的实现底层支持 v2 的 MarshalJSONTo 方法。
我们通过设计新的软件包实现了这一点,即在底层,v1 是基于 v2 实现的。如上所述,v2 支持多种 Options。每种行为差异都由这些选项之一控制。因此,通过传递全套 v1 兼容性选项,v1 可以基于 v2 来实现。
采用这种方法,向后兼容的添加内容(例如新方法 MarshalJSONTo 或新结构体标签 embed)将自动得到支持,从而使现有的 v1 用户受益。
迁移
基于 v2 选项实现 v1 的另一个好处是允许对调用点进行逐步迁移。
encoding/json 记录了 v1 和 v2 之间全套的行为差异,以及哪些选项控制这些行为。
许多应用程序可以直接迁移到 v2 而无需明显的行为更改,但依赖 v1 行为的应用程序可以使用这些选项仅更改它们所依赖的行为。
例如,我们可以选择除允许重复名称之外的所有 v2 行为:
json.Marshal(..., json.DefaultOptionsV2(), jsontext.AllowDuplicateName(true))
或者,我们可以选择除禁止重复名称之外的所有 v1 行为:
json.Marshal(..., jsonv1.DefaultOptionsV1(), jsontext.AllowDuplicateName(false))
encoding/json/v2 迁移指南介绍了更多细节,涵盖将应用程序从 v1 迁移到 v2 的多种场景。
更多信息
有关使用新 JSON 包的其他参考资料,请参阅:
- “处理 JSON”教程,
encoding/json/v2和encoding/json/jsontext包文档,encoding/json包的v2对比文档,- 以及
encoding/json/v2迁移指南。
酝酿已久
encoding/json 新大版本的规划历时多年。
2020 年底,在无法修复当前包中问题的推动下,
Daniel Martí(encoding/json 的维护者之一)首先起草了他关于假想的 v2 包应该是什么样的想法。
在此之前,在完成了协议缓冲区的 Go API 相关工作后,
Joe Tsai 感到遗憾的是,protojson 包
需要使用自定义的 JSON 实现,因为 encoding/json
既无法遵循协议缓冲区规范要求的更严格的 JSON 标准,
也无法以流式传输的方式高效序列化 JSON。
坚信 JSON 的美好未来既有益又可以实现,
Daniel 和 Joe 联手集思广益设计 v2 并
开始构建原型
(初始代码是 Go protobuf 模块中 JSON 序列化逻辑的精炼版本)。
随着时间推移,其他几位成员(Roger Peppe、Chris Hines、Johan Brandhorst-Satzkorn、Damien Neil、Austin Clements 和 Michael Pratt)
通过提供设计审查、代码审查和回归测试加入了这项工作。
许多早期讨论都已公开在我们的
会议录音和会议纪要中。
之后的讨论可以在我们的 encoding/json/v2 工作组会议纪要中找到。
这项工作从一开始就是公开的,
并且我们越来越多地让更广泛的 Go 社区参与进来,
首先是通过
GopherCon 演讲和
2023 年底发布的讨论、
2025 年初发布的正式提案,
最后在 Go 1.25 中将 encoding/json/v2 采纳为 Go 实验特性,
以便所有 Go 用户进行更大规模的测试。
v2 工作历时约 6 年才达到这一点,
吸收了众多贡献者的反馈,并在生产环境的使用中积累了宝贵的
经验数据。
值得一提的是,它在很大程度上是由并非受雇于 Google 的人员开发和推广的,这表明 Go 项目是一项合作事业,拥有致力于改善 Go 生态系统的繁荣全球社区。
感谢所有做出贡献的人!
来源:Hacker News-首页 · go.dev