无法在使用新的 Record 类时进行反序列化

huangapple go评论126阅读模式
英文:

Unable to deserialize when using new Record classes

问题

以下是翻译好的部分:

我正在尝试查看是否可以将现有的 Java 14 中的 Pojos 替换为新的 Record 类。但是无法这样做。出现以下错误:

> com.fasterxml.jackson.databind.exc.InvalidDefinitionException: 无法构造 com.a.a.Post 的实例(没有像默认构造函数一样的创建者存在):无法从对象值进行反序列化(没有委托或基于属性的创建者)

我理解这个错误是在说记录没有构造函数,但从我看到的情况来看,记录类在后台处理这个问题,并且相关的 getter 方法也在后台设置(不是真正的 getter,而是没有 get 前缀的 id()、title() 等方法)。是不是因为 Spring 还没有采用最新的 Java 14 记录?请给予建议。谢谢。

我在 Spring Boot 版本 2.2.6 中进行此操作,使用的是 Java 14。

以下代码在使用常规的 POJO 时有效。

public class PostClass {
    private int userId;
    private int id;
    private String title;
    private String body;

    public int getUserId() {
        return userId;
    }

    public void setUserId(int userId) {
        this.userId = userId;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getBody() {
        return body;
    }

    public void setBody(String body) {
        this.body = body;
    }
}

调用 REST 服务的方法,因为我使用了上述的 POJO,所以它现在有效。

public PostClass[] getPosts() throws URISyntaxException {
    String url = "https://jsonplaceholder.typicode.com/posts";
    return template.getForEntity(new URI(url), PostClass[].class).getBody();
}

但是,如果我切换到以下代码,其中我使用了记录(record),我会得到上面的错误。

新的记录类。

public record Post(int userId, int id, String title, String body) {
}

将方法更改为使用记录而不是 POJO,但会失败。

public Post[] getPosts() throws URISyntaxException {
    String url = "https://jsonplaceholder.typicode.com/posts";
    return template.getForEntity(new URI(url), Post[].class).getBody();
}

编辑:

尝试向记录(record) Post 添加构造函数,但仍然出现相同的错误。

public record Post(int userId, int id, String title, String body) {
    public Post {
    }
}

或者

public record Post(int userId, int id, String title, String body) {
    public Post(int userId, int id, String title, String body) {
        this.userId = userId;
        this.id = id;
        this.title = title;
        this.body = body;
    }
}
英文:

I am trying to see if I can replace my existing Pojos with the new Record classes in Java 14. But unable to do so. Getting following error:

> com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot
> construct instance of com.a.a.Post (no Creators, like default
> construct, exist): cannot deserialize from Object value (no delegate-
> or property-based Creator)

I get that the error is saying the record has no constructors, but from what I see the record class takes care of it in the background and relevant getters are also set in the background (not getters exactly but id() title() and so on without the get prefix). Is it cos Spring has not adopted the latest Java 14 record yet? Please advice. Thanks.

I am doing this in Spring Boot version 2.2.6 and using Java 14.

The following works using the usual POJOs.

PostClass

public class PostClass {
    private int userId;
    private int id;
    private String title;
    private String body;

    public int getUserId() {
        return userId;
    }

