当前位置:首页 > 文章列表 > Golang > Go问答 > encoding/json 解码时怎样区分字段缺失和显式 null

encoding/json 解码时怎样区分字段缺失和显式 null

来源:17golang原创 2026-11-13 11:13:40 0浏览 收藏

在部分更新接口里,JSON 字段“没有出现”和“明确写成 null”通常不是同一个动作:前者可能表示保持原值,后者可能表示清空。Go 的 encoding/json 如果直接把两种输入解码到普通字段,很多时候只留下一个零值,业务层就无法判断用户到底提交了什么。

解决思路是先确定接口需要几种状态,再选择承载方式:简单场景用指针表达“有值/无值”,需要严格区分字段是否出现时用 json.RawMessage,需要让业务代码只面对统一结果时,把判断封装进自定义 UnmarshalJSON。

不要先问“哪个字段类型最方便”,先问“字段缺失、显式 null 和具体值是否对应不同业务动作”。只要三者有区别,就不能把它们都当成普通零值处理。

先把字段状态定义成三态

以修改用户备注为例,接口可能有三种输入:请求体没有 remark,表示不修改;"remark": null,表示清空;"remark": "周末联系",表示替换成新内容。这个判断属于输入契约,不是 JSON 库自动替你做出的业务决策。

字段未出现、显式 null 和有效值进入 Go 解码对象后的三态关系说明图
图1:结构说明图,展示字段存在性与值状态的对应关系;这是静态示意,不是运行截图。

如果三种输入最后都落成空字符串或 nil,更新函数就无法区分“保持不变”和“清空”。因此先把状态写成表格更稳妥:

JSON 输入接口语义示例需要保留的信号
字段未出现保持数据库原值字段是否出现
"remark": null清空数据库字段出现且值为 null
"remark": "周末联系"写入新值出现且值可解析

用指针处理最简单的有值与无值

当业务只需要区分“请求里给了一个值”和“没有可用值”时,指针字段很直接。对指针字段解码时,JSON 的 null 会得到 nil;字段未出现时,字段也不会被填充。因此它适合“非 nil 就更新,nil 就跳过”的二态场景,但不能独立区分缺失和显式 null。

type PatchUser struct {
	// 指针用来表达:本次请求是否提供了一个非 null 的备注值。
	Remark *string `json:"remark"`
}

func applySimplePatch(dst *string, patch PatchUser) {
	// nil 同时覆盖“字段缺失”和“字段显式为 null”,这里只能选择跳过。
	if patch.Remark == nil {
		return
	}
	// 非 nil 时才把请求中的具体字符串写回业务对象。
	*dst = *patch.Remark
}

如果你的接口约定是“null 也不修改”,这个模型已经足够;如果约定是“null 要清空”,就必须增加字段存在信号。一个常见做法是给指针再包一层状态类型,而不是在业务层猜 nil 的含义。

用 RawMessage 捕获字段是否出现

json.RawMessage 可以暂存字段的原始 JSON 字节。把它放在带指针的中间结构中,可以先判断键是否出现,再判断原始内容是否为 null,最后才把具体值解码到目标类型。

type PatchUserRaw struct {
	// RawMessage 保留字段的原始 JSON,便于区分缺失与 null。
	Remark *json.RawMessage `json:"remark"`
}

func parseRemark(data []byte) (present bool, clear bool, value string, err error) {
	var raw PatchUserRaw
	if err := json.Unmarshal(data, &raw); err != nil {
		// 输入不是合法 JSON 时,把错误交给上层,不把它当成字段缺失。
		return false, false, "", err
	}
	if raw.Remark == nil {
		// 指针为 nil 代表字段没有提供,或输入明确为 null;继续看原始结构无法区分。
		return false, false, "", nil
	}
	if bytes.Equal(bytes.TrimSpace(*raw.Remark), []byte("null")) {
		// 出现且是 null,交给业务层执行清空动作。
		return true, true, "", nil
	}
	if err := json.Unmarshal(*raw.Remark, &value); err != nil {
		// 出现但类型不对时保留解析错误,避免静默写入空值。
		return true, false, "", err
	}
	return true, false, value, nil
}

这里有一个容易忽略的限制:*json.RawMessage 仍然会把缺失和显式 null 都表现为 nil。若要保留三态,应该使用自定义类型的 UnmarshalJSON,或者先把整个对象解码到 map[string]json.RawMessage,通过键是否存在来判断。

用 map 加 RawMessage 直接判断键是否存在

当字段数量不固定,或者只需要处理少数动态字段时,map[string]json.RawMessage 更直观:map 中没有键就是缺失;有键但字节内容是 null 就是显式 null;其他内容再解码成目标类型。

