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

Go json.Unmarshal 为什么不拒绝缺少必填字段

来源:17golang原创

时间:2026-10-26 19:51:19 247浏览 收藏

Go json.Unmarshal 负责把 JSON 数据解码到 Go 值,不负责判断业务字段是否必填。JSON 对象少了某个结构体字段,通常不会因此报错;新建目标中的字段仍是零值。json 标签用于字段映射,omitempty 影响编码时是否省略字段,都不是解码时的必填规则。

可靠的处理方式是把输入对象与业务对象分开:先解码,再检查字段是否提供、是否允许 null、数值范围及字符串内容,最后才创建可使用的业务值。下面以创建导出任务为例,name 必须有非空内容,limit 必须明确提供且在 1 到 500 之间,enabled 必须明确提供,但 false 是合法值。

Go 官方地址:https://go.dev/

目标与边界:先定义什么叫“必填”

“不能为空”至少有两种含义:键必须出现,或者值必须有业务意义。对 enabled 来说,false 是明确选择,不能用 if !enabled 判断缺失;对 name 来说,提供了空字符串或全空格仍不合格。对 limit 来说,即便键出现,零也不在本例允许范围内。

输入情况本例策略原因
name 缺失或 null拒绝没有可使用名称
name 是空白拒绝业务内容无效
enabled 为 false接受合法布尔选择
limit 为 0拒绝超出约定范围

完整处理链:解码、校验、转换各有职责

输入结构体 CreateInput 使用指针字段,让新建对象中的 nil 表达“未提供可用值”。解码 null 也会得到 nil,因此这套方案把缺失与 null 一起拒绝,能够区分它们和合法的 false。通过校验以后,转换成不含这些输入指针的 ExportJob,让后续代码不必重复判断 nil。

输入字段与指针状态及业务对象的关系结构图
图1:输入契约结构图。CreateInput 的三个指针字段承载输入状态,ExportJob 是校验后使用的业务对象;图为结构说明,不是执行流程或运行结果。

阶段拆解:每次解码使用新的输入对象

package main

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

type CreateInput struct {
    Name    *string `json:"name"`
    Limit   *int    `json:"limit"`
    Enabled *bool   `json:"enabled"`
}

type ExportJob struct {
    Name    string
    Limit   int
    Enabled bool
}

func parseCreate(data []byte) (ExportJob, error) {
    var in CreateInput // 每次请求新建,避免旧字段值掩盖本次缺失。
    if err := json.Unmarshal(data, &in); err != nil {
        return ExportJob{}, fmt.Errorf("JSON 解码失败: %w", err)
    }
    if in.Name == nil {
        return ExportJob{}, fmt.Errorf("name 必须提供且不能为 null")
    }
    name := strings.TrimSpace(*in.Name)
    if name == "" {
        return ExportJob{}, fmt.Errorf("name 不能是空白")
    }
    if in.Limit == nil {
        return ExportJob{}, fmt.Errorf("limit 必须提供且不能为 null")
    }
    if *in.Limit  500 {
        return ExportJob{}, fmt.Errorf("limit 必须在 1 到 500 之间")
    }
    if in.Enabled == nil {
        return ExportJob{}, fmt.Errorf("enabled 必须提供且不能为 null")
    }
    // 解码和校验成功后,才构造业务对象。
    return ExportJob{Name: name, Limit: *in.Limit, Enabled: *in.Enabled}, nil
}

func main() {
    samples := []string{
        `{}`,
        `{"name":"周报","limit":30,"enabled":false}`,
        `{"name":"周报","limit":30,"enabled":null}`,
        `{"name":"  ","limit":30,"enabled":true}`,
    }
    for _, raw := range samples {
        job, err := parseCreate([]byte(raw))
        fmt.Printf("input=%s job=%+v err=%v\n", raw, job, err)
    }
}

这是完整原创示例,可保存为 main.go 自行运行。按上述规则,只有第二个样本成功,enabled 保留 false;其余分别因缺失名称、空的 enabled 或空白名称失败。这里描述的是代码预期,没有伪造本地测试日志。

