试试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 核心思路

我们依然在实体中保留一个 Map<String, Object> dynamicProps 用于存储所有扩展属性。

然后通过两个注解告诉 Jackson:

  • 反序列化时:遇到实体中没有定义的属性,统统丢进这个 Map(@JsonAnySetter)。

  • 序列化时:把这个 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 Map<String, 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 Map<String, 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 和实体里已有的普通字段同名(比如 id),Jackson会覆盖普通属性。这点要特别注意。

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 字段。

  • 用:通过注解动态塞入/取出。

  • 展:序列化时自动平铺,前端无感知。

从此告别"加一个扩展属性就要改一堆代码"的噩梦。

相关推荐
limanovember15 小时前
yq:命令行下的 YAML 和 JSON 处理利器
其他·json
程序员大阳2 天前
副队长JavaScript教程(65)-详解JSON:轻量级数据交换格式
javascript·json·序列化·反序列化·stringify·parse
光影6272 天前
Unittest单元测试基础速记
测试工具·单元测试·json·测试覆盖率
ZLongTeng3 天前
EIDE的launch.json文件
json
梦想不只是梦与想3 天前
Python 中的 json 模块
python·json·对象转字符串
步行cgn3 天前
JSON.stringify 详解:把 JavaScript 对象变成 JSON 字符串
开发语言·javascript·json
阿正的梦工坊3 天前
深入理解 JSON-RPC 2.0:轻量级远程调用协议详解
rpc·json
腾讯数据架构师3 天前
任务跑完怎么直接出图表?CubeStudio 任务可视化 ECharts 数据格式实操(CSV/JSON + metric.json,11 种图表一键渲染)
前端·json·echarts·数据可视化
救救孩子把3 天前
结构化输出为何“格式正确、答案错误”?JSON Schema 的能力边界
android·java·json·agent·agent服务