登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go jsontext.Value 怎样校验片段后再写回 JSON

来源:17golang原创

时间:2026-11-13 21:25:27 280浏览 收藏

如果一个 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 不等于已经做了校验;官方文档明确区分了保存原始编码和检查语法这两个动作。

原始片段、jsontext.Value、IsValid、Format 与语法错误的静态关系说明图
图1:JSON 片段进入 jsontext.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 语法。

已校验 Value 经 Encoder.WriteValue 写回完整 JSON 对象的静态关系说明图
图2:Encoder 将已校验 Value 放入对象文档的关系说明图,不是运行截图。

语法错误、重复键和空输入怎么处理

把边界分开处理,代码会更容易维护:

情况建议原因
缺少括号或引号先 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 的边界问题,也能把语法错误和业务错误分开。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>