# Java Records Deserve a Mapper Built for Them

> Source: <https://dev.to/dinuka_karunarathna/java-records-deserve-a-mapper-built-for-them-302e>
> Published: 2026-05-24 01:33:30+00:00

Java Records have been stable since Java 16, and with Java 21 now the LTS baseline, they're showing up everywhere - DTOs, value objects, domain models. Immutable by design, concise, and semantically clear.

But here's the gap nobody talks about: **every object mapper in the Java ecosystem was built before Records existed**. They were designed around JavaBeans - mutable objects with getters, setters, and no-arg constructors. Records have none of that. So what happens? These libraries bolt on partial Record support as an afterthought, and the seams show.

I built [Immuto](https://github.com/karunarathnad/immuto) to fill that gap.

## The problem with retrofitted Record support

A Record's identity is its canonical constructor:

```
public record PersonDTO(Long id, String fullName, String email) {}
```

That constructor is the *only* way to create a `PersonDTO`

. There are no setters. There is no builder unless you write one yourself. The component accessors are read-only.

Existing mappers were not designed with this in mind. To work with Records, they either:

- Generate setter calls that don't exist (and fail at runtime)
- Require you to write a mutable builder as a workaround
- Fall back to reflection on private fields - bypassing the canonical constructor entirely

These are runtime failures. You don't know something is wrong until you run the code.

## What Immuto does differently

Immuto is an annotation processor - it runs during `mvn compile`

, the same way Lombok and the APT-based approach work. It generates plain `.java`

source files that call your record's **canonical constructor directly**. No reflection. No setters. No runtime surprises.

```
@RecordMapper
public interface PersonMapper {

    @Mapping(target = "fullName",
             expression = "java(source.firstName() + \" \" + source.lastName())")
    PersonDTO toDto(PersonEntity source);

    @InheritInverseConfiguration(name = "toDto")
    PersonEntity toEntity(PersonDTO source);
}
```

After `mvn compile`

, Immuto writes `PersonMapperImpl.java`

into `target/generated-sources`

. It looks exactly like code you'd write by hand:

```
@Generated("io.github.karunarathnad.immuto.processor.RecordMapperProcessor")
public final class PersonMapperImpl implements PersonMapper, ImmutoMapper {

    @Override
    public PersonDTO toDto(PersonEntity source) {
        if (source == null) return null;
        return new PersonDTO(
            source.id(),
            source.firstName() + " " + source.lastName(),
            source.email()
        );
    }
}
```

Canonical constructor. Always. That's the contract Immuto enforces.

## Compile-time validation

If a record component can't be mapped, the **build fails** - not at runtime, not in a test, but during compilation.

- Unmapped component → build error
- Type mismatch with no registered converter → build error
-
`@RecordMapper`

on a class instead of an interface → build error

This is the behaviour Records deserve. They were designed to be explicit and safe; your mapper should be too.

## Key features

**Nested records** - mapped recursively by matching component names. Use `@Mapping(expression=...)`

for asymmetric nesting.

**Bidirectional mapping** via `@InheritInverseConfiguration`

- define `toDto`

, get `toEntity`

for free.

** @NullSafe** - wraps the result in

`Optional.ofNullable(...)`

at the call site:

```
@NullSafe
Optional<AddressDTO> toAddressDto(AddressEntity entity);
```

**Sealed class support** - Immuto understands sealed hierarchies, something no existing mapper handles.

**Lifecycle hooks** - `@BeforeMapping`

and `@AfterMapping`

methods are inlined into the generated code. No AOP, no proxy.

**Custom type converters**:

```
@Named("isoDate")
public class IsoDateConverter implements TypeConverter<LocalDate, String> {
    @Override
    public String convert(LocalDate source, MappingContext ctx) {
        return source == null ? null : source.toString();
    }
}
```

**Fluent runtime API** - for tests or dynamic environments where APT isn't available:

```
FluentMapper<PersonEntity, PersonDTO> mapper = FluentMapper
    .from(PersonEntity.class)
    .to(PersonDTO.class)
    .override("fullName", p -> p.firstName() + " " + p.lastName())
    .build();
```

Note: `FluentMapper`

does use reflection - it's the explicit opt-in escape hatch, not the default path.

## Getting started

```
<dependency>
    <groupId>io.github.karunarathnad</groupId>
    <artifactId>immuto-annotations</artifactId>
    <version>1.1.0</version>
</dependency>

<dependency>
    <groupId>io.github.karunarathnad</groupId>
    <artifactId>immuto-core</artifactId>
    <version>1.1.0</version>
</dependency>

<dependency>
    <groupId>io.github.karunarathnad</groupId>
    <artifactId>immuto-processor</artifactId>
    <version>1.1.0</version>
    <scope>provided</scope>
</dependency>
```

Add the processor path to the compiler plugin:

```
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <annotationProcessorPaths>
            <path>
                <groupId>io.github.karunarathnad</groupId>
                <artifactId>immuto-processor</artifactId>
                <version>1.1.0</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>
```

Then annotate an interface, run `mvn compile`

, and use it:

```
PersonMapper mapper = Immuto.getMapper(PersonMapper.class);
PersonDTO dto = mapper.toDto(entity);
```

## Why now

Java 21 is the current LTS. Records are not experimental - they're the idiomatic way to model immutable data in modern Java. As more codebases adopt them, the need for tooling that treats them as first-class citizens (not an edge case) grows with it.

Immuto is on Maven Central, Apache 2.0 licensed, and under active development.

### Links

GitHub:

[github.com/karunarathnad/immuto](https://github.com/karunarathnad/immuto)Maven Central:

[https://central.sonatype.com/artifact/io.github.karunarathnad/immuto-core](https://central.sonatype.com/artifact/io.github.karunarathnad/immuto-core)

Feedback, issues, and contributions are very welcome.
