用快递打包的思路,搞定 Fastjson2 序列化未知属性!

506 阅读4分钟

在 JSON 这条物流线上,普通对象就像标准快递盒,按照规则打包和拆箱。但有些场景,我们需要 更自由、更聪明地打包和拆开。前两天写过一篇文章《Java 中 JSON 字段不固定怎么搞序列化?用好这两个注解就够了!》,这个操作是jackson中的注解,有些开发的项目并不使用Jackson,使用的是fastjson,今天我们聊聊 fastjson2 特供版的打包方法。

快递小哥的 JSON 魔法技能(Fastjson 特供版序列化)

1、@JSONField(unwrapped = true):把内部小件直接摊开打包

正常打包时,一个对象里面还有一个子对象,子对象会单独套一层盒子,比如:

{
  "user": {
    "name": "Tom",
    "age": 18
  }
}

但如果你加上了 @JSONField(unwrapped = true),就好比告诉快递员:

“别单独套盒了!直接把里面的小件摊在大箱子里!”

最终打包效果变成:

{
  "name": "Tom",
  "age": 18
}

简单说就是展开内嵌对象的字段,减少一层结构,常用于简化 API 返回格式。

现在我们还是使用这个 Person 类,想要在其中存储一些动态的字段。

这里我们使用Map存放未知字段,示例代码如下所示

public class PersonMap {
    private String name;
    private int age;

    // 存储动态属性
    // 通过@JSONField(unwrapped=true)实现动态属性扁平化序列化
    @JSONField(unwrapped = true)
    private Map<String, Object> additionalProperties = new HashMap<>();

    public void addAdditionalProperty(String key, Object value) {
        this.additionalProperties.put(key, value);
    }

    // 省略标准getter/setter(Fastjson2会默认序列化)

    public static void main(String[] args) {
        PersonMap person = new PersonMap();
        person.setName("小豆1号");
        person.setAge(18);
        person.addAdditionalProperty("address", "123 Street");
        person.addAdditionalProperty("nickname", "dadou");
        // 使用Fastjson序列化
        String json = JSON.toJSONString(person3);
        // 输出:{"address":"123 Street","nickname":"dadou","age":18,"name":"xiaodou"}
        System.out.println(json);
    }
}

在这个例子中:

  • additionalProperties 是一个 Map,用来存储动态字段。
  • 我们通过 addAdditionalProperty() 方法向 additionalProperties 中添加动态字段。
  • 使用 JSON.toJSONString() 方法序列化时,additionalProperties 中的内容会作为额外的字段被序列化到 JSON 中。

就像最开始说的@JSONField(unwrapped = true)是展开属性,这里还可以处理对象,比如下面这种代码

public class PersonObject {
    private String name;
    private int age;

    @JSONField(unwrapped = true) 
    private XiaoDou xiaoDou;
}

这里会有一个问题,假设XiaoDouPersonObject拥有相同的属性,那这个属性会如何处理呢?会覆盖么?

完整的测试代码:点我打开


字段反序列化

@JSONType(deserializer = Class.class):指定专属开箱员

有些货物比较特别,普通快递小哥不会开(比如生鲜、艺术品)。这时候你需要给它指派专业开箱师傅

在代码里,就是给类打上:

@JSONType(deserializer = PersonDeserializer.class)

告诉 Fastjson:

“这个对象(Person4)不能普通拆,要找指定的 Person4Deserializer 来精细处理!”

适合复杂 JSON → Java 对象映射,比如字段名不一致、嵌套特别奇怪、需要自定义处理逻辑的场景。

在这里,我们可以通过自定义 PersonDeserializer 解析器来将不同的字段分别放入不同的 Map 中。具体做法是,检查每个动态字段的名称,根据名称或者其他规则来决定将它放到哪个 Map 中。示例代码:点我打开

@JSONType(deserializer = PersonDeserializer.class)
public class PersonDeserializerTest {
    private String name;
    private int age;
    private Map<String, Object> additionalProperties = new HashMap<>();

    // 这里省略标准getter/setter(Fastjson2会默认序列化)
    

    public static void main(String[] args) {
        String json = "{\"name\":\"xiaodou\",\"age\":18,\"address\":\"123 Street\",\"nickname\":\"xiaodou\"}";

        // 使用Fastjson反序列化
        PersonDeserializerTest person = JSON.parseObject(json, PersonDeserializerTest.class);

        System.out.println("Name: " + person.name);  // 输出:Name: John
        System.out.println("Age: " + person.age);    // 输出:Age: 30
        System.out.println("Additional Properties: " + person.getAdditionalProperties());
    }
}

// 自定义反序列化器
class PersonDeserializer implements ObjectReader<PersonDeserializerTest> {
    @Override
    public PersonDeserializerTest readObject(JSONReader jsonReader, Type fieldType, Object fieldName, long features) {
        PersonDeserializerTest person = new PersonDeserializerTest();
        if (jsonReader.nextIfObjectStart()) {
            while (!jsonReader.nextIfObjectEnd()) {
                String key = jsonReader.readFieldName();
                switch (key) {
                    case "name":
                        person.setName(jsonReader.readString());
                        break;
                    case "age":
                        person.setAge(jsonReader.readInt32());
                        break;
                    default:
                        person.addAdditionalProperty(key, jsonReader.readAny());
                }
            }
        }
        return person;
    }
}

在这个例子中:

  • Fastjson 会自动将 addressnickname 字段映射到 additionalProperties 中,而不需要显式声明这些字段。
  • JSON.parseObject() 方法会自动识别 JSON 中的额外字段并将它们放入 Map 中。

输出:

Name: xiaodou
Age: 18
Additional Properties: {address=123 Street, nickname=xiaodou}

这里也可以换一种调用方式,点我打开源码

public static void main(String[] args) {
    // 注册自定义Reader
    ObjectReaderProvider provider = JSONFactory.getDefaultObjectReaderProvider();
    provider.register(PersonDeserializerTest.class, new PersonDeserializer());

    String json = "{\"name\":\"xiaodou\",\"age\":18,\"address\":\"123 Street\",\"nickname\":\"xiaodou\"}";

    PersonDeserializerTest person = JSON.parseObject(json, PersonDeserializerTest.class);
    System.out.println("Name: " + person.getName());
    System.out.println("Age: " + person.getAge());
    System.out.println("Additional Properties: " + person.getAdditionalProperties());
}

又想到一个问题,Dog、Cat都是Animal的子类,当我们用 fastjson 把一堆 Animal 类型的数据序列化成 JSON 时,JSON里本身是没有类型信息的,所以你反序列化回来只知道是 Animal,不知道是 Dog 还是 Cat,这时候该怎么办呢?