18

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;
    }
}
Naman
  • 27,789
  • 26
  • 218
  • 353
kar
  • 4,791
  • 12
  • 49
  • 74
  • show the new `Post` class, and by error message i assume you don't have no arg or default constructor – Ryuzaki L Apr 10 '20 at 13:14
  • AFAIK all fields in a record are final, which means it probably doesn't have a no-args/default constructor which is what jackson uses to build objects. See [here](https://openjdk.java.net/jeps/359) – 123 Apr 10 '20 at 13:14
  • @Deadpool The new Post class is mentioned above -> public record Post. – kar Apr 10 '20 at 13:22
  • @123 Tried adding consctructors (see above EDIT portion) and same outcome. – kar Apr 10 '20 at 13:22
  • i m also facing the same - did you find any right solution for this?? – Arpan Sharma May 20 '20 at 12:48

4 Answers4

16

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: 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: 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

user140547
  • 7,750
  • 3
  • 28
  • 80
4

This is slated for jackson 2.12

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

Ashok Koyi
  • 5,327
  • 8
  • 41
  • 50
1

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.

Adam Gent
  • 47,843
  • 23
  • 153
  • 203
Vikas
  • 6,868
  • 4
  • 27
  • 41
  • I've tried the suggested option you mentioned. It throws compilation error saying 'Non-canonical record constructor must delegate to another constructor'. – kar Apr 10 '20 at 13:41
  • I didn’t find a way to add default constructor. Posted a question https://stackoverflow.com/questions/61152337/define-default-constructor-for-record – Vikas Apr 11 '20 at 05:04
  • 1
    So in short, you can not use Record for mapping the response. – Vikas Apr 11 '20 at 06:50
  • Thanks for looking into it. Its just so weird. Like the whole point of Record was to replace code clutter and be used as POJOs afaik. Which would be a nice usage for mapping during rest calls instead of making full classes. Yet this is not usable. – kar Apr 11 '20 at 11:05
  • 3
    Jackson has to incorporate these features. It may come in future releases. – Vikas Apr 11 '20 at 13:13
  • 2
    Record is **preview** for a reason, so we can provide feedback. Perhaps if enough people complain that records don't follow JavaBean naming conventions, they will change it. See e.g. [this answer](https://stackoverflow.com/a/60013628/5221149). – Andreas Apr 25 '20 at 01:35
  • 1
    Unlikely that they will change that. Records are useful as implementation detail, where you previously used `Map.Entry` as tuple. – Johannes Kuhn Apr 25 '20 at 11:52
  • I tried this and I managed to get it working. The problem is that I still would like to have builders... therefore lombok... therefore records are not really saving me that much. Hopefully something nice will come along integrated with java with no extra libraries. – daemon_nio Jul 08 '20 at 22:15
  • And by the way, if your class has only one argument it won't work unless you add a constructor annotated with @JsonCreator: https://stackoverflow.com/questions/41243608/jackson-single-argument-constructor-with-single-argument-fails-with-parameternam – daemon_nio Jul 09 '20 at 22:01
  • The answer below proposing to add @JsonProperty works as expected – antoine Nov 23 '20 at 10:08
-1

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;
}
akfp
  • 42
  • 2