The end result of the code developed in this document can be found in the GitHub monorepo springboot-demo-projects, under the tag mapping.
Why Mapping Is Here
Mapping isn't technically a cross-cutting concern. It's not a filter, aspect,
or interceptor. But you'll encounter it so often in real projects that it
deserves early setup with a library alongside your other infrastructure
decisions. That's why we cover it here.
In the first endpoint document, FilmRestMapper was a hand-written class that converted a Film domain model into a FilmResponse DTO field by field. That works for seven fields. It stops being acceptable when you have dozens of fields, nested objects, and type conversions to manage, and it never generates errors at compile time when a new field is added to the domain but forgotten in the mapper.
This document replaces the hand-written mapper with a dedicated mapping library: MapStruct for Java and Kotlin, ModelMapper for Groovy.
The build file gets new dependencies, a config class is added to wire the mapper library into Spring, and FilmRestMapper is replaced. For Groovy, the controller also changes because the mapper is no longer a static utility; it's a Spring-managed bean.
Java and Kotlin use MapStruct with its Spring extensions. The extensions register every @Mapper as a Spring ConversionService converter automatically, which removes the need to inject individual mappers into classes that need them. Kotlin additionally requires the kapt plugin because MapStruct generates code at compile time through annotation processing, and kapt is Kotlin's annotation processor runner.
Groovy uses ModelMapper, a reflection-based library. Unlike MapStruct, it requires no annotation processing. It works entirely at runtime, matching fields by name and type.
Java and Kotlin declare MapperSpringConfig, an interface annotated with @MapperConfig and @SpringMapperConfig. This tells MapStruct two things: use "spring" as the component model (so generated mappers become Spring beans), and where to place the generated ConversionServiceAdapter that registers all converters with Spring's ConversionService.
Groovy declares ModelMapperConfig, a standard @Configuration class that exposes a ModelMapper bean. ModelMapper is stateless and thread-safe, so a single instance shared across the application is the right choice.
Java and Kotlin now declare FilmRestMapper as an interface. The @Mapper(config = MapperSpringConfig.class) annotation tells MapStruct to generate a Spring-managed implementation at compile time. Extending Converter<Film, FilmResponse> integrates the mapper into Spring's ConversionService, so anywhere Spring knows how to convert types, this mapper will be picked up automatically.
The @Nullable annotation on the Java version is a jspecify nullability annotation. MapStruct respects it and generates a null-safe implementation. If the source is null, the converter returns null without a NullPointerException.
Groovy replaces the static utility class with a Spring @Component. It receives the ModelMapper bean through constructor injection and delegates conversion to it. ModelMapper.map(source, FilmResponse) inspects both classes at runtime and copies matching fields by name and type.
The original Groovy controller imported FilmRestMapper.convert as a static method. Now that FilmRestMapper is a Spring bean, the controller needs to receive it through injection instead.
Java and Kotlin controllers don't need updating. They already injected FilmRestMapper as a field. That injection still works the same way, just against the MapStruct-generated implementation instead of the hand-written class.