You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Java和Jackson优雅反序列化JSON Schema?

解决方案:用Jackson多态反序列化处理JSON Schema的动态Properties

环境

Java 19, Jackson-databind 2.14.1, com.networknt.json-schema-validator 1.0.78

问题背景

给定符合JSON Schema规范的结构(properties是键值对对象而非数组),希望用Jackson的多态特性替代粗糙的@JsonAnySetter代码,自动将不同类型的属性反序列化为对应的POJO实现类,同时保留属性的键名(如host、construction_year)。

原JSON Schema:

{
  "$schema": "http://json-schema.org/draft/2020-12/schema#",
  "title": "Starbase Register",
  "description": "A set of records that describe a starbase",
  "type": "object",
  "properties": {
    "host": {
      "type": "string",
      "title": "Host Star System",
      "description": "This is the star system that this starbase resides in"
    },
    "construction_year": {
      "type": "integer",
      "title": "Year of Construction",
      "description": "This is the year that the starbase was first constructed",
      "minimum": 2200
    }
  },
  "required": [ "host" ]
}

原POJO建模存在的问题:SchemaProperties中用List<RegisterType>无法匹配JSON里的键值对结构,导致反序列化后fields为空;手动的@JsonAnySetter代码扩展性差。

简洁实现方案

1. 修正核心POJO结构

调整RegisterSchema

移除错误的@JsonValue注解(一个类不能有多个@JsonValue):

public class RegisterSchema {
    private String title;
    private String description;
    private SchemaProperties properties;

    // Getters and Setters
}

重构SchemaProperties

把List<RegisterType>改为Map<String, RegisterType>,直接匹配JSON的键值对结构,并在Map上配置多态反序列化规则:

public class SchemaProperties {
    @JsonTypeInfo(
        use = JsonTypeInfo.Id.NAME,
        include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
        property = "type"
    )
    @JsonSubTypes({
        @JsonSubTypes.Type(value = RegisterTextType.class, name = "string"),
        @JsonSubTypes.Type(value = RegisterIntegerType.class, name = "integer")
    })
    private Map<String, RegisterType> fields = new HashMap<>();

    // Getters and Setters
}

这里用include = JsonTypeInfo.As.EXTERNAL_PROPERTY是因为type字段是每个属性对象内部的字段,而Map的键是属性名(如host),需要告诉Jackson从每个Map值的内部读取type来确定具体类型。

完善RegisterType接口及实现类

给接口添加setName方法,用于接收Map的键:

public interface RegisterType {
    String getName();
    void setName(String name);
    String getType();
    String getTitle();
    String getDescription();
}

实现类示例(RegisterTextType):

public class RegisterTextType implements RegisterType {
    private String name;
    private String type;
    private String title;
    private String description;

    // 所有字段的Getters和Setters
}

RegisterIntegerType示例:

public class RegisterIntegerType implements RegisterType {
    private String name;
    private String type;
    private String title;
    private String description;
    private Integer minimum;
    private Integer maximum;

    // 所有字段的Getters和Setters
}

2. 反序列化时注入属性名

因为Jackson默认不会把Map的键传递给RegisterType的name字段,需要添加一个自定义的BeanDeserializerModifier来实现这个逻辑:

import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.node.ObjectNode;

import java.io.IOException;

public class RegisterTypeDeserializerModifier extends BeanDeserializerModifier {
    @Override
    public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
        if (RegisterType.class.isAssignableFrom(beanDesc.getBeanClass())) {
            return new StdDeserializer<RegisterType>(beanDesc.getBeanClass()) {
                @Override
                public RegisterType deserialize(com.fasterxml.jackson.databind.JsonParser p, com.fasterxml.jackson.databind.DeserializationContext ctxt) throws IOException {
                    ObjectNode node = p.readValueAsTree();
                    // 获取当前Map的键(属性名)
                    String fieldName = p.getCurrentName();
                    // 用原反序列化器创建实例
                    RegisterType type = (RegisterType) deserializer.deserialize(p, ctxt);
                    // 设置name字段
                    type.setName(fieldName);
                    return type;
                }
            };
        }
        return super.modifyDeserializer(config, beanDesc, deserializer);
    }
}

3. 配置Jackson ObjectMapper

注册自定义的BeanDeserializerModifier:

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;

public class JacksonConfig {
    public static ObjectMapper getConfiguredMapper() {
        ObjectMapper mapper = new ObjectMapper();
        SimpleModule module = new SimpleModule();
        module.setDeserializerModifier(new RegisterTypeDeserializerModifier());
        mapper.registerModule(module);
        return mapper;
    }
}

4. 使用示例

String jsonSchema = "..."; // 你的JSON Schema字符串
ObjectMapper mapper = JacksonConfig.getConfiguredMapper();
RegisterSchema schema = mapper.readValue(jsonSchema, RegisterSchema.class);

// 遍历所有字段
schema.getProperties().getFields().forEach((name, type) -> {
    System.out.println("字段名: " + name);
    System.out.println("类型: " + type.getType());
    System.out.println("标题: " + type.getTitle());
    if (type instanceof RegisterIntegerType) {
        System.out.println("最小值: " + ((RegisterIntegerType) type).getMinimum());
    }
});

关键说明

  • 用Map<String, RegisterType>替代List,完美匹配JSON Schema的properties键值对结构,解决了原fields为空的问题。
  • 通过@JsonTypeInfo的EXTERNAL_PROPERTY模式,让Jackson从每个属性对象的type字段自动选择对应的实现类,无需手动判断。
  • 自定义BeanDeserializerModifier自动将Map的键注入到RegisterType的name字段,避免了手动处理键名的繁琐。

内容的提问来源于stack exchange,提问作者James

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 17:42:47