You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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中存在的特征字段自动判断具体实现类。

  1. 创建自定义反序列化器:
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("无效的登录请求格式");
    }
}
  1. 在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 05:20:32