简体   繁体   English

Jackson 何时需要无参数构造函数进行反序列化?

[英]When does Jackson require no-arg constructor for deserialization?

In my spring boot project, I noticed a strange Jackson behavior.在我的 Spring Boot 项目中,我注意到了一个奇怪的 Jackson 行为。 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) com.fasterxml.jackson.databind.exc.MismatchedInputException:无法构造 TagDto 的实例(尽管至少存在一个 Creator):无法从 Object 值反序列化(没有基于委托或属性的 Creator)

The solution to the problem was to add zero-arg constructor to the TagDto class.该问题的解决方案是在 TagDto 类中添加零参数构造函数。

Why does Jackson require no-arg constructor for deserialization in TagDto, while working just fine with UserDto?为什么 Jackson 在 TagDto 中需要无参数构造函数进行反序列化,而与 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:并拥有所有 args 构造函数:

@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解析JSON时出错

{"tag":"example"}

sent via postman localhost:8081/tag/add, returns通过邮递员发送 localhost:8081/tag/add,返回

{
    "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.我正在使用 Lombok v1.18.12 和 Spring boot 2.3.3.RELEASE 和 Jackson v2.11.2。

TL;DR: Solution is at the end. TL;DR:解决方案在最后。

Jackson supports multiple ways of creating POJOs. Jackson 支持多种创建 POJO 的方式。 The following lists the most common ways, but it likely not a complete list:下面列出了最常见的方法,但可能不是完整的列表:

  1. Create instance using no-arg constructor, then call setter methods to assign property values.使用无参数构造函数创建实例,然后调用 setter 方法来分配属性值。

     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 , ...指定@JsonProperty是可选的,但可用于微调映射,以及@JsonIgnore@JsonAnyGetter等注释。

  2. 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.为构造函数指定@JsonCreator是可选的,但我相信如果有多个构造函数,则这是必需的。 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).为参数指定@JsonProperty是可选的,但如果类文件中不包含参数名称( -parameters编译器选项),则命名属性时需要使用@JsonProperty

    The parameters imply that the properties are required.参数暗示属性是必需的。 Optional properties can be set using setter methods.可以使用 setter 方法设置可选属性。

  3. 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; } }
  4. Create instance from text value using String constructor.使用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); } }

    This is useful when a the POJO has a simply text representation, eg a LocalDate is a POJO with 3 properties ( year , month , dayOfMonth ), but is generally best serialized as a single string ( yyyy-MM-dd format).当 POJO 具有简单的文本表示时,这很有用,例如LocalDate是具有 3 个属性( yearmonthdayOfMonth )的 POJO,但通常最好将其序列化为单个字符串( yyyy-MM-dd格式)。 @JsonValue identifies the method to be used during serialization, and @JsonCreator identifies the constructor/factory-method to be used during deserialization. @JsonValue标识序列化过程中使用的方法,并且@JsonCreator识别反序列化过程中使用的构造/工厂方法。

    Note: This can also be used for single-value construction using JSON values other than String , but that is very rare.注意:这也可以用于使用String以外的 JSON 值的单值构造,但这种情况非常少见。

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).问题中的示例发生了什么, UserDto起作用,是因为只有一个构造函数(因此@JsonCreator )和许多参数(因此@JsonProperty )。

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.但是,对于TagDto ,只有一个没有任何注释的单参数构造函数,因此 Jackson 将该构造函数分类为类型 #4(来自我上面的列表),而不是类型 #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"}, ... } .这意味着它期望 POJO 是一个值类,其中封闭对象的 JSON 将是{ ..., "tag": "value", ... } ,而不是{ ..., "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.要解决此问题,您需要通过在构造函数参数上指定@JsonProperty来告诉 Jackson 构造函数是属性初始化构造函数(#2),而不是值类型构造函数(#4)。

This means that you cannot have Lombok create the constructor for you:这意味着您不能让 Lombok 为您创建构造函数:

@Setter
@Getter
public class TagDto {

    private String tag;

    public TagDto(@JsonProperty("tag") String tag) {
        this.tag = tag;
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM