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:

MapperEntity (E)DTO (D)Source
KeycloakRoleMapperRoleRepresentation (Keycloak Admin Client)KeycloakRole (local record)org.arya.banking.admin.mapper
VaultResponseMapperVaultResponse (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:

Java code-highlight
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.

Java code-highlight
@Mapper(componentModel = "spring")
public interface KeycloakRoleMapper
        extends BaseMapper<RoleRepresentation, KeycloakRole> {

    KeycloakRole toDto(RoleRepresentation roleRepresentation);

    List<KeycloakRole> toDtoList(List<RoleRepresentation> roleRepresentations);
}

Mapped Fields

RoleRepresentation FieldKeycloakRole Record FieldNotes
id(not mapped)Internal Keycloak UUID, not exposed
namenameRole identifier string, e.g. ROLE_ADMIN
descriptiondescriptionHuman-readable description
compositecompositetrue if the role is composed of other roles
compositescompositesNested Composites object (sub-roles)
attributesattributesMap<String, List<String>> of custom attributes

Usage in KeyCloakServiceImpl

Java code-highlight
// 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.

Java code-highlight
@Mapper(componentModel = "spring")
public interface VaultResponseMapper
        extends BaseMapper<VaultResponse, VaultApiResponseDto> {

    VaultApiResponseDto toDto(VaultResponse vaultResponse);
}

Mapped Fields

VaultResponse FieldVaultApiResponseDto Record FieldNotes
datadataMap<String, Object> — the actual secret data
leaseDurationleastDurationLease TTL in seconds
leaseIdleaseIdVault lease identifier
requestIdrequestIdVault 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

Java code-highlight
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:

Java code-highlight
// 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:

Xml code-highlight
<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:

Java code-highlight
@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:

Java code-highlight
@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 = "spring" in every @Mapper annotation. It is included explicitly in the admin-service mappers for clarity, but it would work without it.
mapstructmapperdtolombokcompile-time