英文:
When does Jackson require no-arg constructor for deserialization?
问题
在我的 Spring Boot 项目中,我注意到了奇怪的 Jackson 行为。我在互联网上搜索了一下,找到了解决方法,但没有找到为什么。
UserDto:
@Setter
@Getter
@AllArgsConstructor
public class UserDto {
private String username;
private String email;
private String password;
private String name;
private String surname;
private UserStatus status;
private byte[] avatar;
private ZonedDateTime created_at;
}
添加新用户运行得很好。
TagDto:
@Setter
@Getter
@AllArgsConstructor
public class TagDto {
private String tag;
}
尝试添加新标签会导致错误:
>com.fasterxml.jackson.databind.exc.MismatchedInputException: 无法构造 TagDto 的实例(即使存在至少一个 Creator):无法从对象值反序列化(无委托或基于属性的 Creator)
解决问题的方法是在 TagDto 类中添加无参数构造函数。
为什么 Jackson 在 TagDto 的反序列化中需要无参数构造函数,而对于 UserDto 则正常工作?
对两者都使用了相同的方法进行添加。
我的 Tag 和 User 实体都带有以下注解:
@Entity
@Setter
@Getter
@NoArgsConstructor
并且都有全参数构造函数:
@Entity
@Setter
@Getter
@NoArgsConstructor
public class User extends AbstractModel {
private String username;
private String password;
private String email;
private String name;
private String surname;
private UserStatus status;
@Lob
private byte[] avatar;
@Setter(AccessLevel.NONE)
private ZonedDateTime created_at;
public User(final String username, final String password, final String email, final String name, final String surname) {
this.username = username;
this.password = password;
this.email = email;
this.name = name;
this.surname = surname;
this.created_at = ZonedDateTime.now();
}
}
@Entity
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Tag extends AbstractModel {
private String tag;
}
@MappedSuperclass
@Getter
public abstract class AbstractModel {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
}
实体生成:
@PostMapping(path = "/add")
public ResponseEntity<String> add(@Valid @RequestBody final D dto) {
this.abstractModelService.add(dto);
return new ResponseEntity<>("Success", HttpStatus.CREATED);
}
public void add(final D dto) {
//CRUD repository save method
this.modelRepositoryInterface.save(this.getModelFromDto(dto));
}
@Override
protected Tag getModelFromDto(final TagDto tagDto) {
return new Tag(tagDto.getTag());
}
@Override
protected User getModelFromDto(final UserDto userDto) {
return new User(userDto.getUsername(), userDto.getPassword(), userDto.getEmail(), userDto.getName(), userDto.getSurname());
}
当解析 JSON 时发生错误:
{"tag":"example"}
通过 postman 发送到 localhost:8081/tag/add,返回:
{
"timestamp": "2020-09-26T18:50:39.974+00:00",
"status": 400,
"error": "Bad Request",
"message": "",
"path": "/tag/add"
}
我使用的是 Lombok v1.18.12 和 Spring Boot 2.3.3.RELEASE,以及 Jackson v2.11.2。
英文:
In my spring boot project, I noticed a strange Jackson behavior. I searched over internet, found out what to do, but haven't found out why.
UserDto:
@Setter
@Getter
@AllArgsConstructor
public class UserDto {
private String username;
private String email;
private String password;
private String name;
private String surname;
private UserStatus status;
private byte[] avatar;
private ZonedDateTime created_at;
}
Adding a new user works just fine.
TagDto:
@Setter
@Getter
@AllArgsConstructor
public class TagDto {
private String tag;
}
Trying to add a new tag ends with an error:
>com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of TagDto (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
The solution to the problem was to add zero-arg constructor to the TagDto class.
Why does Jackson require no-arg constructor for deserialization in TagDto, while working just fine with UserDto?
Used same method for adding both.
My Tag and User entities are both annotated with
@Entity
@Setter
@Getter
@NoArgsConstructor
and have all args constructors:
@Entity
@Setter
@Getter
@NoArgsConstructor
public class User extends AbstractModel {
private String username;
private String password;
private String email;
private String name;
private String surname;
private UserStatus status;
@Lob
private byte[] avatar;
@Setter(AccessLevel.NONE)
private ZonedDateTime created_at;
public User(final String username, final String password, final String email, final String name, final String surname) {
this.username = username;
this.password = password;
this.email = email;
this.name = name;
this.surname = surname;
this.created_at = ZonedDateTime.now();
}
}
@Entity
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Tag extends AbstractModel {
private String tag;
}
@MappedSuperclass
@Getter
public abstract class AbstractModel {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
}
Entity generation:
@PostMapping(path = "/add")
public ResponseEntity<String> add(@Valid @RequestBody final D dto) {
this.abstractModelService.add(dto);
return new ResponseEntity<>("Success", HttpStatus.CREATED);
}
public void add(final D dto) {
//CRUD repository save method
this.modelRepositoryInterface.save(this.getModelFromDto(dto));
}
@Override
protected Tag getModelFromDto(final TagDto tagDto) {
return new Tag(tagDto.getTag());
}
@Override
protected User getModelFromDto(final UserDto userDto) {
return new User(userDto.getUsername(), userDto.getPassword(), userDto.getEmail(), userDto.getName(), userDto.getSurname());
}
Error occurs when parsing JSON
{"tag":"example"}
sent via postman localhost:8081/tag/add, returns
{
"timestamp": "2020-09-26T18:50:39.974+00:00",
"status": 400,
"error": "Bad Request",
"message": "",
"path": "/tag/add"
}
I am using Lombok v1.18.12 and Spring boot 2.3.3.RELEASE with Jackson v2.11.2.
答案1
得分: 25
TL;DR: 解决方案在文末。
Jackson支持多种创建POJO的方式。以下列出了最常见的方式,但可能不是完整的列表:
-
使用无参构造函数创建实例,然后调用setter方法来分配属性值。
public class Foo { private int id; public int getId() { return this.id; } @JsonProperty public void setId(int id) { this.id = id; } }
指定
@JsonProperty
是可选的,但可以与@JsonIgnore
、@JsonAnyGetter
等注解一起用于微调映射。 -
使用带参数的构造函数创建实例。
public class Foo { private int id; @JsonCreator public Foo(@JsonProperty("id") int id) { this.id = id; } public int getId() { return this.id; } }
为构造函数指定
@JsonCreator
是可选的,但如果存在多个构造函数,我认为是必需的。为参数指定@JsonProperty
是可选的,但如果参数名未包含在类文件中(使用-parameters
编译选项),则需要为属性命名。这些参数意味着属性是必需的。可以使用setter方法设置可选属性。
-
使用工厂方法创建实例。
public class Foo { private int id; @JsonCreator public static Foo create(@JsonProperty("id") int id) { return new Foo(id); } private Foo(int id) { this.id = id; } public int getId() { return this.id; } }
-
使用
String
构造函数从文本值创建实例。public class Foo { private int id; @JsonCreator public Foo(String str) { this.id = Integer.parseInt(id); } public int getId() { return this.id; } @JsonValue public String asJsonValue() { return Integer.toString(this.id); } }
当POJO具有简单的文本表示时,这是有用的,例如,
LocalDate
是一个具有3个属性(year
、month
、dayOfMonth
)的POJO,但通常最好将其序列化为单个字符串(yyyy-MM-dd
格式)。@JsonValue
标识在序列化期间要使用的方法,而@JsonCreator
则标识在反序列化期间要使用的构造函数/工厂方法。注意: 这也适用于使用除
String
以外的JSON值进行单值构造,但这很少见。
好的,那就是背景信息。在问题的示例中,UserDto
起作用,因为只有一个构造函数(所以不需要 @JsonCreator
),并且有许多参数(所以不需要 @JsonProperty
)。
然而,对于 TagDto
,只有一个单参数构造函数没有任何注解,因此Jackson将该构造函数分类为类型 #4(上述列表中的类型 #4),而不是类型 #2。
这意味着它期望POJO是一个值类(value-class),其中包含对象的JSON将是 { ..., "tag": "value", ... }
,而不是 { ..., "tag": {"tag": "example"}, ... }
。
要解决这个问题,你需要告诉Jackson该构造函数是一个属性初始化构造函数(#2),而不是值类型构造函数(#4),通过在构造函数参数上指定 @JsonProperty
。
这意味着你不能让Lombok为你创建构造函数:
@Setter
@Getter
public class TagDto {
private String tag;
public TagDto(@JsonProperty("tag") String tag) {
this.tag = tag;
}
}
英文:
TL;DR: Solution is at the end.
Jackson supports multiple ways of creating POJOs. The following lists the most common ways, but it likely not a complete list:
-
Create instance using no-arg constructor, then call setter methods to assign property values.
public class Foo { private int id; public int getId() { return this.id; } @JsonProperty public void setId(int id) { this.id = id; } }
Specifying
@JsonProperty
is optional, but can be used to fine-tune the mappings, together with annotations like@JsonIgnore
,@JsonAnyGetter
, ... -
Create instance using constructor with arguments.
public class Foo { private int id; @JsonCreator public Foo(@JsonProperty("id") int id) { this.id = id; } public int getId() { return this.id; } }
Specifying
@JsonCreator
for the constructor is optional, but I believe it is required if there is more than one constructor. Specifying@JsonProperty
for the parameters is optional, but is required for naming the properties if the parameter names are not included in the class file (-parameters
compiler option).The parameters imply that the properties are required. Optional properties can be set using setter methods.
-
Create instance using factory method.
public class Foo { private int id; @JsonCreator public static Foo create(@JsonProperty("id") int id) { return new Foo(id); } private Foo(int id) { this.id = id; } public int getId() { return this.id; } }
-
Create instance from text value using
String
constructor.public class Foo { private int id; @JsonCreator public Foo(String str) { this.id = Integer.parseInt(id); } public int getId() { return this.id; } @JsonValue public String asJsonValue() { return Integer.toString(this.id); } }
This is useful when a the POJO has a simply text representation, e.g. a
LocalDate
is a POJO with 3 properties (year
,month
,dayOfMonth
), but is generally best serialized as a single string (yyyy-MM-dd
format).@JsonValue
identifies the method to be used during serialization, and@JsonCreator
identifies the constructor/factory-method to be used during deserialization.Note: This can also be used for single-value construction using JSON values other than
String
, but that is very rare.
Ok, that was the background information. What is happening for the examples in the question, it that UserDto
works because there is only one constructor (so @JsonCreator
is not needed), and many arguments (so @JsonProperty
is not needed).
However, for TagDto
there is only a single-argument constructor without any annotations, so Jackson classifies that constructor as a type #4 (from my list above), not a type #2.
Which means that it is expecting the POJO to be a value-class, where the JSON for the enclosing object would be { ..., "tag": "value", ... }
, not { ..., "tag": {"tag": "example"}, ... }
.
To resolve the issue, you need to tell Jackson that the constructor is a property initializing constructor (#2), not a value-type constructor (#4), by specifying @JsonProperty
on the constructor argument.
This means that you cannot have Lombok create the constructor for you:
@Setter
@Getter
public class TagDto {
private String tag;
public TagDto(@JsonProperty("tag") String tag) {
this.tag = tag;
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论