using java records as DTOs

27. August 2023

When generating interfaces and schema classes (dtos) from an OpenAPI, wouldn’t it be nice to use java record s instead of plain pojos?

This would result in less code, immutable fields and a couple of auto-generated methods: getters, setters, equals(), hashCode() and toString().

openapi-processor-spring/micronaut release 2023.3 is able to generate records instead of pojos from OpenAPI schemas.

This is possible because openapi-processor doesn’t generate inheritance hierarchies. This would be a problem with records because records do not support the extends keyword.

openapi-processor does support allOf by merging all item properties into a single object, and it can add a marker interface to oneOf items to use a named type in the api interfaces instead of Object.

Both ways work with records!

a little bit of OpenAPI

Here is a simple OpenAPI snippet that defines a schema:

components:
  schemas:
    Foo:
      type: object
      properties:
        foo:
          type: string
        id:
          type: string
          format: uuid

OpenAPI as pojo

with its default settings openapi-processor will generate a pojo dto class with some getter/setter noise.

Foo.java pojo dto
package io.openapiprocessor.openapi.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.openapiprocessor.openapi.support.Generated;
import java.util.UUID;

@Generated(value = "openapi-processor-spring", version = "2023.3")
public class Foo {

    @JsonProperty("foo")
    private String foo;

    @JsonProperty("id")
    private UUID id;

    public String getFoo() {
        return foo;
    }

    public void setFoo(String foo) {
        this.foo = foo;
    }

    public UUID getId() {
        return id;
    }

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

It is probably not a big issue because you won’t look at it very often. :-)

a small piece of configuration

It may still be useful to use java records because they are immutable and provide equals() plus hasCode() out of the box.

The generated pojos are not immutable, and they don’t offer equals() or hashCode().

We could add it via annotation mapping and lombok. How this works is described in object annotation mapping & lombok.

But we could avoid lombok for this and simply use the built-in records.

The only thing we have to do to get records is to enable it in the options section of the mapping.yaml:

openapi-processor-mapping: v4

options:
  package-name: io.openapiprocessor.openapi
  model-type: record (1)

map:
  types:
    - type: string:uuid => java.util.UUID
1 that is the interesting line that switches generation of pojos to records.

If model-type is not given the processor will generate pojos.

This is a global setting, i.e. either all schemas are generated as pojo or all schemas are generated as record.

OpenAPI as records

After switching to records the generated Foo.java will look like this:

Foo.java record dto
package io.openapiprocessor.openapi.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.openapiprocessor.openapi.support.Generated;
import java.util.UUID;

@Generated(value = "openapi-processor-spring", version = "2023.3")
public record Foo(
    @JsonProperty("foo")
    String foo,

    @JsonProperty("id")
    UUID id
) {}

less code, the same properties, the same annotations, no explicit getter/setters.

Now we have to find out how well this works in real code.. :-)

summary

This article shows how openapi-processor can generate java records instead of pojos.

To learn more about openapi-processor and how to generate controller interfaces and dto classes from an OpenAPI description take a look at the documentation.