记录一个jackson使用@JsonTypeInfo实现多态反序列化问题:missing type id property '@Clazz'
异常信息
missing type id property ‘@Clazz’ (for POJO property ‘xxx’):
Caused by: com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Missing type id when trying to resolve subtype of [simple type, class com.clearofchina.uaa.model.SysUserDetail]: missing type id property '@Clazz' (for POJO property 'result')
at [Source: (PushbackInputStream); line: 1, column: 388] (through reference chain: com.clearofchina.core.model.ResponseModel["result"])
at com.fasterxml.jackson.databind.exc.InvalidTypeIdException.from(InvalidTypeIdException.java:43)
at com.fasterxml.jackson.databind.DeserializationContext.missingTypeIdException(DeserializationContext.java:1771)
at com.fasterxml.jackson.databind.DeserializationContext.handleMissingTypeId(DeserializationContext.java:1300)
at com.fasterxml.jackson.databind.jsontype.impl.TypeDeserializerBase._handleMissingTypeId(TypeDeserializerBase.java:299)
at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedUsingDefaultImpl(AsPropertyTypeDeserializer.java:164)
at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:105)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeWithType(BeanDeserializerBase.java:1178)
at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:138)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:369)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:159)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4218)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3267)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:239)
... 76 more
问题产生背景及模拟代码
在封装统一认证鉴权模块时,要给网关提供微服务调用接口,返回鉴权成功后的用户信息,用户分系统用户、渠道应用、机构用户等不同类型,最终统一封装成ResponseModel对象返回,由于不同的用户信息实现用户信息抽象出来的接口,所以用到多态。同时又有根据用户ID获取用户信息的微服务需要提供,也封装成ResponseModel对象返回。由于ResponseModel中泛型的原因,导致使用@JsonTypeInfo中的“@Class”属性丢失,产生了以上的异常。业务描述比较难懂,上代码。
- 统一返回对象ResponseModel
package com.clearofchina.uaa.json;
import lombok.Data;
/**
* 统一返回对象
*
* @author Zhouych
* @date 2020年5月21日 上午10:42:52
* @since V1.0.0
*/
@Data
public class ResponseModel<T> {
private int code;
private String message;
private T result;
}
- 用户对象抽象IUserInfo,使用@JsonTypeInfo来实现反序列化时多态区分
package com.clearofchina.uaa.json;
/**
* 用户信息抽象
*
* @author Zhouych
* @date 2020年5月21日 上午10:49:23
* @since V1.0.0
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS,include = JsonTypeInfo.As.PROPERTY,property = "@Clazz")
public interface IUserInfo {
/**
* 获取用户唯一主键
*
* @return
*/
@JsonIgnore
String getUserKey();
}
- 系统用户信息SysUserInfo,实现IUserInfo 接口,继承了@JsonTypeInfo
package com.clearofchina.uaa.json;
import lombok.Data;
/**
* 系统用户信息
*
* @author Zhouych
* @date 2020年5月21日 上午10:52:15
* @since V1.0.0
*/
@Data
public class SysUserInfo implements IUserInfo {
private Long userId;
private String username;
private int age;
@Override
public String getUserKey() {
return String.valueOf(this.userId);
}
}
- 渠道应用信息:ChlAppInfo,实现IUserInfo 接口,继承了@JsonTypeInfo
package com.clearofchina.uaa.json;
import lombok.Data;
/**
* 渠道应用信息
*
* @author Zhouych
* @date 2020年5月21日 上午10:58:46
* @since V1.0.0
*/
@Data
public class ChlAppInfo implements IUserInfo {
private String appKey;
private String appSecret;
private String name;
@Override
public String getUserKey() {
return this.appKey;
}
}
- 鉴权成功返回信息 AuthSuccessInfo
/**
* 鉴权成功返回信息
*
* @author Zhouych
* @date 2020年5月21日 下午1:48:27
* @since V1.0.0
*/
@Data
public class AuthSuccessInfo {
private String sign;
private IUserInfo userInfo;
}
- 产生问题模拟类Main
package com.clearofchina.uaa.json;
import java.util.Random;
import java.util.UUID;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
*
* @author Zhouych
* @date 2020年5月21日 上午11:00:58
* @since V1.0.0
*/
public class Main {
public static void main(String[] args) throws Exception {
// 模拟服务消费端
Main main = new Main();
// 请求并解析数据(真实场景为通过openfeign调用api接口)
String userInfoResponse = main.authService("xxxx");
ObjectMapper objectMapper = new ObjectMapper();
ResponseModel<AuthSuccessInfo> response = objectMapper.readValue(userInfoResponse,
objectMapper.getTypeFactory().constructParametricType(ResponseModel.class, AuthSuccessInfo.class));
System.out.println(userInfoResponse);
System.out.println(response);
//输出正确,没什么问题
// {"code":200,"message":"SUCCESS","result":{"sign":"e40dfabb-1c4c-41d8-af0a-ec1af884a0c2","userInfo":{"@Clazz":"com.clearofchina.uaa.json.ChlAppInfo","appKey":"123654","appSecret":"888888","name":"微信公众号"}}}
// ResponseModel(code=200, message=SUCCESS, result=AuthSuccessInfo(sign=e40dfabb-1c4c-41d8-af0a-ec1af884a0c2, userInfo=ChlAppInfo(appKey=123654, appSecret=888888, name=微信公众号)))
// 问题出在另外有微服务要提供根据ID获取用户信息
String sysUserInfoString = main.getSysUserInfoById(1L);
System.out.println(sysUserInfoString);
// {"code":200,"message":"SUCCESS","result":{"userId":1,"username":"zhangsan","age":18}}
ResponseModel<SysUserInfo> sysUserInfo = objectMapper.readValue(userInfoResponse,
objectMapper.getTypeFactory().constructParametricType(ResponseModel.class, SysUserInfo.class));
System.out.println(sysUserInfo);
// 产生异常
/*
* Exception in thread "main"
* com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Missing type id
* when trying to resolve subtype of [simple type, class
* com.clearofchina.uaa.json.SysUserInfo]: missing type id property '@Clazz'
* (for POJO property 'result') at [Source:
* (String)"{"code":200,"message":"SUCCESS","result":{"sign":"233d310b-0b4f-4131
* -91ed-0989a5525546","userInfo":{"@Clazz":"com.clearofchina.uaa.json.
* ChlAppInfo","appKey":"123654","appSecret":"888888","name":"微信公众号"}}}"; line:
* 1, column: 203] (through reference chain:
* com.clearofchina.uaa.json.ResponseModel["result"]) at
* com.fasterxml.jackson.databind.exc.InvalidTypeIdException.from(
* InvalidTypeIdException.java:43) at
* com.fasterxml.jackson.databind.DeserializationContext.missingTypeIdException(
* DeserializationContext.java:1771) at
* com.fasterxml.jackson.databind.DeserializationContext.handleMissingTypeId(
* DeserializationContext.java:1300) at
* com.fasterxml.jackson.databind.jsontype.impl.TypeDeserializerBase.
* _handleMissingTypeId(TypeDeserializerBase.java:299) at
* com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.
* _deserializeTypedUsingDefaultImpl(AsPropertyTypeDeserializer.java:164) at
* com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.
* deserializeTypedFromObject(AsPropertyTypeDeserializer.java:105) at
* com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeWithType
* (BeanDeserializerBase.java:1178) at
* com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(
* MethodProperty.java:138) at
* com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(
* BeanDeserializer.java:288) at
* com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(
* BeanDeserializer.java:151) at
* com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.
* java:4218) at
* com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3214)
* at com.clearofchina.uaa.json.Main.main(Main.java:38)
*/
}
/**
* 模拟服务提供方提供的鉴权服务:模拟http请求响应json数据
*
* @param token
* @return
* @throws Exception
*/
public String authService(String token) throws Exception {
Random random = new Random();
ObjectMapper objectMapper = new ObjectMapper();
AuthSuccessInfo result = new AuthSuccessInfo();
if (random.nextBoolean()) {
SysUserInfo userInfo = new SysUserInfo();
userInfo.setAge(18);
userInfo.setUsername("zhangsan");
userInfo.setUserId(1L);
result.setSign(UUID.randomUUID().toString());
result.setUserInfo(userInfo);
} else {
ChlAppInfo userInfo = new ChlAppInfo();
userInfo.setAppKey("123654");
userInfo.setAppSecret("888888");
userInfo.setName("微信公众号");
result.setSign(UUID.randomUUID().toString());
result.setUserInfo(userInfo);
}
ResponseModel<AuthSuccessInfo> response = new ResponseModel<>();
response.setCode(200);
response.setMessage("SUCCESS");
response.setResult(result);
return objectMapper.writeValueAsString(response);
}
/**
* 模拟服务提供方提供的查询用户信息服务:模拟http请求响应json数据
*
* @param userId
* @return
* @throws Exception
*/
public String getSysUserInfoById(Long userId) throws Exception {
SysUserInfo userInfo = new SysUserInfo();
userInfo.setAge(18);
userInfo.setUsername("zhangsan");
userInfo.setUserId(userId);
ObjectMapper objectMapper = new ObjectMapper();
ResponseModel<SysUserInfo> response = new ResponseModel<>();
response.setCode(200);
response.setMessage("SUCCESS");
response.setResult(userInfo);
return objectMapper.writeValueAsString(response);
}
}
解决方法
产生问题的原因是,由于反序列化多态的需要,添加了@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS,include = JsonTypeInfo.As.PROPERTY,property = “@Clazz”)这个注解,但由于ResponseModel里T result是泛型,序列化SysUserInfo为json字符串时,“@Clazz”属性丢失,导致反序列化时取@Clazz属性时无法找到。
- 解决方法一:既然是序列化时少了“@Clazz”属性,那就添上
修改的IUserInfo类
package com.clearofchina.uaa.json;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonProperty.Access;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
/**
* 用户信息抽象
*
* @author Zhouych
* @date 2020年5月21日 上午10:49:23
* @since V1.0.0
*/
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@Clazz")
public interface IUserInfo {
/**
* 获取用户唯一主键
*
* @return
*/
@JsonIgnore
String getUserKey();
/**
* 强行添加"@Clazz"属性
*
* @return
*/
@JsonProperty(value = "@Clazz", access = Access.READ_ONLY)
String getClazz();
}
对应的两个实现类SysUserInfo和ChlAppInfo实现该方法即可:
package com.clearofchina.uaa.json;
import lombok.Data;
/**
* 渠道应用信息
*
* @author Zhouych
* @date 2020年5月21日 上午10:58:46
* @since V1.0.0
*/
@Data
public class ChlAppInfo implements IUserInfo {
private String appKey;
private String appSecret;
private String name;
@Override
public String getUserKey() {
return this.appKey;
}
@Override
public String getClazz() {
return this.getClass().getName();
}
}
package com.clearofchina.uaa.json;
import lombok.Data;
/**
1. 系统用户信息
2.
3. @author Zhouych
4. @date 2020年5月21日 上午10:52:15
5. @since V1.0.0
*/
@Data
public class SysUserInfo implements IUserInfo {
private Long userId;
private String username;
private int age;
@Override
public String getUserKey() {
return String.valueOf(this.userId);
}
@Override
public String getClazz() {
return this.getClass().getName();
}
}
此次有个注意的点,就是添加“@Clazz”属性后,反序列化字符串中会把“@Clazz”带到反序列化器,所以要添加@JsonIgnoreProperties(ignoreUnknown = true)注解忽略掉位置属性。
- 解决方法二:在具体实现类使用@JsonTypeInfo覆盖掉接口类的@JsonTypeInfo,并添加默认实现属性,如下:
package com.clearofchina.uaa.json;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import lombok.Data;
/**
* 系统用户信息
*
* @author Zhouych
* @date 2020年5月21日 上午10:52:15
* @since V1.0.0
*/
@Data
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, defaultImpl = SysUserInfo.class)
public class SysUserInfo implements IUserInfo {
private Long userId;
private String username;
private int age;
@Override
public String getUserKey() {
return String.valueOf(this.userId);
}
// @Override
// public String getClazz() {
// return this.getClass().getName();
// }
}