Jackson序列化相同关联对象仅返回ID问题解决方案
这个序列化异常是@JsonIdentityInfo注解的默认行为导致的:
该注解设计目的是解决JSON序列化时的循环引用死循环问题,核心逻辑是:单次序列化流程中,同一个Java对象实例第一次出现时会输出完整属性,后续再次被引用时仅输出预先配置的标识属性(你的配置中为id字段)。
当passPayer和driver指向同一个Driver实例时,序列化器优先处理passPayer字段输出了完整Driver对象,后续处理driver字段时识别到这是已经序列化过的同一实例,就直接返回了id值,最终出现两个字段返回格式不一致的问题。
按生产环境推荐优先级从高到低排列:
方案1:使用DTO替代直接返回JPA实体(最优实践)
不要直接将JPA实体类作为接口返回值,单独定义专用的返回DTO(数据传输对象),彻底脱离JPA和Jackson的实体序列化约束:
- 定义
DriverDTO,固定保留id、firstName、lastName、idNo四个需要返回的字段 - 定义
HireDTO,其中passPayer、driver字段均使用DriverDTO类型,其余字段按接口返回需求定义 - 在业务层将查询到的Hire实体通过BeanUtils、MapStruct等工具转换为HireDTO后返回
该方案可以一次性规避JPA懒加载异常、循环引用、敏感字段泄露、序列化规则不一致等所有实体直返带来的共性问题,是工业界生产环境的标准做法。
示例DTO结构:
@Data public class DriverDTO { private int id; private String firstName; private String lastName; private String idNo; } @Data public class HireDTO { private int id; private DriverDTO passPayer; private DriverDTO driver; private BigDecimal passCost; private BigDecimal driverSalary; // 其余接口需要返回的字段 }
方案2:移除@JsonIdentityInfo,换用轻量循环阻断注解
如果业务场景没有复杂的多层双向循环引用,可以直接删除Hire类(以及Driver类,如果类上也添加了该注解)上的@JsonIdentityInfo注解,Jackson就不会对重复出现的对象做id简写处理,两个字段会正常输出完整对象属性。
如果确实存在双向关联需要避免序列化栈溢出,可以在关联字段上使用@JsonIgnoreProperties注解阻断循环,示例:
// 假设Driver类中存在关联回Hire的一对多字段 @OneToMany(mappedBy = "driver") @JsonIgnoreProperties("driver") // 序列化时忽略关联对象中的driver字段,切断循环 private List<Hire> hires;
方案3:为指定字段自定义全量序列化器
如果必须保留@JsonIdentityInfo解决其他场景的循环引用问题,可以针对passPayer和driver两个字段单独配置自定义序列化器,强制每次都输出完整对象属性:
- 编写Driver的全属性序列化器
public class DriverFullSerializer extends StdSerializer<Driver> { public DriverFullSerializer() { super(Driver.class); } @Override public void serialize(Driver value, JsonGenerator gen, SerializerProvider provider) throws IOException { gen.writeStartObject(); gen.writeNumberField("id", value.getId()); gen.writeStringField("firstName", value.getFirstName()); gen.writeStringField("lastName", value.getLastName()); gen.writeStringField("idNo", value.getIdNo()); gen.writeEndObject(); } }
- 在Hire类的两个关联字段上指定使用该序列化器
@OneToOne( targetEntity = Driver.class, fetch = FetchType.EAGER ) @JoinColumn( name = "pass_payer", referencedColumnName = "id", nullable = true ) @JsonSerialize(using = DriverFullSerializer.class) Driver passPayer; @OneToOne( targetEntity = Driver.class, fetch = FetchType.EAGER ) @JoinColumn( name = "driver_id", referencedColumnName = "id", nullable = true ) @JsonSerialize(using = DriverFullSerializer.class) Driver driver;
该方案不需要改动现有实体的核心注解逻辑,但是可维护性较差,后续Driver类字段变更需要同步修改序列化器代码。
不推荐通过配置Jackson全局关闭对象引用检测逻辑,会导致其他存在循环引用的接口出现序列化栈溢出问题。
内容的提问来源于stack exchange,提问作者Nipuna Perera

