I’m developing an application with Spring Boot and facing an issue using MapStruct to map a DTO (RegisterRequest) to an entity (Usuario) and save the data in a PostgreSQL database.
My goal is for only the password field to be encrypted with PasswordEncoder (using BCrypt), while the other fields (firstName, lastName, email, role) should be mapped directly from the DTO.
However, the implementation generated by MapStruct is encrypting all the fields, which causes a length error:
value too long for type character varying(30)
on the firstName column, which has a 30-character limit, while the generated hashes are approximately 60 characters long.
What I want to do
To avoid doing the mapping manually, I wanted to use MapStruct (as I had done before with other mappings) to delegate this work and reduce boilerplate code. The flow I needed was:
Receive a POST request with a RegisterRequest containing email, password, firstName, and lastName.
Map these fields to a Usuario entity using MapStruct.
Encrypt only the password field using PasswordEncoder.
Assign a constant value "User" to the role field.
Save the Usuario in the database without any length errors.
Relevant Code
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface UsuarioRegisterMapper {
@Mapping(target = "rol", constant = "User")
@Mapping(target = "id", ignore = true)
@Mapping(target = "contrasena", expression = "java(encodePassword(request.contrasena(), passwordEncoder))")
Usuario toEntity(RegisterRequest request, @Context PasswordEncoder passwordEncoder);
default String encodePassword(String rawPassword, @Context PasswordEncoder passwordEncoder) {
return passwordEncoder.encode(rawPassword);
}
}
the implementation automatically generated by MapStruct:
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2025-07-14T21:19:15-0600",
comments = "version: 1.5.5.Final, compiler: javac, environment: Java 23.0.2 (Oracle Corporation)"
)
@Component
public class UsuarioRegisterMapperImpl implements UsuarioRegisterMapper {
@Override
public Usuario toEntity(RegisterRequest request, PasswordEncoder passwordEncoder) {
if (request == null) {
return null;
}
Usuario usuario = new Usuario();
usuario.setNombre(encodePassword(request.nombre(), passwordEncoder));
usuario.setApellido(encodePassword(request.apellido(), passwordEncoder));
usuario.setEmail(encodePassword(request.email(), passwordEncoder));
usuario.setRol(encodePassword("User", passwordEncoder));
usuario.setContrasena(encodePassword(request.contrasena(), passwordEncoder));
return usuario;
}
}
package com.proyectoUno.security.dto;
public record RegisterRequest(
String email,
String contrasena,
String nombre,
String apellido
) {}
@Service
public class AuthService {
// ... (injections)
public AuthResponse register(RegisterRequest request) {
if (usuarioRepository.findByEmail(request.email()).isPresent()) {
throw new EntidadDuplicadaException("Email is already associated with an account", "email", Collections.singletonList(request.email()));
}
// This is where I use the mapper to skip the manual process of instantiating the class and calling .set for each value
Usuario usuario = usuarioRegisterMapper.toEntity(request, this.passwordEncoder);
System.out.println("Usuario before saving: " + usuario);
usuarioRepository.save(usuario);
UserDetails userDetails = new CustomUserDetails(usuario);
String jwtToken = jwtService.generateAccessToken(userDetails);
String refreshToken = jwtService.generateRefreshToken(userDetails);
return new AuthResponse(jwtToken, refreshToken);
}
// ...
}
Postman:
{
"email": "mario.rodriguez@gmail.com",
"contrasena": "soyMario1050",
"nombre": "Mario",
"apellido": "Rodriguez"
}
After sending the request from Postman, the bug appeared. To verify it was caused by the Mapper configuration, I did some quick debugging to check the mapper’s output.
Usuario before saving: Usuario{id=null,
nombre='$2a$10$SKiTvYYJK.vZPetwOtY1OOBMCz6m15.bSUCZzk67Q..Ybs0h0n6nu',
apellido='$2a$10$h/PjGAv8aF7sGdCMo7jK/.CaHfcS.e1bHGIM28bb/RIYd/t1CL0jy',
email='$2a$10$ookM1PA26edcSu0mt4FnZegsvg/Cm3S0zdp5aRmfY/e1pcJ7TqT8K',
contrasena='$2a$10$aMIQJAo/pX7TEWhbKwtj/O0x/yuy8eqkfMVBfY7..fmnnmcwLo1e.',
rol='$2a$10$WV1kZvIx.j/UZfCSmcPMoOvOMFXXUM.qDKIarD5rIOLAHThULlUKK',
fechaRegistro=null, activo=false, prestamo=[]}
And that’s when the issue became clear: the mapper wasn’t just handling the password field — it was applying the default method to all fields, even though it was only supposed to be used for the password.
What I’ve tried
I added explicit mappings in UsuarioRegisterMapper for nombre, apellido, and email (like @Mapping(target = "nombre", source = "request.nombre")), but the generated implementation still applies encodePassword to all fields.
I cleaned and recompiled the project with mvn clean install, but the problem persists.
I verified that the usuario table has the correct column lengths (e.g., nombre as varchar(30)), but the generated hashes exceed this limit.
Question
Why is MapStruct encrypting all fields with PasswordEncoder instead of limiting it to the password field?
How can I fix the configuration or implementation so that only password is encrypted and the other fields are mapped directly?
Is there a bug in MapStruct version 1.5.5.Final with Java 23.0.2?
Any help or suggestions would be greatly appreciated. Thanks in advance.
The reason why everything gets mapped is due to the fact that MapStruct sees encodePassword and thinks that it can be applied to all String -> String mappings.
The best way to avoid this is to use @Named, which would mark the method as a qualified method and you have to explicitly pick that in order to be used. That would look like:
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface UsuarioRegisterMapper {
@Mapping(target = "rol", constant = "User")
@Mapping(target = "id", ignore = true)
@Mapping(target = "contrasena", qualifiedByName = "encode")
Usuario toEntity(RegisterRequest request, @Context PasswordEncoder passwordEncoder);
@Named("encode")
default String encodePassword(String rawPassword, @Context PasswordEncoder passwordEncoder) {
return passwordEncoder.encode(rawPassword);
}
}