如何用Java和Jackson优雅反序列化JSON Schema?
环境
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

