DEV Community

Panos Sideris
Panos Sideris

Posted on • Updated on

Java Records in Spring Boot Rest API

Introduction to Records

Java 14 was released in March 2020 and one of the features that drew our attention was Records. Let's start by explaining what a Record is and then proceed with the potential use cases.

Note: Java 17 is about to be released (September 2021) which is the first LTS version with records support.

From Openjdk JEP359:

Records provide a compact syntax for declaring classes which are transparent holders for shallowly immutable data.

This feature attempts to reduce the amount of boilerplate code in our applications as you only have to define the fields (equivalent to class fields). Records provide a public constructor (with all arguments), read methods for each field (equivalent to getters) and the implementation of hashCode, equals and toString methods.

This is how a Record and Class, for the same data structure, would look like.

Record
public record Person(String name, int age) {
}
Enter fullscreen mode Exit fullscreen mode
Class
public class Person {
  private String name;
  private int age;

  public Person(String name, int age) {
    ...
  }

  public boolean equals(Object o) {
    ...
  }

  public int hashCode() {
    ...
  }

  public String toString() {
    ...
  }
}
Enter fullscreen mode Exit fullscreen mode

As you will notice this is significantly less code given the class' utility.

Usages

So where could the Records be used then? One potential application is in rest controllers. Sometimes it can be quite frustrating having to create a class only to be used as a return type or a request body in a rest controller.

In the next section we are going to demonstrate how to build a spring boot rest api with Records.

Demo

The code for this demo could be found here:

GitHub logo psideris89 / java-records-demo

Demo spring boot rest api for java 14 records demonstration

Step 1: Create Spring Boot app

You can use spring initialiser (https://start.spring.io/) to create a spring boot project. The configuration for our demo is:

  • Project: Gradle
  • Language: Java 16
  • Spring Boot: 2.5.4
  • Name: movies-demo
  • Dependencies: Spring Web

Everything else could be left with the default value.

Step 2: Project configuration

If you are using an IDE you need to use jdk 16. For Intellij set the project sdk to 16 and the project language level to 16.

intellij

Legacy configuration (java 14)

If you are using java 16 you can skip this section.

To use Records with jdk 14 you have to enable the preview features. You might be prompted to accept some terms if you want to use the preview features, so click accept and proceed.

In addition add the following configuration in build.gradle. There is similar configuration for maven.

compileJava {
    options.compilerArgs += ["--enable-preview"]
}

test {
    jvmArgs(['--enable-preview'])
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Create Rest Controller

Before we create the rest controller we need to define the Records.

Director
public record Director(String name, String surname) {
}
Enter fullscreen mode Exit fullscreen mode
Movie
public record Movie(String id, Director director, Boolean released) {
}
Enter fullscreen mode Exit fullscreen mode

Next create a rest controller with 3 endpoints, one to retrieve all the movies, another to retrieve a movie by id and the last one to add a movie (we don't actually save anything as this is to demonstrate deserialisation).

@RestController
public class MoviesController {

    private List<Movie> movies = List.of(
            new Movie("1", new Director("John", "Wick"), true),
            new Movie("2", new Director("Mary", "Poppins"), false),
            new Movie("3", new Director("Jack", "Sparrow"), true),
            new Movie("4", null, false));

    @GetMapping("/movies")
    public List<Movie> getMovies() {
        return movies;
    }

    @GetMapping("/movies/{id}")
    public Movie getMovie(@PathVariable String id) {
        return movies
                .stream()
                .filter(s -> s.id().equals(id))
                .findFirst()
                .orElseThrow(() -> new RuntimeException("Not found"));
    }

    @PostMapping("/movies")
    public Movie addMovie(@RequestBody Movie movie) {
        // Save the movie, be careful as List.of() is immutable
        return movie;
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Configure Jackson for Serialisation / Deserialisation

Start the application and call the first endpoint, localhost:8080/movies. The following error is produced as the objects cannot be serialised.

Exception

The solution to that is to use Jackson annotations and in specific JsonProperty annotation.

public record Director(@JsonProperty("name")String name, @JsonProperty("surname")String surname) {
}

public record Movie(@JsonProperty("id") String id, @JsonProperty("director") Director director, @JsonProperty("released") Boolean released) {
}
Enter fullscreen mode Exit fullscreen mode

Restart the application and you will be able to retrieve the list of movies.

The getMovie endpoint (localhost:8080/movies/1) uses the id() method which is provided by the Record. That way we can compare the id of each movie with the id provided in the api call.

Finally the addMovie endpoint is just a mock endpoint but it proves that deserialisation is also working. To test that you need to do a POST request to localhost:8080/movies with a valid body.

{
    "id": "10",
    "director": {
        "name": "Jim",
        "surname": "White"
    },
    "released": false
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

What could Records mean for the future of java? Maybe they are not meant to be used that way, but in general that feature is something that a lot of java developers anticipated. The fact that Records are immutable is adding a massive value. All the above in combination with the reduction of the boilerplate code make Records a promising feature.

Oldest comments (6)

Collapse
 
omrico profile image
omrico

So with older Java I'd use Lombok to eliminate the boiler plate code. I do like the option of @AllArgsConstructor and @NoArgsConstructor which is missing from this Record class.

Collapse
 
stoussaint profile image
Stéphane Toussaint

Thank you for this introduction !

I'm able to build the project by setting the IDE as you tell, but I can't get the projet running. Whenever I start "bootRun", I get the following exception :

LinkageError com.psideris.moviesapi.MoviesApiApplication
java.lang.UnsupportedClassVersionError: Preview features are not enabled for com/psideris/moviesapi/MoviesApiApplication (class file version 58.65535). Try running with '--enable-preview'

The --enable-preview is set for compile and test in build.gradle. Is there something else to do to run the project with this param ? I can't figure out how.

Collapse
 
stoussaint profile image
Stéphane Toussaint

Ok I get it finally. Need to add

bootRun {
    jvmArgs(['--enable-preview'])
}
Collapse
 
tobq profile image
Tobi • Edited

Would be so much better if you didn't need the Jackson annotations (and it worked automatically like with normal classes). A lot of duplication / boilerplate

Collapse
 
psideris89 profile image
Panos Sideris

That would be nice indeed. For me it seems a fair trade, having to add annotations instead of declaring a whole class however the most significant benefit it's not the amount of code added or reduced but the immutability the Records offer.

Collapse
 
tobq profile image
Tobi • Edited

It seems like something the devs will eventually get working without annotations, as records are so new (still a preview) . Indeed, the immutability is a great bonus / pattern.