校验顺序还决定一次返回哪个错误。示例按字段顺序返回第一项,便于说明;表单需要一次展示全部问题时,可以收集字段级错误,但仍应在解码失败后停止转换,避免把部分解码结果当成完整输入。

推荐流程:明确区分缺失与 null 时再增加状态

指针并不区分全部输入状态。在新建 CreateInput 中,缺失和显式 null 都是 nil;如果更新接口要求“缺失表示不修改,null 表示清空”,就需要单独记录键是否存在。可以先解码成 map[string]json.RawMessage,用键查询的 ok 判断存在,用原始值判断 null,再解码成字段类型。

func readNameState(data []byte) (present, isNull bool, value string, err error) {
    var fields map[string]json.RawMessage
    if err = json.Unmarshal(data, &fields); err != nil {
        return false, false, "", err
    }
    if fields == nil {
        return false, false, "", fmt.Errorf("请求必须是 JSON 对象")
    }
    raw, ok := fields["name"] // 键查询表达是否出现,不能仅看零值。
    if !ok {
        return false, false, "", nil
    }
    if strings.TrimSpace(string(raw)) == "null" {
        return true, true, "", nil
    }
    // 不是 null 时,再要求它是字符串。
    if err = json.Unmarshal(raw, &value); err != nil {
        return true, false, "", fmt.Errorf("name 类型错误: %w", err)
    }
    return true, false, value, nil
}

该函数片段使用上例已经导入的 json、fmt、strings。它只演示 name 的三态,不是完整更新协议。基于 map 的精确键查询要求使用 name 这个大小写;与结构体解码可能接受大小写不敏感匹配的行为不同,接口应统一约定,避免两层规则互相矛盾。

RawMessage 键存在状态与 null 和字段值的结构关系
图2:字段存在状态结构图。fields 的键查询区分缺失与出现,原始值可进一步区分 null 和具体字段值;这是一张静态关系图。

常见误区:严格解码并不自动补上业务规则

DisallowUnknownFields 用于拒绝对象里无法匹配目标结构体字段的键,解决的是“多了陌生字段”,不是“少了必要字段”。同理,给结构体写 validate:"required" 标签,不会让标准库自行执行第三方验证逻辑;采用某个验证库时,要明确调用它并确认零值语义。

不要复用已装有旧数据的输入对象处理下一次完整创建请求。缺失字段可能留下此前的值,使“必填”检查出现假通过。新建对象是本例的一部分;对更新接口有意保留旧值时,也应通过清楚的合并逻辑完成,而不是隐式依赖复用对象。

若改用 Decoder,单次 Decode 可以只读一个 JSON 值。只允许一个对象的接口还应拒绝尾部第二个值,并限制请求体大小。不要为了额外选项从 Unmarshal 换成 Decoder 后,忘记输入边界发生变化。重复键、大小写规则和数字类型也需要按具体协议另行约束,本文的必填校验不等于完整 JSON Schema 实现。

速查:根据字段语义选表示方式

需求表示方式仍需处理
缺失与零值都无效普通值加业务校验也可范围、内容规则
false 或 0 合法,但必须提供新建输入对象的指针字段null 是否允许
缺失、null、具体值各有含义存在标记或 RawMessage键名与类型约定
后续逻辑只想使用合格数据校验后转换业务对象错误返回与状态一致性

“必填”应当写成可读的接口契约。解码成功后明确做校验,再让数据进入存储或任务队列,能够避免把传输层成功误认为业务操作成功。

相关问题

omitempty 能要求请求字段出现吗?

不能。它用于编码省略规则,不是解码时的必填约束。

bool 用指针后,false 会不会仍被当成没填?

只检查指针是否 nil 就不会。false 对应非 nil 指针,取值后仍然是 false。

缺失字段会自动覆盖成零值吗?

不要这样理解。本文每次新建输入对象,所以初始零值自然保留;对已有对象解码时,未出现的结构体字段可能继续保留原值。

参考:json.Unmarshal、DisallowUnknownFields、RawMessage。

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