@JsonTypeInfo在REST端点使用具体类时的序列化异常咨询
问题原因与解决方案
你遇到的这个问题是Jackson注解继承特性导致的预期行为,不是操作错误。让我一步步拆解原因和解决办法:
为什么直接用Message1会要求@class属性?
你在Message接口上添加了@JsonTypeInfo注解,而Jackson会将接口上的序列化/反序列化配置继承给所有实现类。这意味着:
- 当你处理
List<Message>时,Jackson会按照注解要求给每个子类加上@class字段,这符合你的预期; - 但当你直接使用
Message1作为请求体时,Jackson依然会应用接口上的@JsonTypeInfo规则,期望输入JSON中包含@class字段来识别子类型——哪怕你已经明确指定了参数类型是Message1,它还是会触发多态类型解析逻辑,所以才会抛出"Missing type id property '@class'"的异常。
解决办法
根据你的场景,有几种实用的解决方案:
方案1:将@JsonTypeInfo移到抽象类而非接口
把原来的Message接口改成抽象类,将类型信息注解放在抽象类上:
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class") abstract class Message : Serializable data class Message1(val name: String, val age: Int) : Message() data class Message2(val name: String, val nickName: String) : Message()
这样做的好处是:
- 当你处理
List<Message>(抽象类类型)时,Jackson会触发多态类型解析,自动添加/识别@class字段; - 当你直接使用
Message1作为请求体时,Jackson知道这是具体类型,不会触发多态逻辑,也就不需要@class字段了。
方案2:在具体子类上用@JsonIgnoreTypeInfo覆盖接口配置
如果你必须保留Message作为接口,可以在不需要类型信息的子类上添加@JsonIgnoreTypeInfo注解,覆盖接口的配置:
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class") interface Message : Serializable @JsonIgnoreTypeInfo data class Message1(val name: String, val age: Int) : Message @JsonIgnoreTypeInfo data class Message2(val name: String, val nickName: String) : Message
这个注解会告诉Jackson:当直接处理该子类时,忽略接口上的@JsonTypeInfo规则,不需要@class字段。而当你处理List<Message>时,依然会正常生成@class字段用于反序列化。
方案3:使用Mix-in灵活配置不同场景的规则
如果你的场景更复杂(比如部分子类需要在某些场景保留类型信息),可以用Jackson的Mix-in功能来给不同端点配置不同的序列化规则:
- 定义一个不带
@JsonTypeInfo的Mix-in类:
interface MessageWithoutTypeInfo : Serializable
- 在你的
MessageController中,针对/add1端点的ObjectMapper注册Mix-in:
@RestController class MessageController { @Autowired private lateinit var objectMapper: ObjectMapper @PostMapping("/add1") fun add(@RequestBody content: Message1) { // 临时注册Mix-in,处理Message1时忽略类型信息 val localMapper = objectMapper.copy() localMapper.addMixIn(Message::class.java, MessageWithoutTypeInfo::class.java) // 用localMapper处理请求内容 // do something } @GetMapping("/messages") fun getEndpoints(): List<Message> { return listOf(Message1("Marco", 22), Message2("Polo", "Poli")) } }
这种方式更灵活,注意使用ObjectMapper的副本避免影响全局配置。
总结
你的问题本质是Jackson对接口注解的继承特性导致的,属于预期行为。通过调整注解位置、覆盖配置或使用Mix-in,都可以解决"具体类请求体要求@class字段"的问题,推荐优先使用方案1或方案2,实现起来更简洁。
内容的提问来源于stack exchange,提问作者peach