    public void setUserId(int userId) {
        this.userId = userId;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getBody() {
        return body;
    }

    public void setBody(String body) {
        this.body = body;
    }
}

Method to call rest service which works now as I am using the above POJO.

public PostClass[] getPosts() throws URISyntaxException {
    String url = "https://jsonplaceholder.typicode.com/posts";
    return template.getForEntity(new URI(url), PostClass[].class).getBody();
}

But if I switch to following where I am using record instead, I am getting the above error.

The new record class.

public record Post(int userId, int id, String title, String body) {
}

Changing the method to use the record instead which fails.

public Post[] getPosts() throws URISyntaxException {
    String url = "https://jsonplaceholder.typicode.com/posts";
    return template.getForEntity(new URI(url), Post[].class).getBody();
}

EDIT:

Tried adding constructors as follows to the record Post and same error:

public record Post(int userId, int id, String title, String body) {
    public Post {
    }
}

or

public record Post(int userId, int id, String title, String body) {
    public Post(int userId, int id, String title, String body) {
        this.userId = userId;
        this.id = id;
        this.title = title;
        this.body = body;
    }
}

答案1

得分: 16

以下是翻译好的内容:

使用一些Jackson注解可能会实现这一点,这些注解会导致Jackson使用字段而不是getter方法。仍然比Java 14之前的类要简洁得多(不使用Lombok或类似的解决方案)。

record Foo(@JsonProperty("a") int a, @JsonProperty("b") int b){
}

这可能有效,因为根据 https://openjdk.java.net/jeps/359

如果声明注解适用于记录成员、参数、字段或方法,则允许在记录成员上使用声明注解。
适用于这些目标中的任何目标的声明注解都会传播到任何强制成员的隐式声明。

另请参阅:https://stackoverflow.com/questions/12583638/when-is-the-jsonproperty-property-used-and-what-is-it-used-for

还可以使用 @JsonAutoDetect

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
record Bar(int a, int b){
}

如果全局配置Objectmapper以使用字段可见性,那么类级别上的这个注解是不需要的。

另请参阅:https://stackoverflow.com/questions/7105745/how-to-specify-jackson-to-only-use-fields-preferably-globally

示例:

public class Test {
    public static void main(String[] args) throws JsonProcessingException {
        ObjectMapper om = new ObjectMapper();
        System.out.println(om.writeValueAsString(new Foo(1, 2)));  //{"a":1,"b":2}
        System.out.println(om.writeValueAsString(new Bar(3, 4)));  //{"a":3,"b":4} 
    }

    record Foo(@JsonProperty("a") int a, @JsonProperty("b") int b){
    }

    @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
    record Bar(int a, int b){
    }
}

这个特性还有一个Github问题:https://github.com/FasterXML/jackson-future-ideas/issues/46

英文:

It is possible with some Jackson Annotations, which cause Jackson to use fields instead of getters. Still far less verbose than a pre-Java 14 class (without Lombok or similar solutions).

record Foo(@JsonProperty("a") int a, @JsonProperty("b") int b){
}

This probably works because according to https://openjdk.java.net/jeps/359:

> Declaration annotations are permitted on record components if they are
> applicable to record components, parameters, fields, or methods.
> Declaration annotations that are applicable to any of these targets
> are propagated to implicit declarations of any mandated members.

See also: https://stackoverflow.com/questions/12583638/when-is-the-jsonproperty-property-used-and-what-is-it-used-for

It is also possible to make use @JsonAutoDetect

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
record Bar(int a, int b){
}

If configuring the Objectmapper to use field Visibility globally, this annotation on class level is not needed.

See also: https://stackoverflow.com/questions/7105745/how-to-specify-jackson-to-only-use-fields-preferably-globally

Example:

public class Test {
    public static void main(String[] args) throws JsonProcessingException {
        ObjectMapper om = new ObjectMapper();
        System.out.println(om.writeValueAsString(new Foo(1, 2)));  //{"a":1,"b":2}
        System.out.println(om.writeValueAsString(new Bar(3, 4)));  //{"a":3,"b":4} 
    }

    record Foo(@JsonProperty("a") int a, @JsonProperty("b") int b){
    }

    @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
    record Bar(int a, int b){
    }
}

There is also a Github issue for that feature: https://github.com/FasterXML/jackson-future-ideas/issues/46

答案2

得分: 4

这将在 jackson 2.12 中实施

链接:https://github.com/FasterXML/jackson-future-ideas/issues/46

英文:

This is slated for jackson 2.12

https://github.com/FasterXML/jackson-future-ideas/issues/46

答案3

得分: 1

编译器为记录生成构造函数和其他访问器方法。

在您的情况下,

public final class Post extends java.lang.Record {
    public Post(int, int java.lang.String, java.lang.String);
    public java.lang.String toString();
    public final int hashCode();
    public final boolean equals(java.lang.Object);
    public int userId();
    public int id();
    public java.lang.String title();
    public java.lang.String body();
}

在这里,您可以看到没有默认构造函数,而 Jackson 需要一个默认构造函数。您使用的构造函数是一个紧凑构造函数,

public Post {
}

您可以定义一个默认/无参构造函数,如下所示:

public record Post(int userId, int id, String title, String body) {
    public Post() {
        this(0, 0, null, null);
    }
}

但 Jackson 使用 Getter 和 Setter 来设置值。简而言之,您不能使用 Record 来映射响应。


附加说明:截至 2.12 版本 的发布,Jackson 可以正确地序列化和反序列化记录

英文:

The compiler generates the constructor and other accessor method for a Record.

In your case,

