如果一个 Go 服务接收的是“任意 JSON 片段”,又希望把它放回一个更大的 JSON 对象,直接把字节拼到字符串里很容易留下半截对象、未闭合字符串或重复键。更稳妥的做法是把片段先放进 jsontext.Value,用 IsValid 判断语法,再交给 jsontext.Encoder.WriteValue 写入完整文档。
官方地址:https://pkg.go.dev/encoding/json/jsontext
这篇只讨论 JSON 语法层:片段是否合法、错误如何保留、如何写回对象。字段是否符合业务规则,例如 age 是否为正数,不属于 jsontext 的职责。
先分清 jsontext.Value 管的是什么
Go 官方文档把 JSON 的 Token 和 Value 分开:Token 是字符串、数字、对象边界等基础结构;Value 则是一整个 JSON 值,可以是字面量、字符串、数字、对象或数组。也就是说,jsontext.Value 适合暂存一段完整的原始 JSON,而不是把它立即映射成某个业务结构体。
因此,下面这段代码中的 raw 可以是对象、数组或单个数字,但不能是两个连续对象,也不能只是对象内部的一组成员:
// Value 表示一个完整的 JSON 值,原始字节仍按 JSON 语法解释。
var raw jsontext.Value = jsontext.Value(`{"level":"info","retry":2}`)
// 对象和数组都属于完整 Value,可以继续交给 Encoder 写入。
var list = jsontext.Value(`["go","json"]`)
需要注意,Value 的底层类型是 []byte。把输入切片转换成 Value 不等于已经做了校验;官方文档明确区分了保存原始编码和检查语法这两个动作。
先用 IsValid 拦截不合法片段
IsValid 返回一个布尔值,默认按较严格的 JSON 语法检查 UTF-8、字符串转义和对象成员名唯一性。它很适合做快速分支:合法就继续,非法就拒绝写回。
不过,布尔值本身不携带错误位置。工程代码通常会在失败时对副本调用 Format,让它返回一个可包装的语法错误;成功时再返回 Clone,避免后续调用继续依赖外部输入缓冲区。
package main
import (
"errors"
"fmt"
"encoding/json/jsontext"
)
// validateFragment 只负责语法层校验,不判断字段是否满足业务含义。
func validateFragment(raw []byte) (jsontext.Value, error) {
value := jsontext.Value(raw)
if value.IsValid() {
// Clone 让返回值拥有独立的字节副本,避免依赖调用方复用的缓冲区。
return value.Clone(), nil
}
// IsValid 只有布尔结果;副本 Format 失败时可以提供语法错误上下文。
probe := value.Clone()
if err := probe.Format(); err != nil {
return nil, fmt.Errorf("JSON 片段语法无效: %w", err)
}
// 作为兜底分支,避免未来选项变化时把非法值继续向下传递。
return nil, errors.New("JSON 片段校验失败")
}
这里的关键点有两个。第一,IsValid 校验的是 JSON 语法,不会替你判断数字是否能放进 int64 或 float64。第二,UnmarshalJSON 保存原始输入时也不等于完成验证,所以外部输入仍应显式调用 IsValid。
用 Encoder.WriteValue 写回完整对象
片段通过校验后,不要再用 "{" + key + ":" + fragment + "}" 拼接。用 Encoder 写对象边界、键和值,编码器会按自身的 JSON 状态机拒绝不合适的 Token 或 Value。
// writeBack 把一个完整 JSON Value 放入 payload 字段。
func writeBack(fragment jsontext.Value) (jsontext.Value, error) {
if !fragment.IsValid() {
return nil, errors.New("拒绝写回未通过语法校验的片段")
}
var out bytes.Buffer
enc := jsontext.NewEncoder(&out)
// 先写对象边界和字段名,再写入完整 Value,避免手工拼接分隔符。
if err := enc.WriteToken(jsontext.BeginObject); err != nil {
return nil, fmt.Errorf("写入对象开始符失败: %w", err)
}
if err := enc.WriteToken(jsontext.String("payload")); err != nil {
return nil, fmt.Errorf("写入 payload 字段失败: %w", err)
}
if err := enc.WriteValue(fragment); err != nil {
return nil, fmt.Errorf("写入 JSON 片段失败: %w", err)
}
if err := enc.WriteToken(jsontext.EndObject); err != nil {
return nil, fmt.Errorf("写入对象结束符失败: %w", err)
}
// 复制 Buffer 内容,让返回的 Value 与 Buffer 的生命周期脱钩。
return jsontext.Value(append([]byte(nil), out.Bytes()...)), nil
}
完整示例还需要在 import 中加入 bytes。调用方可以先执行 validateFragment,再把返回的 Value 传给 writeBack。如果片段是数组,结果就是 {"payload":[...]};如果片段是数字,结果也仍是合法的 JSON 对象。
WriteValue 不是业务字段校验器。它关心的是当前编码器状态能否接收这个完整 JSON 值,以及写入后整体结构是否仍符合 JSON 语法。
语法错误、重复键和空输入怎么处理
把边界分开处理,代码会更容易维护:
| 情况 | 建议 | 原因 |
|---|---|---|
| 缺少括号或引号 | 先 IsValid,失败时用 Format 返回错误 | 不能把半截片段交给 Encoder |
| 对象键重复 | 默认严格拒绝;只有明确兼容协议时才考虑 AllowDuplicateNames | 重复键会让不同消费者产生不同理解 |
| 空切片或空白 | 在入口明确拒绝,或把协议定义为 JSON null | 空字节不是一个完整 JSON 值 |
| 数字超出常见类型 | 先按 JSON 语法接收,再在业务层做数值范围检查 | IsValid 不负责 float64、int64 的范围 |
| 输入缓冲区会复用 | 校验成功后 Clone,写回结果再复制 Buffer | 避免 Value 引用已被覆盖的字节 |
如果协议允许重复对象键,应把这个选择写进接口约定,而不是因为某个样例“刚好能解析”就放宽规则。对于普通配置、接口请求和持久化 JSON,默认严格规则更容易让上下游保持一致。
一套简洁的调用顺序
可以把整个过程压缩成“保存、校验、写回”三段,每段只做一件事:
// handleFragment 将外部片段接入完整 JSON 文档。
func handleFragment(raw []byte) ([]byte, error) {
// 第一步:先复制并校验,失败立即返回,不进入写回阶段。
fragment, err := validateFragment(raw)
if err != nil {
return nil, err
}
// 第二步:让 Encoder 负责对象结构和 Value 的组合。
document, err := writeBack(fragment)
if err != nil {
return nil, err
}
// 第三步:返回完整文档的独立字节,便于交给 HTTP 或存储层。
return append([]byte(nil), document...), nil
}
这个顺序的价值不在于代码量少,而在于失败点清晰:validateFragment 失败代表输入不是合法 JSON;writeBack 失败代表编码器不能接受当前结构;两者都成功后,才把完整文档交给业务层。
常见问题
jsontext.Value 能直接替代业务结构体吗?
不能。它解决的是原始 JSON 值的语法处理和保留,字段类型、必填项、枚举值和权限规则仍应由业务结构体或专门的校验逻辑处理。
IsValid 返回 false 时为什么还要调用 Format?
因为 IsValid 只返回布尔值。对副本调用 Format 可以拿到 error,便于记录或包装;不要在失败分支继续写回原始片段。
为什么不直接使用字符串拼接?
字符串拼接无法可靠处理引号、转义、逗号和对象边界。由 Encoder 写 Token 与 Value,结构错误会在写入时暴露,代码也更容易按阶段处理。
校验通过就代表数据安全了吗?
只代表语法层成立。数值范围、字段含义、大小限制、权限和业务状态仍要单独检查,不能把语法合法当成业务可信。
总结
用 jsontext.Value 承接原始 JSON 片段时,先用 IsValid 做快速语法判断,失败时用 Format 获取错误,再用 Encoder.WriteValue 写入对象。成功后再进入业务校验和传输层,既能避免手工拼接 JSON 的边界问题,也能把语法错误和业务错误分开。