Jackson 2.9.7中JsonSubTypes未识别子类型优雅处理及大小写属性解析问题
让我们一步步解决你的两个问题,结合Jackson 2.9.7的特性来做优雅处理:
一、优雅处理未识别的子类型
首先要指出一个关键细节:你的@JsonTypeInfo注解里指定的property是"subClass",但传入的JSON里用的是"subType",这会导致Jackson无法正确读取类型标识值,这很可能是抛出InvalidTypeIdException的直接原因!先把字段名统一——要么把注解里的property改成"subType",要么调整JSON负载使用"subClass"。
假设字段名已经统一,接下来按以下步骤处理:
1. 确保UnmappedAnimal可被Jackson反序列化
你当前的UnmappedAnimal是普通内部类,没有使用Immutables注解,Jackson无法实例化它(毕竟其他子类都是用Immutables生成的不可变实现)。给它加上对应的注解并实现subClass()方法,还可以在这里添加日志记录:
@Value.Immutable @JsonDeserialize public abstract static class UnmappedAnimal extends Animal { @Override public abstract String subClass(); // 初始化时记录未知类型警告 @Value.Default public String logUnknownType() { Logger logger = LoggerFactory.getLogger(UnmappedAnimal.class); logger.warn("Received unknown animal type: {}", subClass()); return subClass(); } }
2. 验证defaultImpl的生效条件
在Jackson 2.9.7中,defaultImpl会在无法解析类型ID时自动生效,但需要满足两个条件:
- 类型ID的属性已正确读取(字段名匹配)
- 默认实现类可被Jackson实例化(比如通过Immutables生成了实现类)
如果还是抛出异常,检查你的ObjectMapper是否开启了FAIL_ON_INVALID_TYPE_ID——这个配置会覆盖defaultImpl的行为,需要禁用:
objectMapper.configure(DeserializationFeature.FAIL_ON_INVALID_TYPE_ID, false);
3. 自定义TypeIdResolver实现更灵活的控制
如果上述方法不够,你可以自定义类型解析器,手动处理未知类型并记录日志:
public class AnimalTypeIdResolver extends TypeIdResolverBase { private static final Logger logger = LoggerFactory.getLogger(AnimalTypeIdResolver.class); private JavaType baseType; @Override public void init(JavaType baseType) { this.baseType = baseType; } @Override public String idFromValue(Object value) { return value instanceof Animal ? ((Animal) value).subClass() : null; } @Override public String idFromValueAndType(Object value, Class<?> suggestedType) { return idFromValue(value); } @Override public JavaType typeFromId(DatabindContext context, String id) throws IOException { // 匹配已知类型 switch (id) { case Dog.ANIMAL_TYPE: return context.constructType(Dog.class); case Cat.ANIMAL_TYPE: return context.constructType(Cat.class); case Fish.ANIMAL_TYPE: return context.constructType(Fish.class); case Hamster.ANIMAL_TYPE: return context.constructType(Hamster.class); default: logger.warn("Unknown animal type '{}' received, falling back to UnmappedAnimal", id); return context.constructType(UnmappedAnimal.class); } } @Override public JsonTypeInfo.Id getMechanism() { return JsonTypeInfo.Id.NAME; } }
然后在Animal抽象类上指定这个解析器(可以去掉defaultImpl,因为解析器已经处理了):
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "subClass", include = JsonTypeInfo.As.EXISTING_PROPERTY, visible = true) @JsonTypeIdResolver(AnimalTypeIdResolver.class) @JsonSubTypes({ @JsonSubTypes.Type(value = Dog.class, name = Dog.ANIMAL_TYPE), @JsonSubTypes.Type(value = Cat.class, name = Cat.ANIMAL_TYPE), @JsonSubTypes.Type(value = Fish.class, name = Fish.ANIMAL_TYPE), @JsonSubTypes.Type(value = Hamster.class, name = Hamster.ANIMAL_TYPE) }) public static abstract class Animal { public abstract String subClass(); // 其他代码 }
二、解决子类型属性的大小写不敏感问题
你启用的ACCEPT_CASE_INSENSITIVE_PROPERTIES只对普通JavaBean属性生效,不会影响Jackson解析类型ID的属性名称。在Jackson 2.9.7中,要让类型ID的属性支持大小写不敏感,需要自定义TypeResolverBuilder:
1. 实现大小写不敏感的类型解析器构建器
public class CaseInsensitiveTypeResolverBuilder extends StdTypeResolverBuilder { @Override protected TypeIdResolver idResolver(MapperConfig<?> config, JavaType baseType, Collection<NamedType> subtypes, boolean forSer, boolean forDeser) { TypeIdResolver resolver = super.idResolver(config, baseType, subtypes, forSer, forDeser); if (resolver instanceof DefaultTypeIdResolver) { return new CaseInsensitiveTypeIdResolver((DefaultTypeIdResolver) resolver); } return resolver; } private static class CaseInsensitiveTypeIdResolver extends TypeIdResolverBase { private final DefaultTypeIdResolver delegate; public CaseInsensitiveTypeIdResolver(DefaultTypeIdResolver delegate) { this.delegate = delegate; } @Override public void init(JavaType baseType) { delegate.init(baseType); } @Override public String idFromValue(Object value) { return delegate.idFromValue(value); } @Override public String idFromValueAndType(Object value, Class<?> suggestedType) { return delegate.idFromValueAndType(value, suggestedType); } @Override public JavaType typeFromId(DatabindContext context, String id) throws IOException { // 将类型ID转为小写,匹配已知类型 String lowerCaseId = id.toLowerCase(Locale.ROOT); try { return delegate.typeFromId(context, lowerCaseId); } catch (InvalidTypeIdException e) { // 未知类型回退到默认实现 return delegate.typeFromId(context, null); } } @Override public JsonTypeInfo.Id getMechanism() { return delegate.getMechanism(); } } }
2. 配置ObjectMapper使用自定义构建器
ObjectMapper objectMapper = new ObjectMapper(); // 设置支持大小写不敏感的类型解析 objectMapper.setDefaultTyping(new CaseInsensitiveTypeResolverBuilder() .init(JsonTypeInfo.Id.NAME, null) .inclusion(JsonTypeInfo.As.EXISTING_PROPERTY) .typeProperty("subClass") // 这里要和你的注解property一致 .visibility(JsonTypeInfo.As.EXISTING_PROPERTY, JsonAutoDetect.Visibility.ANY) .defaultImpl(UnmappedAnimal.class)); // 保留你的原有配置 objectMapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
这样,不管JSON里的属性是"subClass"、"SubClass"还是"SUBCLASS",Jackson都能正确读取类型ID,并且转为小写后匹配已知类型,未知类型则回退到UnmappedAnimal。
内容的提问来源于stack exchange,提问作者CiaranC94

