Skip to content
Spring Boot sb dto 3 min read

ModelMapper

ModelMapper is a runtime, reflection-based object mapper. Instead of generating code at compile time, it inspects objects at runtime and copies matching properties using an intelligent matching strategy. The appeal is minimal boilerplate: one dependency, one @Bean, and a single map() call handles most conversions with zero configuration.

Adding the dependency

ModelMapper is a single library with no annotation processor.

<dependency>
    <groupId>org.modelmapper</groupId>
    <artifactId>modelmapper</artifactId>
    <version>3.2.2</version>
</dependency>

Configuring a ModelMapper bean

ModelMapper is thread-safe, so register one shared instance as a @Bean and inject it everywhere.

@Configuration
public class MappingConfig {

    @Bean
    public ModelMapper modelMapper() {
        ModelMapper modelMapper = new ModelMapper();
        modelMapper.getConfiguration()
                .setMatchingStrategy(MatchingStrategies.STRICT)
                .setFieldMatchingEnabled(true)
                .setSkipNullEnabled(true);
        return modelMapper;
    }
}
  • STRICT matching only maps properties whose names match precisely — far safer than the default STANDARD, which can guess wrong on similarly named fields.
  • skipNull is handy for PATCH: nulls in the source won’t overwrite existing values.

Warning: The default STANDARD matching strategy is loose and occasionally maps the wrong fields (e.g. billingCity into shippingCity). Always set STRICT for predictable behaviour.

Basic map() usage

The core API is map(source, TargetType.class). ModelMapper instantiates the target and copies matching properties.

@Service
@RequiredArgsConstructor
public class ProductService {

    private final ProductRepository repository;
    private final ModelMapper modelMapper;

    @Transactional(readOnly = true)
    public ProductResponse findById(Long id) {
        Product product = repository.findById(id)
                .orElseThrow(() -> new ProductNotFoundException(id));
        return modelMapper.map(product, ProductResponse.class);
    }
}

Map a collection with a stream:

List<ProductResponse> dtos = repository.findAll().stream()
        .map(p -> modelMapper.map(p, ProductResponse.class))
        .toList();

Note: ModelMapper maps best into mutable classes with a no-arg constructor and setters. Immutable records need extra configuration, which is one reason MapStruct pairs more naturally with records.

Custom type maps

When names don’t line up — for example flattening category.name into categoryName — define an explicit TypeMap once, typically right after creating the bean.

@Bean
public ModelMapper modelMapper() {
    ModelMapper modelMapper = new ModelMapper();
    modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);

    modelMapper.typeMap(Product.class, ProductResponse.class)
            .addMappings(mapper -> mapper.map(
                    src -> src.getCategory().getName(),
                    ProductResponse::setCategoryName));

    return modelMapper;
}

For conversions ModelMapper can’t infer (string formatting, enum translation), use a Converter:

Converter<BigDecimal, String> priceFormatter =
        ctx -> "$" + ctx.getSource().toPlainString();

modelMapper.typeMap(Product.class, ProductResponse.class)
        .addMappings(m -> m.using(priceFormatter)
                .map(Product::getPrice, ProductResponse::setFormattedPrice));

Runtime vs compile-time tradeoffs vs MapStruct

ModelMapper and MapStruct solve the same problem from opposite ends.

AspectModelMapper (runtime)MapStruct (compile-time)
Mapping resolvedAt runtime via reflectionAt compile time via generated code
SetupOne dependency + a @BeanDependency + annotation processor
BoilerplateLowest — often zero configLow — interface + @Mapping
PerformanceSlower (reflection per call)Native field-copy speed
Mismatch detectedAt runtime, or silently skippedAt compile time (build fails)
Refactor safetyRename can break mapping unnoticedCompiler flags it immediately
Records / immutabilityAwkward, needs configFirst-class support
DebuggabilityThrough opaque internalsStep through generated code
Best forQuick wins, prototypes, small appsLarger apps, hot paths, strict contracts

Tip: Use ModelMapper when speed of development matters more than raw throughput and your DTOs are simple mutable classes. Reach for MapStruct when you want compile-time safety, top performance, or you’re standardising on records.

The biggest practical difference is when you find out a mapping is wrong. MapStruct tells you at build time; ModelMapper tells you in production — or never, if it silently skips an unmatched field. On a critical path, that distinction usually decides the choice.

Last updated June 13, 2026
Was this helpful?