Jackson 什么时候需要无参数构造函数来进行反序列化?

ayf*_*kly 20 java json jackson lombok spring-boot

在我的春季启动项目中,我注意到杰克逊的一个奇怪的行为。我在互联网上搜索,找到了该怎么做,但还没有找到原因

用户D至:

@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;
}
Run Code Online (Sandbox Code Playgroud)

添加新用户效果很好。

标签D至:

@Setter
@Getter
@AllArgsConstructor
public class TagDto {

    private String tag;
}
Run Code Online (Sandbox Code Playgroud)

尝试添加新标签会出现错误:

com.fasterxml.jackson.databind.exc.MismatchedInputException:无法构造 TagDto 的实例(尽管至少存在一个 Creator):无法从对象值反序列化(没有基于委托或属性的 Creator)

该问题的解决方案是向 TagDto 类添加零参数构造函数。

为什么 Jackson 在 TagDto 中需要无参数构造函数进行反序列化,而在 UserDto 中却可以正常工作?

使用相同的方法添加两者。我的标签和用户实体均注释为

@Entity
@Setter
@Getter
@NoArgsConstructor
Run Code Online (Sandbox Code Playgroud)

并具有所有参数构造函数:

@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;
}
Run Code Online (Sandbox Code Playgroud)

实体生成:

    @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());
    }

Run Code Online (Sandbox Code Playgroud)

解析JSON时出现错误

{"tag":"example"}
Run Code Online (Sandbox Code Playgroud)

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

{
    "timestamp": "2020-09-26T18:50:39.974+00:00",
    "status": 400,
    "error": "Bad Request",
    "message": "",
    "path": "/tag/add"
}
Run Code Online (Sandbox Code Playgroud)

我正在使用 Lombok v1.18.12 和 Spring boot 2.3.3.RELEASE 以及 Jackson v2.11.2。

And*_*eas 29

TL;DR:解决方案在最后。

Jackson 支持多种创建 POJO 的方法。下面列出了最常见的方法,但可能不是完整的列表:

  1. 使用无参数构造函数创建实例,然后调用 setter 方法来分配属性值。

    public class Foo {
        private int id;
    
        public int getId() { return this.id; }
    
        @JsonProperty
        public void setId(int id) { this.id = id; }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    指定是可选的,但可以与诸如, , ...@JsonProperty之类的注释一起微调映射。@JsonIgnore@JsonAnyGetter

  2. 使用带参数的构造函数创建实例。

    public class Foo {
        private int id;
    
        @JsonCreator
        public Foo(@JsonProperty("id") int id) {
            this.id = id;
        }
    
        public int getId() {
            return this.id;
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    指定@JsonCreator构造函数是可选的,但我相信如果有多个构造函数,则这是必需的。指定@JsonProperty参数是可选的,但如果参数名称未包含在类文件中(编译器选项),则需要命名属性-parameters

    这些参数意味着属性是必需的。可以使用 setter 方法设置可选属性。

  3. 使用工厂方法创建实例。

    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;
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)
  4. 使用构造函数从文本值创建实例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);
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    当 POJO 具有简单的文本表示形式时,这很有用,例如 a是具有 3 个属性( 、、 )LocalDate的 POJO ,但通常最好序列化为单个字符串(格式)。标识序列化期间要使用的方法,并标识反序列化期间要使用的构造函数/工厂方法。yearmonthdayOfMonthyyyy-MM-dd@JsonValue@JsonCreator

    注意:这也可以用于使用 以外的 JSON 值进行单值构造String,但这种情况非常罕见。

好的,这就是背景信息。问题中的示例发生了什么,它之所以UserDto有效,是因为只有一个构造函数(因此@JsonCreator不需要)和许多参数(因此@JsonProperty不需要)。

但是,由于TagDto只有一个没有任何注释的单参数构造函数,因此 Jackson 将该构造函数归类为类型 #4(来自我上面的列表),而不是类型 #2。

这意味着它期望 POJO 是一个值类,其中封闭对象的 JSON 是{ ..., "tag": "value", ... },而不是{ ..., "tag": {"tag": "example"}, ... }

@JsonProperty要解决此问题,您需要通过在构造函数参数上指定来告诉 Jackson 该构造函数是属性初始化构造函数 (#2),而不是值类型构造函数 (#4) 。

这意味着您不能让 Lombok 为您创建构造函数:

@Setter
@Getter
public class TagDto {

    private String tag;

    public TagDto(@JsonProperty("tag") String tag) {
        this.tag = tag;
    }
}
Run Code Online (Sandbox Code Playgroud)