Spring Boot升级后@JsonProperty(READ_ONLY)字段反序列化值为null
Jackson在2.x版本(对应Spring Boot 2.x)中,@JsonProperty(access = JsonProperty.Access.READ_ONLY)的语义是:序列化时输出该字段,但反序列化时完全跳过对该字段的赋值。这和Spring Boot 1.5.x使用的Jackson旧版本(2.8及以下)行为不同——旧版本中READ_ONLY仅限制客户端通过setter写入,允许通过反射直接给字段赋值。
所以Service1返回的JSON里存在dateCreated、lastUpdated的有效值,但Service2用Jackson反序列化WorkOrder DTO时,这些字段因READ_ONLY标记被跳过,最终为null;而Postman看到的是序列化后的JSON,因此值显示正常。
方案1:拆分输入/输出DTO(架构层面最清晰的方案)
分别定义三类DTO,明确区分不同场景的字段范围:
- WorkOrderCreateRequest:客户端输入专用,仅包含允许修改的字段,不含dateCreated、lastUpdated等只读字段;
- WorkOrderResponse:服务输出专用,包含所有字段,用于给客户端和内部服务返回数据;
- 内部调用直接复用
WorkOrderResponse即可。
示例代码:
// 客户端输入DTO class WorkOrderCreateRequest { String orderNumber String status // 其他可写业务字段 } // 输出/内部调用DTO class WorkOrderResponse { String orderNumber String status Date dateCreated Date lastUpdated // 所有业务字段 }
通过DTO拆分天然实现客户端写入限制,彻底避免注解冲突问题。
方案2:启用Jackson的ALLOW_READ_ONLY_PROPERTIES特性(快速修复)
在Service2的ObjectMapper中启用DeserializationFeature.ALLOW_READ_ONLY_PROPERTIES,让Jackson允许反序列化标记为READ_ONLY的字段。
如果使用RestTemplate调用Service1,可自定义RestTemplate配置:
@Configuration class RestTemplateConfig { @Bean RestTemplate internalRestTemplate() { RestTemplate restTemplate = new RestTemplate() MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter() ObjectMapper objectMapper = new ObjectMapper() // 启用允许反序列化READ_ONLY字段的特性 objectMapper.enable(DeserializationFeature.ALLOW_READ_ONLY_PROPERTIES) converter.setObjectMapper(objectMapper) // 替换默认的Jackson转换器 restTemplate.getMessageConverters().removeIf { it instanceof MappingJackson2HttpMessageConverter } restTemplate.getMessageConverters().add(converter) return restTemplate } }
Service2调用时使用这个internalRestTemplate,即可正常反序列化READ_ONLY字段。
方案3:用@JsonIgnore标记setter(兼顾业务限制与反序列化)
保留字段的getter用于序列化输出,在setter上标记@JsonIgnore,这样客户端无法通过JSON写入该字段,但Jackson默认允许通过反射直接给private字段赋值,服务间调用时能正常获取值。
示例DTO:
class WorkOrder { private String orderNumber private String status private Date dateCreated private Date lastUpdated // 可写字段的getter和setter String getOrderNumber() { orderNumber } void setOrderNumber(String orderNumber) { this.orderNumber = orderNumber } String getStatus() { status } void setStatus(String status) { this.status = status } // 只读字段的getter,setter加@JsonIgnore限制写入 Date getDateCreated() { dateCreated } @JsonIgnore void setDateCreated(Date dateCreated) { this.dateCreated = dateCreated } Date getLastUpdated() { lastUpdated } @JsonIgnore void setLastUpdated(Date lastUpdated) { this.lastUpdated = lastUpdated } }
这种方式既满足了客户端禁止写入的业务要求,又能让服务间调用的反序列化正常赋值。
内容的提问来源于stack exchange,提问作者Prashant Sahi