func parseDynamicRemark(data []byte) (present bool, clear bool, value string, err error) {
	var fields map[string]json.RawMessage
	if err := json.Unmarshal(data, &fields); err != nil {
		// 顶层不是对象或 JSON 格式错误,都应作为输入错误返回。
		return false, false, "", err
	}
	raw, ok := fields["remark"]
	if !ok {
		// map 中没有键,表示客户端没有提交 remark。
		return false, false, "", nil
	}
	if bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
		// 键存在且值为 null,表示显式清空。
		return true, true, "", nil
	}
	if err := json.Unmarshal(raw, &value); err != nil {
		// 类型不匹配时不要降级成默认字符串。
		return true, false, "", err
	}
	return true, false, value, nil
}

这个方案的代价是字段检查和类型解码都由调用方维护,适合动态字段,不适合让大量业务结构都依赖字符串键。固定请求结构更推荐下一节的自定义类型。

用自定义 UnmarshalJSON 封装三态结果

可以定义一个带 Set、Null、Value 的字段类型:Set=false 表示缺失;Set=true, Null=true 表示显式 null;Set=true, Null=false 表示出现了具体值。自定义解码器只负责把输入翻译成这三个状态,更新逻辑仍由业务层决定。

自定义 UnmarshalJSON 通过 RawMessage、存在标记和 null 判断封装三态结果的关系说明图
图2:关系说明图,展示自定义 UnmarshalJSON 内部的字段存在、null 判断与值解析边界;这是静态示意,不是运行截图。
type OptionalString struct {
	// Set 表示字段是否在 JSON 中出现,Null 表示出现后是否为 null。
	Set   bool
	Null  bool
	Value string
}

func (o *OptionalString) UnmarshalJSON(data []byte) error {
	// 进入自定义解码器就说明字段已经出现,先记录这一事实。
	o.Set = true
	o.Null = bytes.Equal(bytes.TrimSpace(data), []byte("null"))
	if o.Null {
		// 显式 null 不再尝试解码成字符串,清空旧值避免残留。
		o.Value = ""
		return nil
	}
	// 具体值沿用 encoding/json 的类型检查和转义处理。
	return json.Unmarshal(data, &o.Value)
}

type PatchUserTriState struct {
	// 缺失时不会调用 OptionalString.UnmarshalJSON,Set 会保持 false。
	Remark OptionalString `json:"remark"`
}

func applyTriState(dst *string, patch PatchUserTriState) {
	if !patch.Remark.Set {
		// 字段缺失:保持原值,不执行更新。
		return
	}
	if patch.Remark.Null {
		// 显式 null:执行接口约定的清空动作。
		*dst = ""
		return
	}
	// 有效值:写入已经由解码器检查过类型的字符串。
	*dst = patch.Remark.Value
}

这种类型的关键不在字段名字,而在“缺失时不会调用字段解码方法”这一点:只有键出现时,UnmarshalJSON 才会收到对应字节。若结构体对象本身会被复用,解码前应创建新值或显式重置状态,避免上一次请求留下的 Set 和 Value 影响下一次判断。

按接口场景选择承载方式

需求推荐方式注意点
只区分有值与无值指针字段缺失和 null 通常都会落到 nil
动态字段或少量临时判断map[string]json.RawMessage通过 map 键存在性保留缺失信号
固定结构需要三态自定义 UnmarshalJSON把 Set、Null 和 Value 封装起来
部分更新要清空或保留三态类型 + 业务更新函数不要在解码层直接写数据库

写测试时至少覆盖三份 JSON:{}、{"remark":null}、{"remark":"周末联系"}。再增加一个类型错误样例,确认错误不会被吞成空字符串。测试的重点不是输出一模一样,而是三个输入能触发三个明确的业务分支。

相关问题

为什么普通 string 字段分不出缺失和 null? 因为两者都可能让字段保持空字符串这个零值,类型本身没有保存字段是否出现的信号。

指针字段能直接实现 PATCH 的三态更新吗? 不能。指针可以表达非 nil 值,但缺失与显式 null 仍可能都对应 nil,需要额外的存在标记或自定义类型。

自定义 UnmarshalJSON 是否会影响字段缺失? 字段缺失时该字段的解码方法通常不会被调用,所以可以用 Set 的初始值 false 表示缺失;复用对象时要注意清理旧状态。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
os.MkdirTemp 目录清理遗漏的测试修复方法os.MkdirTemp 目录清理遗漏的测试修复方法
上一篇
os.MkdirTemp 目录清理遗漏的测试修复方法
unsafe 与 checkptr 一起使用时如何定位越界转换
下一篇
unsafe 与 checkptr 一起使用时如何定位越界转换
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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模型性能。
    1010次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    1014次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    1139次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    1111次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    955次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码