  public final class Post extends java.lang.Record {  
  public Post(int, int java.lang.String, java.lang.String);
  public java.lang.String toString();
  public final int hashCode();
  public final boolean equals(java.lang.Object);
  public int userId();
  public int id();
  public java.lang.String title();
  public java.lang.String body();
}

Here you can see that there is not default constructor which is needed got Jackson. The constructor you used is a compact constructor,

public Post {
 }

You can define a default/no args constructor as,

public record Post(int userId, int id, String title, String body) {
    public Post() {
        this(0,0, null, null);
    }
}

But Jackson uses Getter and Setters to set values. So in short, you can not use Record for mapping the response.


EDIT as PSA: Jackson can properly serialize and deserialize records as of 2.12 which has been released.

答案4

得分: -1

  • 使用参数名称模块来处理 Jackson,https://github.com/FasterXML/jackson-modules-java8/tree/master/parameter-names (确保编译器设置了 -parameters 选项),或者为记录中的每个字段添加 @JsonProperty("name") 注解。
  • 在构造函数上添加 @JsonCreator 注解。我无法确定继承是否能正常工作,所以您可能需要显式声明构造函数并对其进行注解。
    >如果显式声明了公共访问器方法或(非紧凑的)规范构造函数,则它仅具有直接出现在其上的注解;没有从相应的记录组件传播到这些成员的注解。

来自 https://openjdk.java.net/jeps/384

因此添加以下内容:

new ObjectMapper().registerModules(new ParameterNamesModule())

并尝试以下内容:

@JsonCreator record Value(String x);

或类似于以下内容:

record Value(String x) {

@JsonCreator
public Value(String x) {
this.x = x;
}
}

或者一直到如下内容:

record Value(@JsonProperty("x") String x) {

@JsonCreator
public Value(@JsonProperty("x") String x) {
this.x = x;
}
}

这就是我如何让 Lombok 和 Jackson 协同工作以实现不可变的 POJOs,我不明白为什么记录类型(records)不会在相同的格式下工作。我的设置是使用 Jackson 参数名称模块,对于 Java 8,需要设置 -parameters 编译器标志(我认为在 JDK 9+ 中可能不需要此选项),以及在构造函数上使用 @JsonCreator 注解。以下是使用此设置的实际类的示例。

@Value
@AllArgsConstructor(onConstructor_ = @JsonCreator)
public final class Address {

  private final String line1;

  private final String line2;

  private final String city;

  private final String region;

  private final String postalCode;

  private final CountryCode country;
}
英文:
  • Use the parameter names module for jackson, https://github.com/FasterXML/jackson-modules-java8/tree/master/parameter-names (make sure the compiler sets -parameters) or add `@JsonProperty("name") to each field in the record
  • add @JsonCreator to the constructor. I can't tell if the inheritance will work properly, so you might have to explicitly declare the constructor and annotate it.
    >If a public accessor method or (non-compact) canonical constructor is declared explicitly, then it only has the annotations which appear on it directly; nothing is propagated from the corresponding record component to these members.

From https://openjdk.java.net/jeps/384

So add

new ObjectMapper().registerModules(new ParameterNamesModule())

and try

@JsonCreator record Value(String x);

or something like

record Value(String x) {

@JsonCreator
public Value(String x) {
this.x = x;
}
}

or all the way to

record Value(@JsonProperty("x") String x) {

@JsonCreator
public Value(@JsonProperty("x") String x) {
this.x = x;
}
}

This is how I get immutable pojos with lombok and jackson to work, and I don't see why records wouldn't work under the same format. My setup is Jackson parameter names module, -parameters compiler flag for java 8 (I don't think this is required for like jdk9+), @JsonCreator on the constructor. Example of a real class working with this setup.

@Value
@AllArgsConstructor(onConstructor_ = @JsonCreator)
public final class Address {

  private final String line1;

  private final String line2;

  private final String city;

  private final String region;

  private final String postalCode;

  private final CountryCode country;
}

huangapple
  • 本文由 发表于 2020年4月10日 21:03:36
  • 转载请务必保留本文链接:https://go.coder-hub.com/61140857.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定