You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用@JsonUnwrapped注解的Java record反序列化时出现Can not set final错误

Java Record 配合 @JsonUnwrapped 反序列化失败的解决办法

问题场景

我定义了一个包含传统POJO(Address类)的Java Record,使用@JsonUnwrapped注解实现嵌套对象的扁平化序列化:

public record TestRecord(String name, @JsonUnwrapped Address address) {}

序列化过程完全正常,能生成预期的扁平化JSON:

Address address = new Address();
address.setCity("New York");
address.setState("NY");
TestRecord rec = new TestRecord("Bob", address);

JsonMapper jsonMapper = new JsonMapper();
jsonMapper.writeValueAsString(rec); // 输出: {"name":"Bob","city":"New York","state":"NY"}

但反序列化时直接抛出IllegalAccessException,提示无法给Record的final字段address赋值:

@Test
void should_DeserializeUnwrappedRecord() throws JsonProcessingException {
    Address address = new Address();
    address.setCity("New York");
    address.setState("NY");
    TestRecord expectedRecord = new TestRecord("Bob", address);

    String json = """
            {"name":"Bob","city":"New York","state":"NY"}
            """;

    JsonMapper jsonMapper = new JsonMapper();
    TestRecord actualValue = jsonMapper.readValue(json, TestRecord.class);
    assertEquals(expectedRecord, actualValue);
}

完整异常堆栈:

com.fasterxml.jackson.databind.JsonMappingException: Can not set final org.example.AppTest$Address field org.example.AppTest$TestRecord.address to org.example.AppTest$Address
 at [Source: UNKNOWN; byte offset: #UNKNOWN]

    at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:276)
Caused by: java.lang.IllegalAccessException: Can not set final org.example.AppTest$Address field org.example.AppTest$TestRecord.address to org.example.AppTest$Address
    at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:76)
    at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:80)
    at java.base/jdk.internal.reflect.UnsafeQualifiedObjectFieldAccessorImpl.set(UnsafeQualifiedObjectFieldAccessorImpl.java:79)
    at java.base/java.lang.reflect.Field.set(Field.java:799)

将TestRecord改为普通Java类时,反序列化恢复正常。

原因分析

Java Record的所有字段默认是final修饰的,Jackson处理@JsonUnwrapped注解时,对于普通类可以通过setter方法或直接赋值字段完成反序列化,但对于Record,正确的实例化方式应该是通过其自动生成的构造器。旧版本Jackson(或默认配置下)处理Record的@JsonUnwrapped时,错误地尝试直接修改final字段,触发了Java的访问权限限制,导致异常。

解决办法

1. 升级Jackson版本

Jackson Databind从2.12版本开始修复了Java Record与@JsonUnwrapped的兼容问题,会自动通过Record的构造器完成反序列化,无需额外配置。直接将jackson-databind依赖升级到2.12及以上版本即可解决问题。

2. 强制Jackson使用构造器反序列化

如果无法升级版本,可以显式配置JsonMapper,强制其使用构造器实例化Record:

JsonMapper jsonMapper = JsonMapper.builder()
        .enable(MapperFeature.USE_CONSTRUCTOR_PROPERTIES)
        .build();

也可以给Record的构造器添加@JsonCreator注解,明确参数与JSON字段的映射关系(Record默认构造器已被Jackson识别,此步骤可选,但能进一步确保兼容性):

public record TestRecord(String name, @JsonUnwrapped Address address) {
    @JsonCreator
    public TestRecord(
            @JsonProperty("name") String name,
            @JsonUnwrapped Address address) {
        this.name = name;
        this.address = address;
    }
}

3. 自定义反序列化器(兜底方案)

如果以上方法都不可行,可以编写自定义反序列化器,手动解析JSON字段并组装Record实例:

public class TestRecordDeserializer extends StdDeserializer<TestRecord> {
    protected TestRecordDeserializer() {
        super(TestRecord.class);
    }

    @Override
    public TestRecord deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        JsonNode node = p.getCodec().readTree(p);
        String name = node.get("name").asText();
        Address address = new Address();
        address.setCity(node.get("city").asText());
        address.setState(node.get("state").asText());
        return new TestRecord(name, address);
    }
}

注册该反序列化器到JsonMapper:

JsonMapper jsonMapper = JsonMapper.builder()
        .addDeserializer(TestRecord.class, new TestRecordDeserializer())
        .build();

内容的提问来源于stack exchange,提问作者nogridbag

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 02:50:53