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

Java Jackson record 缺少 JSON 字段时如何设默认

来源:17golang原创

时间:2026-09-15 14:32:06 278浏览 收藏

我在把接口请求对象从普通 Java Bean 改成 record 时,最容易误判的一点就是:少传一个 JSON 字段,并不会触发 record 自己的“字段默认值”。Jackson 仍然会调用 record 的 canonical constructor,缺少的引用类型参数通常以 null 进入构造过程,随后是否变成业务默认值,要由构造器明确决定。

要点速览
  • record 没有无参构造器,默认值要放进 canonical 或紧凑构造器。
  • 缺少字段与显式 null 在简单 record 方案里通常会汇合,不能假装已经区分。
  • 集合默认值要同时处理空值和防御性拷贝,再用四组输入做回归。

一、先判断缺失字段会传入什么值

Java record 的组件是构造器参数和最终状态的一部分。下面这个类型没有给 timeoutSeconds 声明类似 Bean 字段初始化的机会:

// record 的组件会进入 canonical constructor,不能依赖无参构造器补值
public record RequestOptions(String traceId, Integer timeoutSeconds) {
}

当 JSON 只有 {"traceId":"a-17"} 时,Jackson 会按属性名找到 traceId,而 timeoutSeconds 没有输入。对引用类型来说,构造器参数通常是 null;如果组件是 int,则要面对基本类型默认值 0。显式传入 null 也会落到相同的引用类型参数上。

Java Jackson record 缺失 JSON 字段进入 canonical constructor 的静态结构说明图
图1:结构说明图,查看 JSON 属性、Jackson 属性绑定、canonical constructor 与 record 组件之间的静态关系;这不是运行截图。

所以排查时先不要把问题归咎于 Jackson “没有读取默认值”。record 本身没有隐式无参构造器,默认值必须出现在构造入口,或者在更早的输入模型层完成。

二、在 record 紧凑构造器中集中设默认

只要业务规则是“字段缺失和 null 都按同一个默认值处理”,紧凑构造器是最小改动方案。它保留 record 的声明形状,同时允许在组件真正赋值前归一化参数:

import java.util.List;

// 统一处理默认值,并避免把可变列表直接暴露给调用方
public record RequestOptions(
        String traceId,
        Integer timeoutSeconds,
        List scopes) {

    public RequestOptions {
        // 缺少 traceId 时给出可观察的占位值,生产项目也可改成直接拒绝
        traceId = traceId == null ? "anonymous" : traceId;
        // 引用类型用 null 判断,避免把 0 和“没有提交”混在一起
        timeoutSeconds = timeoutSeconds == null ? 30 : timeoutSeconds;
        // 空值归一化后再拷贝,保证 record 内部列表不会被外部修改
        scopes = scopes == null ? List.of() : List.copyOf(scopes);
    }
}

这里的默认策略有三个特征:Integernull 表示未提供,30 才是业务默认;scopes 用空列表表达“没有范围”,并通过 List.copyOf 保持不可变;traceId 则用明确的占位值,方便日志关联。默认值应该表达业务语义,不要为了省一行代码把所有引用类型都静默改成空字符串。

Java record 紧凑构造器对 Jackson 参数做默认值归一化和集合拷贝的静态结构说明图
图2:结构说明图,查看紧凑构造器、null 归一化、默认超时、空列表和不可变 record 组件的静态关系;这不是运行截图。

三、根据业务需要处理显式 null

如果接口契约要求“缺少字段使用默认值,但显式 null 必须报错”,仅写紧凑构造器还不够,因为构造器通常只看到一个 null。这时有两种稳妥方向:

  • 把入参先绑定到能表达存在性的 DTO、JsonNode 或带 presence 标记的中间对象,再转换成最终 record。
  • 为该 record 提供自定义反序列化器,在 JSON 令牌层判断属性是否出现,再决定传默认值、传 null 还是抛出异常。

@JsonSetter(nulls = Nulls.SKIP) 更适合有可写属性和既有初始化值的 Bean 场景,不应直接当成 record 缺失字段的通用开关。record 的核心状态由 canonical constructor 一次建立,先明确“缺失”和“null”是否等价,再选择模型。

四、用最小回归表验证序列化边界

我会把下面四组输入固定成参数化测试,重点不是测试 Jackson 会不会解析 JSON,而是验证默认规则有没有把接口契约说清:

输入预期 timeoutSeconds预期 scopes检查点
字段缺失30空列表默认生效
显式 null30 或拒绝空列表或拒绝与契约一致
空数组30空列表不是 null
正常值请求值请求列表值未被覆盖
// 这段断言示意默认策略;测试项目中应使用自己的断言库
RequestOptions options = mapper.readValue("{\"traceId\":\"a-17\"}", RequestOptions.class);
// 缺失 timeoutSeconds 和 scopes 时,构造器负责给出稳定结果
if (options.timeoutSeconds() != 30 || !options.scopes().isEmpty()) {
    throw new AssertionError("record 默认值不符合接口契约");
}

还要单独检查一个容易遗漏的边界:如果组件使用 int 而不是 Integer,缺失输入可能以 0 进入构造器;这不等于“采用了 30 秒默认值”。涉及超时、分页大小、重试次数时,我更愿意使用包装类型接住“未提供”,然后在构造器里显式归一化。

常见问题

record 能不能像 Bean 一样给组件直接写初始值?

不能用普通实例字段初始化来替代组件默认值。应在 canonical 或紧凑构造器中规范化参数,或者在反序列化前的输入模型中处理。

缺少字段和显式 null 一定相同吗?

在只接收构造器参数的简单 record 方案里通常会汇合,但业务若要求区分,就必须保留属性存在性信息,使用 DTO、JsonNode 或自定义反序列化。

为什么默认集合还要 List.copyOf?

空列表只解决 null 语义,List.copyOf 还解决外部可变列表被后续修改的问题,让 record 的不可变边界更可靠。

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