使用@JsonUnwrapped注解的Java record反序列化时出现Can not set final错误
问题场景
我定义了一个包含传统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

