Mappers
MapStruct compile-time mapping — BaseMapper contract, KeycloakRoleMapper, VaultResponseMapper, and how annotation processor ordering is configured.
Overview
The admin-service uses MapStruct 1.5.5.Final for compile-time, type-safe DTO ↔ entity mapping. All generated mapper implementations are Spring beans (componentModel = "spring") and are injected like any other @Component.
There are two mappers in the service:
| Mapper | Entity (E) | DTO (D) | Source |
|---|---|---|---|
KeycloakRoleMapper | RoleRepresentation (Keycloak Admin Client) | KeycloakRole (local record) | org.arya.banking.admin.mapper |
VaultResponseMapper | VaultResponse (Spring Vault) | VaultApiResponseDto (local record) | org.arya.banking.admin.mapper |
Both extend BaseMapper<E, D> from arya-banking-common.
BaseMapper Contract
Defined in arya-banking-common, this interface establishes the four-method contract that all mappers in the platform must implement:
public interface BaseMapper<E, D> {
D toDto(E entity);
E toEntity(D dto);
List<D> toDtoList(List<E> entityList);
List<E> toEntityList(List<D> dtoList);
}
MapStruct generates all four implementations automatically when a mapper interface extends BaseMapper and declares its type parameters. The list methods are generated by delegating to toDto and toEntity element-by-element.
KeycloakRoleMapper
File: org.arya.banking.admin.mapper.KeycloakRoleMapper
Maps between Keycloak's RoleRepresentation (from keycloak-admin-client) and the admin-service's local KeycloakRole Java record.
@Mapper(componentModel = "spring")
public interface KeycloakRoleMapper
extends BaseMapper<RoleRepresentation, KeycloakRole> {
KeycloakRole toDto(RoleRepresentation roleRepresentation);
List<KeycloakRole> toDtoList(List<RoleRepresentation> roleRepresentations);
}
Mapped Fields
RoleRepresentation Field | KeycloakRole Record Field | Notes |
|---|---|---|
id | (not mapped) | Internal Keycloak UUID, not exposed |
name | name | Role identifier string, e.g. ROLE_ADMIN |
description | description | Human-readable description |
composite | composite | true if the role is composed of other roles |
composites | composites | Nested Composites object (sub-roles) |
attributes | attributes | Map<String, List<String>> of custom attributes |
Usage in KeyCloakServiceImpl
// Read path — convert from Keycloak model to local DTO
public List<KeycloakRole> getRealmRoles() {
return keycloakRoleMapper.toDtoList(
keyCloakManager.getRealmRoles().list()
);
}
// Write path — convert from local DTO back to Keycloak model
public KeyCloakResponse createRealmRole(KeycloakRole keycloakRole) {
RoleRepresentation role = keycloakRoleMapper.toEntity(keycloakRole);
keyCloakManager.getKeyCloakInstanceWithRealm().roles().create(role);
...
}
VaultResponseMapper
File: org.arya.banking.admin.mapper.VaultResponseMapper
Maps Spring Vault's low-level VaultResponse object to the admin-service's VaultApiResponseDto record, which is the shape returned to API callers.
@Mapper(componentModel = "spring")
public interface VaultResponseMapper
extends BaseMapper<VaultResponse, VaultApiResponseDto> {
VaultApiResponseDto toDto(VaultResponse vaultResponse);
}
Mapped Fields
VaultResponse Field | VaultApiResponseDto Record Field | Notes |
|---|---|---|
data | data | Map<String, Object> — the actual secret data |
leaseDuration | leastDuration | Lease TTL in seconds |
leaseId | leaseId | Vault lease identifier |
requestId | requestId | Vault request trace ID |
Usage in VaultAppRoleServiceImpl
public VaultApiResponseDto getAppRole(String role) {
VaultResponse response = vaultTemplate.read(
String.format(APPROLE_PATH, role));
return vaultResponseMapper.toDto(response);
}
How MapStruct Code Generation Works
At compile time, the maven-compiler-plugin runs MapStruct's annotation processor against all @Mapper interfaces and generates concrete implementation classes in target/generated-sources/annotations/. For example, KeycloakRoleMapper produces KeycloakRoleMapperImpl:
// Generated — do not edit
@Component
public class KeycloakRoleMapperImpl implements KeycloakRoleMapper {
@Override
public KeycloakRole toDto(RoleRepresentation roleRepresentation) {
if (roleRepresentation == null) return null;
return new KeycloakRole(
roleRepresentation.isComposite(),
roleRepresentation.getName(),
roleRepresentation.getDescription(),
roleRepresentation.getComposites(),
roleRepresentation.getAttributes()
);
}
@Override
public RoleRepresentation toEntity(KeycloakRole keycloakRole) {
if (keycloakRole == null) return null;
RoleRepresentation rep = new RoleRepresentation();
rep.setComposite(keycloakRole.composite());
rep.setName(keycloakRole.name());
rep.setDescription(keycloakRole.description());
// ...
return rep;
}
@Override
public List<KeycloakRole> toDtoList(List<RoleRepresentation> list) {
if (list == null) return null;
return list.stream().map(this::toDto).collect(Collectors.toList());
}
// toEntityList similarly generated
}
Lombok + MapStruct Annotation Processor Order
MapStruct must run after Lombok so that Lombok-generated getters and setters are visible when MapStruct inspects the class structure. The order is enforced in pom.xml:
<annotationProcessorPaths>
<!-- Lombok FIRST — generates getters/setters/constructors -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.36</version>
</path>
<!-- MapStruct SECOND — reads Lombok-generated methods -->
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.5.Final</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>-Amapstruct.suppressGeneratorTimestamp=true</arg>
<arg>-Amapstruct.defaultComponentModel=spring</arg>
</compilerArgs>
Adding a New Mapper
To add a mapper for a new entity:
Step 1 — Define the interface:
@Mapper(componentModel = "spring")
public interface MyEntityMapper extends BaseMapper<MyEntity, MyDto> {
// MapStruct generates toDto, toEntity, toDtoList, toEntityList automatically
// Override only if you need custom field mappings:
@Mapping(source = "sourceField", target = "targetField")
MyDto toDto(MyEntity entity);
}
Step 2 — Inject and use:
@Service
@RequiredArgsConstructor
public class MyServiceImpl implements MyService {
private final MyEntityMapper myEntityMapper;
public MyDto getItem(String id) {
MyEntity entity = repository.findById(id).orElseThrow(...);
return myEntityMapper.toDto(entity);
}
}
No further configuration is needed — mvn compile generates the implementation and Spring auto-wires it as a @Component.