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 four 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 |
TopicListingMapper | TopicListing (kafka-clients) | TopicListingDto{name,isInternal} | org.arya.banking.admin.mapper |
TopicDescriptionMapper | TopicDescription (kafka-clients) | TopicDescriptionDto{name,partitions,isInternal,aclOperation} | org.arya.banking.admin.mapper |
Keycloak/Vault mappers are MapStruct; Topic mappers are hand-written Spring @Components extending the local BaseMapper abstract class (convertToEntity/convertToDto + collection/list/set helpers).
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 |
The VaultApiResponseDto field is named leastDuration (not leaseDuration). This is a typo that has persisted from initial implementation. It does not break functionality but should be corrected in a future patch to match Vault's actual field name.
Usage in VaultAppRoleServiceImpl
public VaultApiResponseDto getAppRole(String role) {
VaultResponse response = vaultTemplate.read(
String.format(APPROLE_PATH, role));
return vaultResponseMapper.toDto(response);
}
TopicListingMapper / TopicDescriptionMapper
Hand-written @Components for the Kafka Admin API (no MapStruct — TopicListing/TopicDescription are kafka-clients types):
TopicListingMapper.convertToDto:TopicListing{name,isInternal}→TopicListingDto{name,isInternal}(and back vianew TopicListing(name, null, isInternal)).TopicDescriptionMapper.convertToDto:TopicDescription{name, partitions.size(), isInternal, authorizedOperations→Set<Byte>}→TopicDescriptionDto;convertToEntityreturnsnull(describe-only).
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>
If you ever see a MapStruct compile error like 'No property named X found' on a Lombok-annotated class, check that lombok comes before mapstruct-processor in annotationProcessorPaths. Reversing the order is the single most common cause of Lombok+MapStruct build failures.
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.
Because defaultComponentModel=spring is set globally in the compiler args, you do not need to specify componentModel =