试试Jackson:把JSON拉平成普通属性 一张表一个 ext 字段输出时却要“平铺”成普通 JSON 属性不用改实体、不用写死字段一个注解全搞定一、当前痛点在日常业务开发中我们经常会遇到这样一个场景数据库设计时预留一个 ext 或 extra 字段类型是 JSON 字符串用来存储一些可扩展的属性。比如用户表有个 ext里面可能放性别、地址、爱好等不确定的字段商品表也有个 ext里面可能放颜色、尺寸、产地……存的时候没问题直接把一个 Map 转成 JSON 字符串塞进去。但取出来给前端的时候前端希望得到的是平铺的 JSON 对象而不是嵌套的 { ext: {...} }。比如后端想要输出{ id: 1, age: 17, sex: 男, address: 山东省 }而不是{ id: 1, age: 17, ext: { sex: 男, address: 山东省 } }传统做法是什么在实体类里把 sex、address 全部定义成独立属性然后手动从 ext 里 get/set。问题是扩展属性随时可能增加难道每次加一个属性就要改一次实体、改一次 VO、改一次转换逻辑太痛苦了有没有一种方法不在实体里增加字段却能让扩展字段在序列化时像普通属性一样被平铺展示答案是有Jackson 的 JsonAnySetter 和 JsonAnyGetter 就是为此而生的。二、解决方案2.1 核心思路我们依然在实体中保留一个 MapString, Object dynamicProps 用于存储所有扩展属性。然后通过两个注解告诉 Jackson反序列化时遇到实体中没有定义的属性统统丢进这个 MapJsonAnySetter。序列化时把这个 Map 里的所有键值对直接“拉平”到 JSON 对象的顶层JsonAnyGetter。这样前端看到的 JSON 和实体里写死的普通属性一模一样而我们的实体类无需为扩展字段添加任何具体成员变量。2.2 代码实现import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; import java.util.HashMap; import java.util.Map; public class Student { private Long id; private Integer age; // 动态扩展属性容器 private MapString, Object dynamicProps new HashMap(); // --- 普通属性的 getter/setter --- public Long getId() { return id; } public void setId(Long id) { this.id id; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age age; } // --- 动态属性操作 --- JsonAnySetter public void setDynamicProp(String key, Object value) { dynamicProps.put(key, value); } JsonAnyGetter public MapString, Object getDynamicProps() { return dynamicProps; } // 便捷取值方法 SuppressWarnings(unchecked) public T T get(String key) { return (T) dynamicProps.get(key); } }2.3 测试效果RestController RequestMapping(/student) public class StudentController { // GET返回一个包含动态属性的 Student 对象 GetMapping(/info) public ResultObject getStudent() { Student stu new Student(); stu.setId(1L); stu.setAge(17); stu.setExt(name, 张三); stu.setExt(sex, 男); stu.setExt(address, 山东省); return Result.success(stu); } // 输出{code: 200, data: { id: 1, age: 17, name: 张三, sex: 男, address: 山东省 }} // POST接收前端 JSON自动将未知属性映射到 dynamicProps PostMapping(/save) public ResultObject saveStudent(RequestBody Student student) { String name student.get(name); String sex student.get(sex); String grade student.get(grade); // 假设前端还传了 grade return Result.success(name name , sex sex , grade grade); } // 输出{id: 2, age: 18, name: 李四, sex: 女, grade: 高三 } }完美没有任何额外实体字段扩展属性却被自然地平铺到了 JSON 中。三、进阶技巧与注意事项3.1 避免与普通字段冲突如果动态属性中的 key 和实体里已有的普通字段同名比如 idJackson会覆盖普通属性。这点要特别注意。3.2 处理 null 值Jackson 默认会忽略值为 null 的属性。如果你希望 null 的动态属性也被输出可以配置mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);3.3 反序列化时限制动态属性如果想只允许某些 key 进入动态 Map可以在 JsonAnySetter 方法中加入校验逻辑JsonAnySetter public void setDynamicProp(String key, Object value) { if (allowedKeys.contains(key)) { dynamicProps.put(key, value); } }3.4 与数据库 JSON 字段配合数据库中 ext 字段是 JSON 字符串从 DB 读取后可以用 Jackson 先反序列化成 Map再通过 JsonAnySetter 填充到实体中。也可以直接让 MyBatis 等 ORM 框架支持将 JSON 列映射为 Map 类型。四、总结对于需要预留扩展字段的业务场景使用 Jackson 的 JsonAnySetter / JsonAnyGetter 将 JSON 内容拉平到实体输出是一个非常优雅且实用的方案。存将扩展属性以 Map 形式存入 JSON 字段。用通过注解动态塞入/取出。展序列化时自动平铺前端无感知。从此告别“加一个扩展属性就要改一堆代码”的噩梦。