当前位置:首页 > 文章列表 > Golang > Go问答 > json/v2 解码 null 到指针字段的兼容处理

json/v2 解码 null 到指针字段的兼容处理

来源:17golang原创 2026-10-10 10:53:32 0浏览 收藏

接口迁移到 encoding/json/v2 时,最容易漏掉的不是字段名,而是 null 的业务含义。官方规则是:JSON null 解码到 Go 指针会把指针置为 nil;字段完全缺失时,解码器不会调用该字段的解码逻辑。若请求 DTO 只写成 *T,新对象上的“缺失”和“显式 null”最终都可能表现为 nil。

官方地址:https://pkg.go.dev/encoding/json/v2

要点速览
  • *T 适合表达“有值或没有值”,不适合独立承载三态 PATCH 语义。
  • 先决定 null 是清空、忽略,还是必须区别于缺失,再选择 DTO 形状。
  • 需要三态时,用 Present、Null、Value 包装,并把它转换成业务命令。

先区分指针字段的三种输入

把字段初始化为旧值,再分别解码三种请求,能看清兼容边界:缺失字段会保留原值,显式 null 会清成 nil,字符串则会分配或覆盖指针指向的值。可是新建一个零值 DTO 时,缺失与 null 都可能得到 nil,这正是旧接口迁移后出现“没有更新却被清空”或“无法清空”的根源。

输入*string 结果适合表达
字段缺失通常保持已有值未提交该字段
nullnil显式清空
\"Ada\"指向字符串设置新值
json v2 指针字段对缺失 null 和具体值的状态说明图
图1:json/v2 指针字段状态说明图,展示 null 与缺失的兼容边界。
package main

import (
	"fmt"
	"encoding/json/v2"
)

type Patch struct {
	Nickname *string `json:"nickname"`
}

func main() {
	old := "旧昵称"
	for label, input := range map[string][]byte{
		"缺失": []byte(`{}`),
		"null": []byte(`{"nickname":null}`),
		"新值": []byte(`{"nickname":"Ada"}`),
	} {
		p := Patch{Nickname: &old}
		if err := json.Unmarshal(input, &p); err != nil {
			panic(err) // 示例中直接终止,生产代码应返回带请求上下文的错误
		}
		fmt.Printf("%s: %#v\\n", label, p.Nickname)
	}
}

确定旧代码要保留的契约

迁移前先写一张策略表,不要用 omitempty 或多套指针层级猜业务意图。表单更新通常有三种契约:字段缺失代表“不改”,null 代表“清空”,具体值代表“替换”;另一类旧接口会把 null 当成“忽略”,这时就必须在 DTO 到命令的转换层显式保留旧规则。

  • 允许清空:普通 *T 可以接收 null,但要让业务层知道这是一次删除动作。
  • 忽略 null:解码后不要直接覆盖领域对象,先把 nil 解释为“无操作”。
  • 区分缺失:不要把零值 DTO 直接交给更新逻辑,改用存在性感知类型或同时记录原始字段集合。

兼容的关键是把“解析成功”与“业务动作”分开:解码器只负责把输入变成稳定状态,清空、保留或更新由命令层决定。

用存在性感知类型承接 null

需要三态语义的字段可以用一个小型泛型类型记录字段是否出现、是否为 null 以及实际值。缺失字段不会触发 UnmarshalJSON,因此 Present 能天然区分缺失;出现 null 时再把 Null 置为 true。

package patch

import (
	"bytes"
	"encoding/json/v2"
)

type Field[T any] struct {
	Present bool // 字段是否出现在请求 JSON 中
	Null    bool // 字段是否明确要求写入 null
	Value   T    // 非 null 时的业务值
}

func (f *Field[T]) UnmarshalJSON(data []byte) error {
	f.Present = true
	trimmed := bytes.TrimSpace(data)
	if bytes.Equal(trimmed, []byte("null")) {
		f.Null = true
		var zero T
		f.Value = zero // 清除复用对象中的旧值,避免状态串线
		return nil
	}
	f.Null = false
	return json.Unmarshal(trimmed, &f.Value) // 类型不匹配时把错误交给调用方
}

type UpdateProfile struct {
	Nickname Field[string] `json:"nickname"`
}

func toCommand(in UpdateProfile) string {
	if !in.Nickname.Present {
		return "忽略"
	}
	if in.Nickname.Null {
		return "清空"
	}
	return "更新为: " + in.Nickname.Value
}

这个包装只应放在确实有三态需求的字段上。普通响应对象若只需要“可有可无”,继续使用 *T 更直观;如果整个业务都依赖三态,可再统一封装请求命令,避免把 Present 泄漏到领域模型。

请求 JSON 经存在性感知字段进入业务命令的结构说明图
图2:兼容处理结构说明图,展示存在性感知字段到业务决策的边界。

用迁移测试锁住边界

最后把旧样本和业务动作写成表驱动测试,至少覆盖缺失、null、合法值和非法类型。测试不要只断言指针是否为 nil,还要断言转换后的命令:缺失应为忽略,null 应为清空,合法字符串应为更新,数字等错误类型应被拒绝。

func TestUpdateProfileStates(t *testing.T) {
	cases := []struct {
		name, input, want string
	}{
		{"缺失", `{}`, "忽略"},
		{"清空", `{"nickname":null}`, "清空"},
		{"更新", `{"nickname":"Ada"}`, "更新为: Ada"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			var req UpdateProfile
			if err := json.Unmarshal([]byte(tc.input), &req); err != nil {
				t.Fatalf("解码失败: %v", err) // 失败时保留输入场景,便于定位迁移差异
			}
			if got := toCommand(req); got != tc.want {
				t.Fatalf("动作=%q,期望=%q", got, tc.want)
			}
		})
	}
}

上线前再检查两件事:是否有复用同一个请求结构体的池化代码,以及响应端的 omitempty 是否仍符合旧客户端协议。json/v2 对空值和零值的定义存在差异,输入兼容通过并不代表输出 JSON 可以直接替换。

相关问题

只想判断字段是否传入,必须自定义类型吗?

不一定。可以先解码到字段集合记录存在性,再解码到普通结构体;字段少且需要三态的 PATCH 请求,使用 Field[T] 更容易让业务转换保持单一入口。

json/v2 会自动把旧指针字段变成三态吗?

不会。它会按规则处理 null 和具体值,但缺失字段仍然没有“出现”标记。三态是业务协议,需要由 DTO、原始字段集合或自定义解码类型主动承载。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Redis 官方 FastAPI SDK 发布后的 Python 应用接入路径Redis 官方 FastAPI SDK 发布后的 Python 应用接入路径
上一篇
Redis 官方 FastAPI SDK 发布后的 Python 应用接入路径
PHP readonly 属性克隆对象时的状态复制边界
下一篇
PHP readonly 属性克隆对象时的状态复制边界
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    402次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    478次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    487次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    433次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    260次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码