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

Posted

技术标签:

【中文标题】Jackson 啥时候需要无参数构造函数进行反序列化?【英文标题】:When does Jackson require no-arg constructor for deserialization?Jackson 什么时候需要无参数构造函数进行反序列化? 【发布时间】:2021-01-12 18:50:21 【问题描述】:

在我的 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):无法从 Object 值反序列化(没有基于委托或属性的 Creator)

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

为什么 Jackson 需要无参数构造函数在 TagDto 中进行反序列化,而与 UserDto 一起工作得很好?

使用相同的方法添加两者。 我的 Tag 和 User 实体都带有注释

@Entity
@Setter
@Getter
@NoArgsConstructor

并拥有所有 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;

实体生成:

    @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"

通过邮递员 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。

【问题讨论】:

Jackson 默认不会使用带参数的构造函数,您需要通过 @JsonCreator 注释告诉它这样做。默认情况下,它会尝试使用您的类中不存在的无参数构造函数。 @Thomas 那么 UserDto 如何正常工作? 这是个好问题。我不是 Lombox 专家,所以我不得不猜测,但一个原因可能是 TagDto 只有一个可能导致问题的属性。有时这些库存在或导致错误,例如 this one,因此最好提供有关您正在使用的版本的更多信息。 你能添加完整的代码吗? @Entity 类的外观如何?你到底是怎么存储的? Jackson 可能会在适当的时候尝试使用 AllArgsConstructor。杰克逊可能对一个实体使用所有参数,而对另一个实体使用无参数。这取决于您如何实例化这些 DTO。尝试删除 Lombok 注释并提供您自己的构造函数。然后在其中添加一些打印语句,然后自己查看-称为-when。 我仍然看不到如何您将实体转换为 DTO,您如何添加它们。请提供minimal reproducible example,并告诉我们——您究竟何时/何地有错误?当您转换为 DTO 时,很可能,对吧?此外,您的 User 课程缺少某些部分。 【参考方案1】:

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 个属性(yearmonthdayOfMonth)的 POJO,但通常最好序列化为单个字符串(yyyy-MM-dd 格式)。 @JsonValue 标识要在序列化期间使用的方法,@JsonCreator 标识要在反序列化期间使用的构造函数/工厂方法。

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

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

然而,对于TagDto,只有一个 single-argument 构造函数没有任何注释,因此 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;
    

【讨论】:

以上是关于Jackson 啥时候需要无参数构造函数进行反序列化?的主要内容,如果未能解决你的问题,请参考以下文章

Jackson MismatchedInputException(没有从字符串值反序列化的字符串参数构造函数/工厂方法)

com.fasterxml.jackson.databind.JsonMappingException 没有从字符串值('1')反序列化的字符串参数构造函数/工厂方法

Jackson@JsonCreator 注解

有没有办法让以前公开的无参数构造函数私有化,而不会对 protobuf(反)序列化进行重大更改?

使用 Jackson 反序列化 JSON - 为啥 JsonMappingException“没有合适的构造函数”?

Protobuf-Net 无法在没有无参数构造函数的情况下反序列化记录