Spring中@RequestBody反序列化密封接口LoginRequest的问题
解决Spring中Sealed接口作为@RequestBody的反序列化问题
错误信息翻译
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: 无法构造
com.example.testingdemo.Types.UserFlow$LoginRequest的实例(不存在默认构造函数等创建者):抽象类型要么需要映射到具体类型,要么有自定义反序列化器,要么包含额外的类型信息。
问题原因
LoginRequest是密封接口(抽象类型),Jackson无法自动判断要把请求JSON反序列化成哪一个具体实现类(DefaultLogin/LoginWithEmail/LoginWithPhone),必须明确告知Jackson类型识别规则。
方案1:添加Jackson类型信息注解
在密封接口上通过注解指定类型标识字段,让Jackson根据该字段匹配具体实现类。
修改LoginRequest接口:
import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "loginType" // 用于标识具体登录类型的字段 ) @JsonSubTypes({ @JsonSubTypes.Type(value = DefaultLogin.class, name = "default"), @JsonSubTypes.Type(value = LoginWithEmail.class, name = "email"), @JsonSubTypes.Type(value = LoginWithPhone.class, name = "phone") }) public sealed interface LoginRequest permits DefaultLogin, LoginWithEmail, LoginWithPhone {}
前端请求时需携带loginType字段,示例如下:
- 默认登录请求:
{ "loginType": "default", "userId": 123, "password": "abc123" }
- 邮箱登录请求:
{ "loginType": "email", "email": "test@example.com" }
- 手机号登录请求:
{ "loginType": "phone", "phoneNumber": "13800138000" }
方案2:自定义反序列化器
无需前端额外传类型字段,根据JSON中存在的特征字段自动判断具体实现类。
- 创建自定义反序列化器:
import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import java.io.IOException; public class LoginRequestDeserializer extends StdDeserializer<LoginRequest> { protected LoginRequestDeserializer() { super(LoginRequest.class); } @Override public LoginRequest deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { JsonNode node = p.getCodec().readTree(p); if (node.has("userId") && node.has("password")) { return p.getCodec().treeToValue(node, DefaultLogin.class); } else if (node.has("email")) { return p.getCodec().treeToValue(node, LoginWithEmail.class); } else if (node.has("phoneNumber")) { return p.getCodec().treeToValue(node, LoginWithPhone.class); } throw new IllegalArgumentException("无效的登录请求格式"); } }
- 在
LoginRequest接口上注册该反序列化器:
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; @JsonDeserialize(using = LoginRequestDeserializer.class) public sealed interface LoginRequest permits DefaultLogin, LoginWithEmail, LoginWithPhone {}
前端直接传对应字段即可,比如邮箱登录请求:
{ "email": "test@example.com" }
方案3:拆分登录接口(可选)
如果不需要统一登录入口,直接拆分三个独立接口,避免处理抽象类型反序列化:
@PostMapping("/login/default") public ResponseEntity<String> defaultLogin(@RequestBody @Valid DefaultLogin loginRequest) { UserFlow.Authenticated login = userService.login(loginRequest); return ResponseEntity.accepted().body(login.toString()); } @PostMapping("/login/email") public ResponseEntity<String> emailLogin(@RequestBody @Valid LoginWithEmail loginRequest) { UserFlow.Authenticated login = userService.login(loginRequest); return ResponseEntity.accepted().body(login.toString()); } @PostMapping("/login/phone") public ResponseEntity<String> phoneLogin(@RequestBody @Valid LoginWithPhone loginRequest) { UserFlow.Authenticated login = userService.login(loginRequest); return ResponseEntity.accepted().body(login.toString()); }
内容的提问来源于stack exchange,提问作者Somjit
相关产品推荐
相关产品推荐

