在 Spring Boot 4 里,Jackson 3 用多参数构造器反序列化,大多数情况连 @JsonCreator 都不用写。前提是类文件里带着参数名,而 Spring Boot 的 starter parent 默认就打开了 -parameters。一旦把这个编译参数关掉,同一批类几乎全部反序列化失败,HTTP 接口直接 500。
这篇对着 Baeldung 的 JSON Deserialization with Multiple Parameters Constructor Using Jackson 重走了一遍。原文基于 Jackson 2.x,要手动引入 jackson-module-parameter-names、配 ConstructorDetector。我用的是 Spring Boot 4.1.1 自带的 Jackson 3.1.5,有几条结论和原文不一样。下文引用的 Jackson 2 报错文本和原文结论都出自 Baeldung 这篇文章。
环境
JDK 27 GA(build 27+35-2325),Spring Boot 4.1.1,依赖只有 spring-boot-starter-webmvc。打出来的 fat jar 里和 Jackson 相关的就这几个:
BOOT-INF/lib/spring-boot-jackson-4.1.1.jar
BOOT-INF/lib/jackson-databind-3.1.5.jar
BOOT-INF/lib/jackson-annotations-2.21.jar
BOOT-INF/lib/jackson-core-3.1.5.jar
没有 jackson-module-parameter-names。按 Jackson 3.0 的发布说明,这个模块的功能(databind#1775)已经并入 jackson-databind,构造器参数名自动检测默认开启,可以用 MapperFeature.DETECT_PARAMETER_NAMES 关掉。Jackson 3 的包名换成了 tools.jackson,注解还在 com.fasterxml.jackson.annotation,所以下面代码里两种包名都会出现。
-parameters 是 spring-boot-starter-parent 给的。它的 pom 里 maven-compiler-plugin 配了 <parameters>true</parameters>。为了对照,我在项目里加了个 profile 把它关掉:
<profiles>
<profile>
<id>no-parameters</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<parameters>false</parameters>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
用 javap 看得出区别。默认构建下,构造器带着 MethodParameters 属性:
MethodParameters:
Name Flags
currency
price
用 -Pno-parameters 构建后,同一条 javap ... | grep -A3 MethodParameters 什么都没输出。
测试类
沿用原文的 Ticket 例子,JSON 也一样:
{"event":"Devoxx","guest":"Maria Monroe","currency":"EUR","price":50}
最基本的一个类只有一个两参构造器,不加任何 Jackson 注解,guest 走 setter,eventName 只有字段上的 @JsonProperty("event"):
public class TicketNoAnnotation {
@JsonProperty("event")
private String eventName;
private String guest;
private final Currency currency;
private final int price;
public TicketNoAnnotation(Currency currency, int price) {
this.currency = currency;
this.price = price;
}
public void setGuest(String guest) { this.guest = guest; }
public String getEventName() { return eventName; }
public String getGuest() { return guest; }
public Currency getCurrency() { return currency; }
public int getPrice() { return price; }
@Override
public String toString() {
return "Ticket[event=" + eventName + ", guest=" + guest + ", currency=" + currency + ", price=" + price + "]";
}
}
另外几个类对应原文里的各种情况:TicketAnnotated 在构造器上写全 @JsonCreator 和参数上的 @JsonProperty;TicketTwoCtors 有两参和三参两个构造器,都不加注解;TicketTwoCreators 两个构造器都标 @JsonCreator;还有 record 和枚举,后面单独说。
所有类都在一个 ApplicationRunner 里用 Spring Boot 自动配置的 JsonMapper 反序列化,失败就打印异常类名和第一行消息:
@Component
public class CreatorReport implements ApplicationRunner {
private static final Logger log = LoggerFactory.getLogger(CreatorReport.class);
static final String TICKET = """
{"event":"Devoxx","guest":"Maria Monroe","currency":"EUR","price":50}""";
static final String GUEST = """
{"firstname":" Maria ","surname":"Monroe","id":7}""";
static final String CURRENCY = """
{"fullName":"Pound sterling","fractionalUnit":"penny"}""";
private final JsonMapper mapper;
CreatorReport(JsonMapper mapper) {
this.mapper = mapper;
}
@Override
public void run(ApplicationArguments args) throws Exception {
log.info("Jackson {}, -parameters: {}", tools.jackson.databind.cfg.PackageVersion.VERSION,
TicketNoAnnotation.class.getConstructors()[0].getParameters()[0].isNamePresent());
read("TicketNoAnnotation", TICKET, TicketNoAnnotation.class);
read("TicketAnnotated", TICKET, TicketAnnotated.class);
read("TicketTwoCtors", TICKET, TicketTwoCtors.class);
read("TicketTwoCreators", TICKET, TicketTwoCreators.class);
read("Guest(record + @JsonCreator ctor)", GUEST, Guest.class);
read("GuestFactory(record + @JsonCreator factory)", GUEST, GuestFactory.class);
log.info("ObjCurrency.GBP serialized: {}", mapper.writeValueAsString(ObjCurrency.GBP));
read("ObjCurrency", CURRENCY, ObjCurrency.class);
}
private void read(String name, String json, Class<?> type) {
try {
log.info("{} -> {}", name, mapper.readValue(json, type));
} catch (Exception e) {
String msg = e.getMessage().lines().findFirst().orElse("");
log.info("{} -> {}: {}", name, e.getClass().getName(), msg);
}
}
}
再加一个接口,看 Spring MVC 收请求体时是不是同样的行为:
@RestController
public class TicketController {
@PostMapping("/tickets")
public String create(@RequestBody TicketNoAnnotation ticket) {
return ticket.toString();
}
}
默认构建:不写注解也能用
2026-10-14T08:06:28.933+08:00 INFO 781558 --- [ main] com.spring4all.jackson.CreatorReport : Jackson 3.1.5, -parameters: true
2026-10-14T08:06:28.993+08:00 INFO 781558 --- [ main] com.spring4all.jackson.CreatorReport : TicketNoAnnotation -> Ticket[event=Devoxx, guest=Maria Monroe, currency=EUR, price=50]
2026-10-14T08:06:28.996+08:00 INFO 781558 --- [ main] com.spring4all.jackson.CreatorReport : TicketAnnotated -> Ticket[event=Devoxx, guest=Maria Monroe, currency=EUR, price=50]
Baeldung 原文第 3 节说,只有两参构造器、没有无参构造器时,Jackson 2 会抛 MismatchedInputException,说找不到 Creator。在 Jackson 3 加 -parameters 的组合下,这个类直接就反序列化成功了,构造器参数按名字对上 currency 和 price,guest 走 setter,event 通过字段注解写进 eventName。这和发布说明里参数名检测并入 databind 的变化对得上:类文件里有参数名,单个公开构造器就能被当成按属性构造的 Creator。
HTTP 接口也一样:
$ curl -s -X POST localhost:18082/tickets -H 'Content-Type: application/json' -d '{"event":"Devoxx","guest":"Maria Monroe","currency":"EUR","price":50}'
Ticket[event=Devoxx, guest=Maria Monroe, currency=EUR, price=50]
但构造器一多,自动检测就不干了:
2026-10-14T08:06:28.998+08:00 INFO 781558 --- [ main] com.spring4all.jackson.CreatorReport : TicketTwoCtors -> tools.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.spring4all.jackson.model.TicketTwoCtors` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
有两个可选的构造器,Jackson 3 不会替你选,这时候还得用 @JsonCreator 指定一个。两个都标上,报的是原文提到的那个冲突:
2026-10-14T08:06:28.999+08:00 INFO 781558 --- [ main] com.spring4all.jackson.CreatorReport : TicketTwoCreators -> tools.jackson.databind.exc.InvalidDefinitionException: Conflicting property-based creators: already had explicit creator [constructor for `com.spring4all.jackson.model.TicketTwoCreators` (2 args), annotations: {interface com.fasterxml.jackson.annotation.JsonCreator=@com.fasterxml.jackson.annotation.JsonCreator(mode=DEFAULT)}, encountered another: [constructor for `com.spring4all.jackson.model.TicketTwoCreators` (3 args), annotations: {interface com.fasterxml.jackson.annotation.JsonCreator=@com.fasterxml.jackson.annotation.JsonCreator(mode=DEFAULT)}
Baeldung 原文贴的 Jackson 2 异常是 com.fasterxml.jackson.databind.JsonMappingException,消息里写的是 already had explicitly marked creator。到了 Jackson 3.1.5,异常类型变成 tools.jackson.databind.exc.InvalidDefinitionException,消息变成 already had explicit creator。项目里有按异常类名或消息做判断的代码,升级时要一起改。
record 的非规范构造器会被用上
Baeldung 原文第 5 节说,record 上用 @JsonCreator 标一个非规范构造器,Jackson 会忽略它,改用规范构造器。我照着写了一个,构造器把 id 拼到 surname 后面,方便看到底走了哪个:
public record Guest(String firstname, String surname) {
@JsonCreator
public Guest(String firstname, String surname, int id) {
this(firstname, surname + "#" + id);
}
}
输入是 {"firstname":" Maria ","surname":"Monroe","id":7},结果:
2026-10-14T08:06:29.002+08:00 INFO 781558 --- [ main] com.spring4all.jackson.CreatorReport : Guest(record + @JsonCreator ctor) -> Guest[firstname= Maria , surname=Monroe#7]
surname 变成了 Monroe#7,说明 Jackson 3.1.5 用的就是这个三参构造器,和原文的结论相反。这个变化不是 Jackson 3 才有:jackson-databind 2.15 起通过 PR #3724 让 record 改走和普通 POJO 相同的 Creator 检测机制,显式标了 @JsonCreator 的构造器会被采用,Jackson 3 沿用了这套机制。静态工厂方法也能用,我在工厂里顺手 trim 了一下:
public record GuestFactory(String firstname, String surname) {
@JsonCreator
public static GuestFactory fromJson(String firstname, String surname, int id) {
return new GuestFactory(firstname.trim(), surname.trim() + "#" + id);
}
}
2026-10-14T08:06:29.004+08:00 INFO 781558 --- [ main] com.spring4all.jackson.CreatorReport : GuestFactory(record + @JsonCreator factory) -> GuestFactory[firstname=Maria, surname=Monroe#7]
枚举按对象形状反序列化
枚举加上 @JsonFormat(shape = JsonFormat.Shape.OBJECT) 后会序列化成对象,反序列化要靠一个 @JsonCreator 静态工厂。Baeldung 原文的工厂方法里把 fractionalUnit 拼成了 franctionalUnit,照抄编译不过,修正后是这样:
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum ObjCurrency {
EUR("Euro", "cent"),
GBP("Pound sterling", "penny");
private final String fullName;
private final String fractionalUnit;
ObjCurrency(String fullName, String fractionalUnit) {
this.fullName = fullName;
this.fractionalUnit = fractionalUnit;
}
public String getFullName() { return fullName; }
public String getFractionalUnit() { return fractionalUnit; }
@JsonCreator
public static ObjCurrency fromJson(String fullName, String fractionalUnit) {
for (ObjCurrency c : values()) {
if (c.fullName.equalsIgnoreCase(fullName) && c.fractionalUnit.equalsIgnoreCase(fractionalUnit)) {
return c;
}
}
throw new IllegalArgumentException("Unknown currency: " + fullName + " " + fractionalUnit);
}
}
2026-10-14T08:06:29.017+08:00 INFO 781558 --- [ main] com.spring4all.jackson.CreatorReport : ObjCurrency.GBP serialized: {"fullName":"Pound sterling","fractionalUnit":"penny"}
2026-10-14T08:06:29.018+08:00 INFO 781558 --- [ main] com.spring4all.jackson.CreatorReport : ObjCurrency -> GBP
工厂方法的参数上同样没写 @JsonProperty。
关掉 -parameters 之后
用 mvn -q clean package -DskipTests -Pno-parameters 重新打包,同一个 jar 再跑一次:
2026-10-14T08:06:35.126+08:00 INFO 781906 --- [ main] com.spring4all.jackson.CreatorReport : Jackson 3.1.5, -parameters: false
2026-10-14T08:06:35.172+08:00 INFO 781906 --- [ main] com.spring4all.jackson.CreatorReport : TicketNoAnnotation -> tools.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.spring4all.jackson.model.TicketNoAnnotation` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
2026-10-14T08:06:35.183+08:00 INFO 781906 --- [ main] com.spring4all.jackson.CreatorReport : TicketAnnotated -> Ticket[event=Devoxx, guest=Maria Monroe, currency=EUR, price=50]
2026-10-14T08:06:35.186+08:00 INFO 781906 --- [ main] com.spring4all.jackson.CreatorReport : TicketTwoCtors -> tools.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.spring4all.jackson.model.TicketTwoCtors` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
2026-10-14T08:06:35.187+08:00 INFO 781906 --- [ main] com.spring4all.jackson.CreatorReport : TicketTwoCreators -> tools.jackson.databind.exc.InvalidDefinitionException: Invalid type definition for type `com.spring4all.jackson.model.TicketTwoCreators`: More than one argument (#0 and #1) left as delegating for Creator [constructor for `com.spring4all.jackson.model.TicketTwoCreators` (2 args), annotations: {interface com.fasterxml.jackson.annotation.JsonCreator=@com.fasterxml.jackson.annotation.JsonCreator(mode=DEFAULT)}: only one allowed
2026-10-14T08:06:35.189+08:00 INFO 781906 --- [ main] com.spring4all.jackson.CreatorReport : Guest(record + @JsonCreator ctor) -> tools.jackson.databind.exc.InvalidDefinitionException: Invalid type definition for type `com.spring4all.jackson.model.Guest`: More than one argument (#0 and #1) left as delegating for Creator [constructor for `com.spring4all.jackson.model.Guest` (3 args), annotations: {interface com.fasterxml.jackson.annotation.JsonCreator=@com.fasterxml.jackson.annotation.JsonCreator(mode=DEFAULT)}: only one allowed
2026-10-14T08:06:35.190+08:00 INFO 781906 --- [ main] com.spring4all.jackson.CreatorReport : GuestFactory(record + @JsonCreator factory) -> tools.jackson.databind.exc.InvalidDefinitionException: Invalid type definition for type `com.spring4all.jackson.model.GuestFactory`: More than one argument (#0 and #1) left as delegating for Creator [method com.spring4all.jackson.model.GuestFactory#fromJson(3 params)]: only one allowed
2026-10-14T08:06:35.204+08:00 INFO 781906 --- [ main] com.spring4all.jackson.CreatorReport : ObjCurrency -> tools.jackson.databind.exc.InvalidDefinitionException: Invalid type definition for type `com.spring4all.jackson.model.ObjCurrency`: More than one argument (#0 and #1) left as delegating for Creator [method com.spring4all.jackson.model.ObjCurrency#fromJson(2 params)]: only one allowed
只有在参数上写了 @JsonProperty 的 TicketAnnotated 还能用。其余几个要么找不到 Creator,要么报 More than one argument (#0 and #1) left as delegating:Jackson 拿不到参数名,就把 @JsonCreator 当成委托式 Creator 处理,而委托式只允许一个参数。这和 Baeldung 原文第 4.2 节讲的情况是一回事,只是 Jackson 3 的报错文案不同。
接口这边返回的是 500,不是 400:
{"timestamp":"2026-10-14T00:06:36.160Z","status":500,"error":"Internal Server Error","path":"/tickets"}
2026-10-14T08:06:36.150+08:00 ERROR 781906 --- [io-18082-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class com.spring4all.jackson.model.TicketNoAnnotation]] with root cause
InvalidDefinitionException 属于类型定义错误,Spring MVC 把它当成服务端问题,记 ERROR 日志并返回 500。
序列化结果也变了:
2026-10-14T08:06:35.203+08:00 INFO 781906 --- [ main] com.spring4all.jackson.CreatorReport : ObjCurrency.GBP serialized: {"fractionalUnit":"penny","fullName":"Pound sterling"}
带 -parameters 时输出是 fullName 在前,关掉之后变成 fractionalUnit 在前。Jackson 会参考 Creator 参数的顺序来排属性,拿不到参数名时这层信息就没了。有接口对比 JSON 字符串、或者对字段顺序有约定的,这一点要留意。
-parameters 不只是 Maven 的事。用 Gradle、或者在 IDE 里直接编译而没有走 starter parent 的配置时,都可能不带这个参数。Gradle 的 Spring Boot 插件会给 JavaCompile 任务加上 -parameters,不用 starter parent 的 Maven 项目则需要自己在 maven-compiler-plugin 里配。
之前那篇把 JDK 28 孵化的 JSON API 和 Jackson 放在一起对比的文章讲的是解析的严格程度,这篇关心的是对象怎么构造出来,两篇用的都是 Spring Boot 4.1.1 自带的 Jackson 3.1.5。
参考:Baeldung,JSON Deserialization with Multiple Parameters Constructor Using Jackson(Ticket、Guest、Currency 示例与对照的结论来自此文)。
一起交流
分享你的思考,让讨论更进一步。