
Mapper Creator
- 209 installs
- 105 repo stars
- Updated July 27, 2026
- amplicode/spring-skills
mapper-creator is an agent skill that generates MapStruct @AfterMapping methods to link inverse sides of OneToOne and OneToMany entity associations.
About
mapper-creator is an Amplicode Spring skill that helps solo builders and small teams emit correct MapStruct after-mapping hooks for JPA-style associations. When you map request DTOs into entities, orphan or one-sided links break cascades and REST contracts; this skill encodes the exact Java fragments for OneToMany collections and optional OneToOne owners using @org.mapstruct.AfterMapping and @MappingTarget. It is aimed at indie SaaS backends on Spring where you generate mappers from entity models rather than hand-writing boilerplate every sprint. Invoke it while scaffolding CRUD modules, Amplicode entity exports, or fixing integration tests that fail because children never received the parent reference. The skill stays narrow: it does not replace full mapper definitions, only the association-linking slice with templated variable substitution from attribute names and mappedBy inverses. That precision reduces copy-paste bugs when your agent adds a new aggregate root in Claude Code or Cursor.
- Inserts @AfterMapping default methods in mapper interface bodies at the documented insert point
- OneToMany template: forEach child with setInverse on mappedBy owner
- OneToOne template: null-safe link of inverse mappedBy attribute
- Variable table maps entityAttrName, FQN, inverseAttributeNameCapitalized from entity metadata
- Skips fragment when entity lacks non-owner OneToOne/OneToMany with mappedBy plus sub-DTO
Mapper Creator by the numbers
- 209 all-time installs (skills.sh)
- Ranked #25 of 89 Java & JVM skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/amplicode/spring-skills --skill mapper-creatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 209 |
|---|---|
| repo stars | ★ 105 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 27, 2026 |
| Repository | amplicode/spring-skills ↗ |
What it does
Generate MapStruct @AfterMapping default methods that wire bidirectional OneToOne and OneToMany associations after DTO-to-entity mapping.
Who is it for?
Best when you use MapStruct with Spring Data entities that use mappedBy on the non-owning association side.
Skip if: Greenfield projects on non-JVM stacks, mappers without entity associations, or cases where associations are read-only and never written from DTOs.
When should I use this skill?
Entity has non-owner OneToOne or OneToMany with mappedBy and you are editing or generating the MapStruct mapper interface.
What you get
Mapper interfaces gain default link methods that set inverse mappedBy references on children or optional OneToOne peers after each mapping target update.
- @AfterMapping default link methods inside the mapper interface
- Correct forEach or null-checked setInverse calls per association type
By the numbers
- Two association templates: OneToMany and OneToOne
- Seven documented template variables in the variable table
Files
Preflight: Spring MCP
This skill is part of the Spring Agent Toolkit and is designed to work with the Spring MCP server (provided by the Amplicode IntelliJ plugin). Before doing anything else, check your tool list for any Spring MCP tool — they are exposed under the amplicode MCP server (e.g. get_project_summary, list_module_dependencies, get_entity_details); harnesses that flatten MCP tools into the tool list use the mcp__amplicode__ prefix on the same names.
- If at least one Amplicode tool is available — MCP is connected. Proceed with the skill below.
- If none are available — stop and invoke the `amplicode-install` skill (bundled with the Spring Agent Toolkit). It installs the Amplicode plugin and walks the user through the «Настроить Spring Agent» welcome-screen button + MCP-client restart. After it completes, the MCP tools become available — resume this skill.
- If
amplicode-installis not registered in your skill list, tell the user (in their language): "This skill needs the Amplicode IntelliJ plugin and its MCP server. Install it from https://amplicode.ru/marketplace into IntelliJ IDEA Ultimate/Community or GigaIDE, open any project, click «Настроить Spring Agent» on the Amplicode welcome screen, then restart your MCP client."
---
Mapper Creator
Creates a mapper (MapStruct interface/abstract class or custom converter) for converting between an entity and a DTO.
---
CRITICAL: Code ONLY from examples/ files. If no matching example -- STOP and ask user.
CRITICAL: For questions with a fixed set of choices, prefer `AskUserQuestion` > its analogue > plain text list. Plain numbered text lists are the last resort when no interactive tool is available.
CRITICAL: Read the conversation context BEFORE running Step 1. Half the questions in Steps 2–3 may already be answered by the user's prompt and prior turns. Re-asking what was already said is the #1 reason this skill feels slow.
---
Defaults
| Option | Default | Always ask? | Notes |
|---|---|---|---|
| entity | — | YES | which entity to map |
| dtoClass | — | YES | which DTO to map to |
| mapperType | MapStruct | YES | main choice: MapStruct or Custom |
| className | {EntityName}Mapper | NO | suggest, confirm |
| packageName | package next to DTO | NO | auto-determined |
| parentInterface | null | NO | extend a common mapper interface |
| language | from get_project_summary | NO | auto-determined |
| componentModel | SPRING (if Spring is in dependencies), otherwise DEFAULT | NO | CDI is not auto-detected — user must specify explicitly |
| partialUpdate | no | NO | add partialUpdate method |
| partialUpdateNullStrategy | SET_TO_NULL | NO | if partialUpdate = yes: SET_TO_NULL / IGNORE / SET_TO_DEFAULT |
| updateWithNull | no | NO | add updateWithNull method |
| dtoIsRecord | false | NO | whether to use Java record for DTO (Java only; affects accessors in custom toEntity) |
Smart defaults: If the user says "use defaults", "all defaults", "default settings" -- skip all questions where "Always ask?" = NO. Ask only the required ones.
Smart answer recognition: When the user directly provides a value instead of choosing from a list -- accept it. Examples:
- Question "Which entity?" -> user answers "Order" -> that IS the entity
- Question "Mapper type?" -> user answers "mapstruct" -> that IS the choice
- If the user gave multiple answers in one message -> accept all, skip the answered questions
- NEVER re-ask what the user has already answered (even indirectly)
Batch questions: Group related questions into a single AskUserQuestion call (up to 4 questions):
- The main question (mapper type) is always asked SEPARATELY
- Do not group questions from DIFFERENT decision branches
- Prefer
AskUserQuestion; fall back to plain text only if the tool is unavailable
---
Step 0 -- Conversation context first (REQUIRED, no tool calls)
Before any MCP call, before any question, re-read the user's prompt and the prior turns of this conversation and extract whatever is already stated. This step costs nothing and prevents the most common failure mode of this skill — asking the user something they already said.
Build a mental checklist of inputs and tick off everything the user has already provided, explicitly or implicitly:
| Input | Look for in the prompt / context |
|---|---|
| entity | a class name (Order, Vet, ScheduleTemplate); "for X"; "from X to Y"; an open file in the IDE; a recently discussed entity |
| DTO | a class name ending in Dto / Response / Request; "to OrderDto"; "from X to Y"; a DTO that was just generated by dto-creator in this same conversation |
| mapperType | "MapStruct", "mapstruct", "@Mapper", "custom", "manually", "static methods", "extension function" → MapStruct vs Custom |
| className | "name it OrderConverter", "class FooMapper" |
| package | "in package …", "next to DTO", "next to controller" |
| methods | "only toDto", "with update", "partial update", "updateWithNull" |
| smart defaults | "use defaults", "all defaults", "default settings", "as usual" |
| prior project facts | language, JDK, dependencies — already known if discussed earlier in this conversation; do not re-fetch |
| delegated invocation | if dto-creator just delegated to this skill, the entity, DTO, package, and language are ALL known — never re-ask |
For every input that is explicitly or strongly implicitly answered: mark it as decided and skip the corresponding question in Steps 2–3. Do NOT ask "which entity?" if the user wrote "create a mapper for Order to OrderDto" — both entity and DTO are answered. Do NOT ask "MapStruct or Custom?" if the user wrote "create a MapStruct mapper".
For every input that is not answered: defer to the Decision-making principle below — try to derive it from project context first (Step 1), and only then ask.
Step 0 is mental, not a tool call. Do not announce it to the user. Do not write "Step 0 done". Just internalize what the user already said before proceeding to Step 1.
---
Decision-making principle — context first, then ask
Before asking the user any question, attempt to derive the answer from the context already gathered: project summary, module dependencies, entity details, existing files in the package, prior turns of this conversation, and the user's original prompt. Only ask when the context yields no clear default or when the choice is genuinely user-specific (e.g. which entity, which DTO).
Hierarchy of decisions:
1. Context is unambiguous → decide silently, do NOT ask. Examples: language and module from get_project_summary; MapStruct presence from list_module_dependencies; mapper package from the DTO's package; className from {Entity}Mapper; componentModel from Spring presence; mapperType when the user said "MapStruct" or "Custom" outright.
2. Context gives a strong signal → state the decision + alternatives in one line, let the user override or stay silent. Format:
Will create `OrderMapper` (MapStruct, componentModel=spring, in the same package as `OrderDto`).
Alternatives: Custom mapper. OK?The user can answer "ok" / "yes" / silence → accept; or name an alternative → switch.
3. Context yields no clear default → ask with `AskUserQuestion` (preferred) or its analogue, with the recommended option first and `(Recommended)` appended. Fall back to plain text if no interactive tool is available.
4. Context is fully empty for a critical input → ask plainly. This applies to: which entity, which DTO (when neither was mentioned), the user's intent itself.
How to ask — prefer AskUserQuestion
When a question must be asked, prefer the `AskUserQuestion` tool (or its analogue) over writing a numbered list in the response body. Fall back to plain text only if no interactive choice tool is available.
Rules for AskUserQuestion calls in this skill:
- Each call may contain up to 4 questions that are independent of each
other (the tool will render them together). Use this to batch related decisions in one round-trip.
- Each question has 2–4 options. The tool auto-adds an "Other" choice
for free-form input — never include it manually.
- Mark the recommended option by putting it first with
(Recommended)
appended to the label.
headeris a 12-char chip label (e.g. "Mapper", "Methods", "Package").- Each option has a
descriptionexplaining what the choice means.
When AskUserQuestion is not the right tool:
- Free-form input where there is no enumerable set of options
(e.g. arbitrary class name) — ask in plain text.
- The "single confirmation line" form from principle 2 — that is a plain
yes/no, not an enumerated choice.
The screen-driven question lists in Steps 2–3 below are a fallback for case 4. They are NOT a script to execute top-to-bottom. If a question's answer is already determined by principles 1–3, skip the question.
---
Step 1 -- Gather minimal project context (automatic, no questions)
Call only the MCP tools whose result is actually consumed by a later step. Do not pre-fetch "in case we need it" — every variable here must have a concrete downstream user.
| Tool | What to extract | Variable | Used for |
|---|---|---|---|
get_project_summary | language, moduleName, buildFile | language, moduleName, buildFile | language → Step 4 reference selection (Java vs Kotlin); moduleName → multi-module disambiguation; buildFile → Step 5 dependency injection |
list_module_dependencies(moduleName) | artifact IDs | presentDeps | Step 5 (is MapStruct already present? do we need to add it?) and componentModel decision below |
That is the entire Step 1. Do NOT fetch:
- Spring Boot version — no branching depends on it
- application.properties path — Step 5 writes nothing to properties
mainPackage— Step 4 derives the mapper package from the DTO's
package, not from the project root
get_entity_details/list_class_members— these depend on knowing
the entity and DTO, which happens in Step 2. Defer them to Step 2.
list_entity_mappers— needed only at Step 13 of the MapStruct
reference (uses = {...} resolution) and only per association entity, which is unknown until entityDetails is fetched. Defer to Step 13, do NOT pre-fetch in Step 1.
If multi-module project (multiple modules in get_project_summary): Ask which module to use. Then re-call list_module_dependencies for that module.
componentModel — simplified
Determine componentModel from presentDeps:
- Spring (any
spring-boot-starter*orspring-context) →SPRING - Otherwise →
DEFAULT
CDI is intentionally not auto-detected. If the user has a CDI project and wants componentModel = "cdi", they will say so explicitly; the skill should not branch on it by default.
---
Step 2 -- Entity and DTO
By Step 0 you should already know entity and DTO if the user mentioned them. Most common cases:
- User wrote "create a mapper for Order to OrderDto" → both known, skip
the questions, go straight to the parallel fetch below.
- `dto-creator` just delegated → entity and DTO are passed in by the
delegating skill. Never ask, never re-derive.
- User wrote "create a mapper for Order" → entity is
Order. The DTO
is the most recently created/discussed DTO for that entity in this conversation, OR — if there are multiple candidates — call list_entity_dtos(orderFqn) and pick the unique one. Only ask if there are multiple and no other signal.
Ask only when context is genuinely empty. When asking, prefer plain text (entity/DTO names are free-form input — AskUserQuestion is the wrong tool here):
Which entity should I create a mapper for? And which DTO to map to?After both entity FQN and DTO FQN are known, call (in parallel):
| Tool | Variable | Used for |
|---|---|---|
get_entity_details(entityFqn) | entityDetails | Step 4 — building @Mapping annotations, detecting non-owner associations with mappedBy for @AfterMapping |
list_class_members(dtoFqn) | dtoFields | Step 4 — comparing DTO fields against entity fields to decide which @Mapping(source, target) lines are needed |
Both calls are deferred to Step 2 because they require Step 2's inputs. They are NOT part of Step 1.
---
Step 3 -- Mapper type and variant settings
Mapper type — context first
Apply the Decision-making principle. Decide silently when context is clear:
| Context signal | Decision |
|---|---|
| User said "MapStruct" / "@Mapper" | MapStruct, no question |
| User said "Custom" / "manually" / "static methods" / "extension function" | Custom, no question |
MapStruct already in presentDeps AND user gave no signal | MapStruct (silent or one-line confirmation per principle 2) |
MapStruct NOT in presentDeps AND project is small/simple | MapStruct is still a fine default — Step 5 will add the dependency. State this in the one-line confirmation: "Will create a MapStruct mapper. Will add dependencies to the build file. Alternative: Custom with no dependencies. OK?" |
| User says "use defaults" | MapStruct |
Only fall back to AskUserQuestion when none of the rows above matches. Use it with these options:
| Question | Header | Options (first = recommended) |
|---|---|---|
What mapper type for {Entity} ↔ {Dto}? | Mapper | MapStruct (Recommended): interface with @Mapper/@Mapping / Custom: plain class with static methods |
Variant settings — defaults are usually correct
For both MapStruct and Custom variants the defaults are almost always correct:
className={EntityName}MapperpackageName= same package as the DTOpartialUpdate= noupdateWithNull= no
Do NOT batch-ask these settings unless the user explicitly requested configuration ("configure methods", "I want partial update") or said something that contradicts a default.
When the user did ask for configuration, use a single AskUserQuestion call (multiSelect: true) with the relevant subset:
| Question | Header | Options (first = recommended) |
|---|---|---|
| Which methods to add to the mapper? | Methods | toDto + fromDto (Recommended): basic bidirectional conversion / + partialUpdate: update entity from DTO / + updateWithNull: partialUpdate with null overwrite |
Class name and package are free-form — ask in plain text only when the user said "I want a different name" or "in a different package".
---
Step 4 -- Generate code
Determine the reference file based on mapper type and language:
- MapStruct + Java -> read
references/mapstruct-java.md - MapStruct + Kotlin -> read
references/mapstruct-kotlin.md - Custom + Java -> read
references/custom-java.md - Custom + Kotlin -> read
references/custom-kotlin.md
Read the corresponding reference file and follow its Generation order exactly.
Building @Mapping annotations
Read references/mapping-annotations.md for rules on how to build @Mapping annotations.
Compare entity fields from entityDetails with DTO fields from dtoFields: 1. Match DTO fields to entity fields by name 2. For fields with different names, add @Mapping(source, target) 3. For association ID fields (e.g. customerId -> customer.id), add appropriate mapping 4. For flat fields (e.g. customerName -> customer.name), add expression or source.target mapping
Reading skeleton and fragments
1. Read the skeleton file from examples/_skeletons/{variant}-{language}.md 2. Apply variable substitutions 3. Write the file 4. For each fragment in the generation order:
- Check if the fragment's condition is met
- Read the fragment from
examples/_fragments/{fragment-name}/{language}.md - Apply variable substitutions
- Insert/edit into the created file
Variable substitution rules
{packageName}-> from Step 1 context or user answer{className}-> from user answer or default{EntityName}Mapper{entityClassFqn}-> entity FQN from context{dtoClassFqn}-> DTO FQN from context{entityParamName}-> decapitalized entity short name{dtoParamName}-> decapitalized DTO short name{methodName}-> from naming conventions (seereferences/method-naming.md)- NEVER substitute anything not listed in Variables section of the example file
- NEVER add imports, methods, or code not in the example
- FQN handling (CRITICAL): examples contain FQNs (e.g.
org.mapstruct.Mapper,
org.mapstruct.Mapping, org.mapstruct.ReportingPolicy, org.mapstruct.MappingConstants.ComponentModel.SPRING, entity/DTO FQNs). When writing the final file, you MUST: 1. Replace every FQN in the body with its short name (e.g. @org.mapstruct.Mapper(...) -> @Mapper(...), org.mapstruct.ReportingPolicy.IGNORE -> ReportingPolicy.IGNORE, {entityClassFqn} -> entity short name, {dtoClassFqn} -> DTO short name). 2. Collect every FQN you shortened and emit a corresponding import line right after the package statement, sorted, no duplicates. 3. Classes from the same package as the mapper (entity, DTO if collocated) must NOT be imported — just use the short name. 4. Types from java.lang must NOT be imported. 5. Kotlin: same rules — shorten in the body and add import lines at the top. Kotlin does not need imports for classes in the same package. 6. The IDE will NOT optimize imports for you — the file is saved as-is.
---
Step 5 -- Add MapStruct dependencies (automatic, MapStruct variant only)
For Custom mapper: skip this step entirely. Custom mappers have no external dependencies.
For MapStruct: check presentDeps and add missing artifacts to the project's build file.
Required artifacts
| Artifact ID | Group ID | Scope |
|---|---|---|
mapstruct | org.mapstruct | implementation |
mapstruct-processor | org.mapstruct | annotationProcessor (Java) / kapt or ksp (Kotlin) |
If neither artifact is missing from presentDeps, skip the rest of this step — nothing to add.
How to edit the build file
Use the buildFile path captured in Step 1 — that is the exact file the skill must edit. Do NOT guess; do NOT search the project for build files.
Pick the editing strategy by file extension:
- `build.gradle.kts` — add
implementation("org.mapstruct:mapstruct:{version}")
inside the existing dependencies { … } block. For the processor:
- Java project →
annotationProcessor("org.mapstruct:mapstruct-processor:{version}") - Kotlin project →
kapt("org.mapstruct:mapstruct-processor:{version}")(applykotlin("kapt")plugin if not present) orksp(...)if KSP is already configured - `build.gradle` (Groovy) — same as above, with single-quoted Groovy syntax
- `pom.xml` — add a
<dependency>entry inside<dependencies>with<scope>matching the role (compileformapstruct, processor configured viamaven-compiler-plugin<annotationProcessorPaths>)
For the version: do NOT hardcode. Read the latest stable MapStruct version from the project's existing version catalogue (e.g. gradle/libs.versions.toml) if present; otherwise use the version that matches the Spring Boot BOM / project parent if Maven; otherwise emit a property/variable placeholder and ask the user to confirm.
Use the Edit tool with {buildFile} as file_path. Make the edit minimally — insert the new lines into the existing dependencies block, do not rewrite the file.
No properties needed
Mapper creation does not write any application.properties entries.
Report: "Created mapper {className} in package {packageName}. Type: {mapperType}. Methods: {list of methods}."
---
Anti-hallucination checklist
Before writing ANY code, verify:
- [ ] The code comes from an examples/ file (cite which one)
- [ ] Only declared variables were substituted
- [ ] No framework API calls were added "from knowledge"
- [ ] Import list matches the example exactly
- [ ] Method signatures match the example exactly
- [ ] No comments or convenience methods were added
- [ ] FQNs from examples are shortened in the body AND corresponding
importlines were added afterpackage(IDE will NOT do this for you) - [ ] @Mapping annotations match entity-DTO field comparison, not guessed
- [ ] Kotlin: multiple @Mapping wrapped in @Mappings(value = [...])
- [ ] @AfterMapping only added when entity has non-owner associations with mappedBy + sub-DTO
- [ ] toDto uses individual
@Mapping(source, target)pairs by default —@InheritInverseConfigurationonly when the user explicitly asked for it - [ ] Flat collection helper is generated only in
toDtodirection;toEntitydoes NOT try to load entities by id (no repository injection) - [ ] Flat collection helper name follows
{assocFieldName}To{capitalize(flatDtoFieldName)}(e.g.petsToPetIds, notpetsToId)
@AfterMapping method (Java)
Insert Point
Default method in mapper interface body.
Code
skip this fragment (only added when entity has non-owner OneToOne/OneToMany with mappedBy + sub-DTO)OneToMany association
@org.mapstruct.AfterMapping
default void link{entityAttrNameCapitalized}(@org.mapstruct.MappingTarget {entityClassFqn} {entityParamName}) {
{entityParamName}.get{entityAttrNameCapitalized}().forEach({unpluralizedAttrName} -> {unpluralizedAttrName}.set{inverseAttributeNameCapitalized}({entityParamName}));
}OneToOne association
@org.mapstruct.AfterMapping
default void link{entityAttrNameCapitalized}(@org.mapstruct.MappingTarget {entityClassFqn} {entityParamName}) {
{attrTypeEntityFqn} {entityAttrName} = {entityParamName}.get{entityAttrNameCapitalized}();
if ({entityAttrName} != null) {
{entityAttrName}.set{inverseAttributeNameCapitalized}({entityParamName});
}
}Variables
| Variable | Source | Default |
|---|---|---|
{entityAttrName} | entity association attribute name | e.g. children, address |
{entityAttrNameCapitalized} | capitalized | e.g. Children, Address |
{entityParamName} | decapitalized entity short name | e.g. order |
{entityClassFqn} | entity class FQN | — |
{inverseAttributeNameCapitalized} | capitalized mappedBy attribute | e.g. Parent, Order |
{unpluralizedAttrName} | unpluralized attr name (OneToMany only) | e.g. child |
{attrTypeEntityFqn} | association target entity FQN (OneToOne only) | — |
@AfterMapping method (Kotlin)
Insert Point
Function in mapper abstract class body.
Code
skip this fragment (only added when entity has non-owner OneToOne/OneToMany with mappedBy + sub-DTO)OneToMany association
@org.mapstruct.AfterMapping
fun link{entityAttrNameCapitalized}(@org.mapstruct.MappingTarget {entityParamName}: {entityClassFqn}) {
{entityParamName}.{entityAttrName}.forEach { it.{inverseAttributeName} = {entityParamName} }
}OneToOne association
@org.mapstruct.AfterMapping
fun link{entityAttrNameCapitalized}(@org.mapstruct.MappingTarget {entityParamName}: {entityClassFqn}) {
{entityParamName}.{entityAttrName}?.{inverseAttributeName} = {entityParamName}
}Variables
| Variable | Source | Default |
|---|---|---|
{entityAttrName} | entity association attribute name | e.g. children, address |
{entityAttrNameCapitalized} | capitalized | e.g. Children, Address |
{entityParamName} | decapitalized entity short name | e.g. order |
{entityClassFqn} | entity class FQN | — |
{inverseAttributeName} | mappedBy attribute name | e.g. parent, order |
mapFromAggregateReference helper (Java)
Insert Point
Default method in mapper interface body.
Code
skip this fragment (only for Spring Data JDBC entities with AggregateReference attributes)When entity has AggregateReference attributes with ID sub-DTO type
default <T, R> R mapFromAggregateReference(org.springframework.data.jdbc.core.mapping.AggregateReference<T, R> aggregateReference) {
if (aggregateReference == null) {
return null;
}
return aggregateReference.getId();
}Variables
None (generic helper method).
mapFromAggregateReference helper (Kotlin)
Insert Point
Function in mapper abstract class body.
Code
skip this fragment (only for Spring Data JDBC entities with AggregateReference attributes)When entity has AggregateReference attributes with ID sub-DTO type
fun <T, R> mapFromAggregateReference(aggregateReference: org.springframework.data.jdbc.core.mapping.AggregateReference<T, R>): R {
return aggregateReference?.id
}Variables
None (generic helper method).
mapToAggregateReference helper (Java)
Insert Point
Default method in mapper interface body.
Code
skip this fragment (only for Spring Data JDBC entities with AggregateReference attributes)When entity has AggregateReference attributes with ID sub-DTO type
default <T, R> org.springframework.data.jdbc.core.mapping.AggregateReference<T, R> mapToAggregateReference(R id) {
if (id == null) {
return null;
}
return org.springframework.data.jdbc.core.mapping.AggregateReference.to(id);
}Variables
None (generic helper method).
mapToAggregateReference helper (Kotlin)
Insert Point
Function in mapper abstract class body.
Code
skip this fragment (only for Spring Data JDBC entities with AggregateReference attributes)When entity has AggregateReference attributes with ID sub-DTO type
fun <T, R> mapToAggregateReference(id: R): org.springframework.data.jdbc.core.mapping.AggregateReference<T, R> {
return id?.let(org.springframework.data.jdbc.core.mapping.AggregateReference::to)
}Variables
None (generic helper method).
Context inner record + helper methods (Java)
Insert Point
Inner record in mapper interface body + helper methods.
Code
skip this fragment (only when entity has AggregateReference attributes with non-ID sub-DTO type)Context inner record
record {dtoName}LoadedContext({type1} {field1}, java.util.Collection<{type2}> {field2s}) {
}Context helper methods are generated per aggregate reference attribute. Their exact form depends on whether the attribute is a collection, child of collection, etc. Each attribute generates one helper method that uses the context record fields.
Variables
| Variable | Source | Default |
|---|---|---|
{dtoName} | DTO short class name | e.g. OrderDto |
{fieldN} | from attribute type class name, decapitalized | — |
{typeN} | attribute type FQN | — |
Context inner data class + helper methods (Kotlin)
Insert Point
Inner data class in mapper abstract class body + helper methods.
Code
skip this fragment (only when entity has AggregateReference attributes with non-ID sub-DTO type)Context inner data class
data class {dtoName}LoadedContext(val {field1}: {type1}?, val {field2s}: java.util.Collection<{type2}>)Context helper methods are generated per aggregate reference attribute. Their exact form depends on whether the attribute is a collection, child of collection, etc.
Variables
| Variable | Source | Default |
|---|---|---|
{dtoName} | DTO short class name | e.g. OrderDto |
{fieldN} | from attribute type class name, decapitalized | — |
{typeN} | attribute type FQN | — |
Custom toDto method (Java)
Insert Point
Static method in custom mapper class body.
Code
Always generated for Custom mapper.Standard form
Extract every direct entity property into a local first, then call the DTO constructor with the locals (and any chained accessors) in positional order.
public static {dtoClassFqn} {methodName}({entityClassFqn} {entityParamName}) {
{dtoFieldType1} {entityParamName}{field1Cap} = {entityParamName}.get{field1Cap}();
{dtoFieldType2} {entityParamName}{field2Cap} = {entityParamName}.get{field2Cap}();
{dtoClassFqn} {dtoParamName} = new {dtoClassFqn}(
{entityParamName}{field1Cap},
{entityParamName}{field2Cap}
);
return {dtoParamName};
}Local-variable rule
For direct property access (entity.getName()) extract a local. For chained access through a flat ToOne association (entity.getType().getId()) inline the expression directly into the constructor call — do not introduce a petType.id local. Example with one direct field and one flat field:
public static PetDto toPetDto(Pet pet) {
String petName = pet.getName();
PetDto petDto = new PetDto(petName, pet.getType().getId());
return petDto;
}If you would rather always extract locals (slightly more uniform, slightly more lines) that is functionally identical and acceptable — but the canonical form inlines the chained call.
Java Record DTO
The toDto side does not depend on whether the DTO is a record vs class — both are constructed via the canonical constructor. The same extract-locals-then-construct shape works unchanged. (Records change only the toEntity side, where DTO values are read via component accessors — see custom-to-entity/java.md.)
Variables
| Variable | Source | Default |
|---|---|---|
{methodName} | naming pattern to${DTO_NAME} | e.g. toOrderDto, toPetDto |
{entityParamName} | decapitalized entity short name | e.g. pet |
{dtoParamName} | decapitalized DTO short name | e.g. petDto |
{dtoClassFqn} | DTO class FQN | — |
{entityClassFqn} | entity class FQN | — |
{fieldNCap} | capitalized field names for getter calls | — |
{dtoFieldTypeN} | DTO field types | — |
Custom toDto method (Kotlin)
Insert Point
Extension function in the mapper file (not inside a class).
Code
Always generated for Custom mapper.fun {entityClassFqn}.{methodName}() = {dtoClassFqn}(
{field1} = this.{field1},
{field2} = this.{field2}
)Variables
| Variable | Source | Default |
|---|---|---|
{methodName} | naming pattern to${DTO_NAME} | e.g. toOrderDto |
{entityClassFqn} | entity class FQN | — |
{dtoClassFqn} | DTO class FQN | — |
{fieldN} | DTO attribute names | — |
Custom toEntity method (Java)
Insert Point
Static method in custom mapper class body.
Code
Always generated for Custom mapper.Standard form
Extract every DTO value into a local first, then construct the entity and apply setters. This mirrors custom-to-dto and is the canonical style. Use this form by default.
public static {entityClassFqn} {methodName}({dtoClassFqn} {dtoParamName}) {
{dtoFieldType1} {dtoParamName}{dtoField1Cap} = {dtoParamName}.{dtoField1Accessor};
{dtoFieldType2} {dtoParamName}{dtoField2Cap} = {dtoParamName}.{dtoField2Accessor};
{entityClassFqn} {entityParamName} = new {entityClassFqn}();
{entityParamName}.set{field1Cap}({dtoParamName}{dtoField1Cap});
{entityParamName}.set{field2Cap}({dtoParamName}{dtoField2Cap});
return {entityParamName};
}{dtoFieldNAccessor} — record vs class
- DTO is a regular class → use the getter:
getName(),getTypeId(), … - DTO is a Java record → use the record component accessor, no
get prefix: name(), typeId(), …
Use the bare-component form for records and the getX() form for classes. The choice is determined statically from the DTO declaration: a public record PetDto(...) always uses the component-accessor form.
Flat ToOne — JPA stub pattern
When the DTO contains flat fields from a ToOne association (e.g. only the id, or id+name extracted from Pet.type), do NOT call a repository or fetch the association. Build a stub of the association entity, set ONLY the flat fields, and assign it. JPA treats the stub as a known-id reference; loading the real row is the persistence layer's job.
public static Pet toEntity(PetDto petDto) {
String petDtoName = petDto.getName();
PetType petType = new PetType();
petType.setId(petDto.getTypeId());
Pet pet = new Pet();
pet.setName(petDtoName);
pet.setType(petType);
return pet;
}If multiple flat fields come from the same association (typeId and typeName), set them on the same stub:
PetType petType = new PetType();
petType.setId(petDto.getTypeId());
petType.setName(petDto.getTypeName());
pet.setType(petType);Java Record + flat ToOne (combined)
public static Pet toEntity(PetDto petDto) {
String petDtoName = petDto.name();
PetType petType = new PetType();
petType.setId(petDto.typeId());
Pet pet = new Pet();
pet.setName(petDtoName);
pet.setType(petType);
return pet;
}Variables
| Variable | Source | Default |
|---|---|---|
{methodName} | naming pattern | toEntity |
{dtoParamName} | decapitalized DTO short name | e.g. petDto |
{entityParamName} | decapitalized entity short name | e.g. pet |
{dtoClassFqn} | DTO class FQN | — |
{entityClassFqn} | entity class FQN | — |
{fieldNCap} | capitalized entity field names for setter | — |
{dtoFieldNCap} | capitalized DTO field names (used in local var name) | — |
{dtoFieldNAccessor} | getX() for class DTO, x() for record DTO | — |
{dtoFieldTypeN} | DTO field types | — |
Custom toEntity method (Kotlin)
Insert Point
Extension function in the mapper file (not inside a class).
Code
Always generated for Custom mapper.fun {dtoClassFqn}.{methodName}() = {entityClassFqn}().also {
it.{field1} = this.{field1}
it.{field2} = this.{field2}
}Variables
| Variable | Source | Default |
|---|---|---|
{methodName} | naming pattern | toEntity |
{dtoClassFqn} | DTO class FQN | — |
{entityClassFqn} | entity class FQN | — |
{fieldN} | attribute names | — |
Custom updateWithNull method (Java)
Insert Point
Static method in custom mapper class body.
Code
skip by default (only generated when user explicitly requests full update method)When UPDATE_WITH_NULL_VALUES requested
public static {entityClassFqn} {methodName}({dtoClassFqn} {dtoParamName}, {entityClassFqn} {entityParamName}) {
{entityParamName}.set{field1Cap}({dtoParamName}.get{dtoField1Cap}());
{entityParamName}.set{field2Cap}({dtoParamName}.get{dtoField2Cap}());
return {entityParamName};
}Variables
| Variable | Source | Default |
|---|---|---|
{methodName} | naming pattern | updateWithNull |
{dtoParamName} | decapitalized DTO short name | — |
{entityParamName} | decapitalized entity short name | — |
{dtoClassFqn} | DTO class FQN | — |
{entityClassFqn} | entity class FQN | — |
{fieldNCap} | capitalized entity field names | — |
{dtoFieldNCap} | capitalized DTO field names | — |
Custom updateWithNull method (Kotlin)
Insert Point
Extension function in the mapper file (not inside a class).
Code
skip by default (only generated when user explicitly requests full update method)When UPDATE_WITH_NULL_VALUES requested
fun {entityClassFqn}.{methodName}({dtoParamName}: {dtoClassFqn}) = apply {
{field1} = {dtoParamName}.{field1}
{field2} = {dtoParamName}.{field2}
}Variables
| Variable | Source | Default |
|---|---|---|
{methodName} | naming pattern | updateWithNull |
{dtoParamName} | decapitalized DTO short name | — |
{dtoClassFqn} | DTO class FQN | — |
{entityClassFqn} | entity class FQN | — |
{fieldN} | attribute names | — |
MAPPER factory field (Java)
Insert Point
Field inside the mapper interface body. Only when componentModel = DEFAULT.
Code
skip this fragment (componentModel is SPRING by default)When componentModel = DEFAULT
{className} MAPPER = org.mapstruct.factory.Mappers.getMapper({className}.class);Variables
| Variable | Source | Default |
|---|---|---|
{className} | user choice | {EntityName}Mapper |
MAPPER factory field (Kotlin)
Insert Point
Companion object inside the mapper abstract class body. Only when componentModel = DEFAULT.
Code
skip this fragment (componentModel is SPRING by default)When componentModel = DEFAULT
companion object {
@kotlin.jvm.JvmStatic
val MAPPER: {className} = org.mapstruct.factory.Mappers.getMapper({className}::class.java)
}Variables
| Variable | Source | Default |
|---|---|---|
{className} | user choice | {EntityName}Mapper |
Flat expression method (Java)
Insert Point
Default method in mapper interface body. Added together with the corresponding @Mapping(target=..., expression="java(...)") line on toDto (see examples/_fragments/to-dto-method/java.md).
⚠ Scope — collections only
This fragment applies only to flat ToMany fields (List<Integer> petIds, Set<Integer> specialtyIds, etc.). For flat ToOne fields (Integer typeId, String typeName flattened from Pet.type) do not emit a helper method or expression = .... Instead, use MapStruct dot-notation in the regular @Mapping(source, target) annotation — see examples/_fragments/to-entity-method/java.md ("With @Mapping dot-notation for ToOne flat fields"). MapStruct generates the intermediate new PetType(); petType.setId(...); itself.
Code
skip this fragment (only for DTOs with flat **collection** attributes where subDtoType == FLAT)Flat collection mapping method
default {collectionFqn}<{attrDtoTypeFqn}> {functionName}(java.util.Collection<{mappedEntityFqn}> {assocFieldName}) {
return {assocFieldName}.stream().map({mappedEntityName}::get{mappedAttrNameCap}){aggregateReferenceMapper}{toCollectionExpression};
}The parameter type is always java.util.Collection — not the concrete collection type from the entity field. The method only calls .stream(), so the concrete type is irrelevant. This also avoids mismatches when the entity getter returns a different collection type than the field declaration (e.g. field is Set<Specialty> but getter returns List<Specialty>).
Where {toCollectionExpression}:
.toList()for List on JDK 16+.collect(java.util.stream.Collectors.toList())for List on JDK < 16.collect(java.util.stream.Collectors.toSet())for Set
Where {aggregateReferenceMapper}:
.map(org.springframework.data.jdbc.core.mapping.AggregateReference::getId)if mapped attr is AggregateReference- empty otherwise
Naming rule (CRITICAL)
The helper function name is built from the entity association field name and the flat DTO field name — not from the nested attribute name. This guarantees uniqueness even when the same association is flattened in multiple ways:
{functionName} = {assocFieldName} + "To" + capitalize({flatDtoFieldName})For example, if Owner.pets is flattened twice into petIds and petNames, you get two distinct helpers petsToPetIds and petsToPetNames. A naming scheme based on the nested attribute (petsToId, petsToName) would NOT collide here either, but it does collide as soon as two different flat fields use the same nested attribute on different associations — so use the DTO-field-based scheme unconditionally.
Worked examples
// Vet.specialties: Set<Specialty> -> VetDto.specialtyIds: Set<Integer>
default Set<Integer> specialtiesToSpecialtyIds(Collection<Specialty> specialties) {
return specialties.stream().map(Specialty::getId).collect(java.util.stream.Collectors.toSet());
}
// Owner.pets: List<Pet> -> OwnerDto.petIds: List<Integer> (JDK 16+)
default List<Integer> petsToPetIds(Collection<Pet> pets) {
return pets.stream().map(Pet::getId).toList();
}Variables
| Variable | Source | Default |
|---|---|---|
{functionName} | {assocFieldName} + "To" + capitalize({flatDtoFieldName}) | e.g. specialtiesToSpecialtyIds, petsToPetIds |
{assocFieldName} | name of the entity association field (the part of the unFlatName before .); also used as the parameter name | e.g. specialties, pets |
{flatDtoFieldName} | name of the flattened collection field on the DTO (the target of the @Mapping) | e.g. specialtyIds, petIds |
{mappedEntityFqn} | association target entity FQN | — |
{mappedEntityName} | short entity name for method reference | e.g. Specialty, Pet |
{mappedAttrNameCap} | capitalized attribute name after . in unFlatName, used in ::get… | e.g. Id |
{collectionFqn} | java.util.List or java.util.Set (used for return type only; parameter is always java.util.Collection) | — |
{attrDtoTypeFqn} | DTO field element type FQN | e.g. java.lang.Integer |
Flat expression method (Kotlin)
Insert Point
Function in mapper abstract class body. Added via postProcess.
Code
skip this fragment (only for DTOs with flat collection attributes where subDtoType == FLAT)Flat collection mapping function
fun {functionName}({mappedFieldName}: Collection<{mappedEntityFqn}>): {collectionFqn}<{attrKotlinType}?> {
return {mappedFieldName}.mapNotNull{ it.{mappedAttrName} }{aggregateReferenceMapper}.toMutable{collectionClassName}()
}The parameter type is always Collection — not the concrete collection type from the entity field. The method only iterates, so the concrete type is irrelevant. This also avoids mismatches when the entity getter returns a different collection type than the field declaration.
Where {aggregateReferenceMapper}:
.map { it.id }if mapped attr is AggregateReference- empty otherwise
Variables
| Variable | Source | Default |
|---|---|---|
{functionName} | {mappedFieldName}To{capitalize(attrName)} | — |
{mappedFieldName} | from unFlatName before . | — |
{mappedEntityFqn} | association target entity FQN | — |
{mappedAttrName} | attribute name after . | — |
{collectionFqn} | kotlin.collections.MutableList or kotlin.collections.MutableSet | — |
{collectionClassName} | List or Set | — |
{attrKotlinType} | DTO field Kotlin type | — |
updateWithNull MapStruct method (Java)
Insert Point
Abstract method in mapper interface body.
Code
skip by default (only generated when user explicitly requests full update method)When UPDATE_WITH_NULL_VALUES requested
@org.mapstruct.InheritConfiguration(name = "{firstUpdateMethodName}")
{entityClassFqn} {methodName}({dtoClassFqn} {dtoParamName}, @org.mapstruct.MappingTarget {entityClassFqn} {entityParamName});Variables
| Variable | Source | Default |
|---|---|---|
{methodName} | naming pattern | updateWithNull |
{firstUpdateMethodName} | name of first update/toEntity method | toEntity |
{dtoParamName} | decapitalized DTO short name | — |
{entityParamName} | decapitalized entity short name | — |
{dtoClassFqn} | DTO class FQN | — |
{entityClassFqn} | entity class FQN | — |
updateWithNull MapStruct method (Kotlin)
Insert Point
Abstract function in mapper abstract class body.
Code
skip by default (only generated when user explicitly requests full update method)When UPDATE_WITH_NULL_VALUES requested
@org.mapstruct.InheritConfiguration(name = "{firstUpdateMethodName}")
abstract fun {methodName}({dtoParamName}: {dtoClassFqn}, @org.mapstruct.MappingTarget {entityParamName}: {entityClassFqn}): {entityClassFqn}Variables
| Variable | Source | Default |
|---|---|---|
{methodName} | naming pattern | updateWithNull |
{firstUpdateMethodName} | name of first update/toEntity method | toEntity |
{dtoParamName} | decapitalized DTO short name | — |
{entityParamName} | decapitalized entity short name | — |
{dtoClassFqn} | DTO class FQN | — |
{entityClassFqn} | entity class FQN | — |
@Mapper annotation (Java)
Insert Point
Annotation on the mapper interface created from skeleton.
Code
@org.mapstruct.Mapper(unmappedTargetPolicy = org.mapstruct.ReportingPolicy.IGNORE, componentModel = org.mapstruct.MappingConstants.ComponentModel.SPRING)SPRING component model (MappingConstants available)
@org.mapstruct.Mapper(unmappedTargetPolicy = org.mapstruct.ReportingPolicy.IGNORE, componentModel = org.mapstruct.MappingConstants.ComponentModel.SPRING)SPRING component model (MappingConstants NOT available, older MapStruct)
@org.mapstruct.Mapper(unmappedTargetPolicy = org.mapstruct.ReportingPolicy.IGNORE, componentModel = "spring")CDI component model
@org.mapstruct.Mapper(unmappedTargetPolicy = org.mapstruct.ReportingPolicy.IGNORE, componentModel = "cdi")DEFAULT component model (no Spring, no CDI)
@org.mapstruct.Mapper(unmappedTargetPolicy = org.mapstruct.ReportingPolicy.IGNORE)Variables
| Variable | Source | Default |
|---|---|---|
| componentModel | SPRING if any spring-boot-starter* / spring-context in presentDeps, else DEFAULT. CDI only when the user explicitly asked for it. | SPRING |
Note — recommended default
Prefer the SPRING (modern) variant (MappingConstants.ComponentModel.SPRING) whenever Spring is on the classpath. The DEFAULT and CDI variants are listed for completeness but are rarely needed in practice.
@Mapper annotation (Kotlin)
Insert Point
Annotation on the mapper abstract class created from skeleton.
Code
@org.mapstruct.Mapper(unmappedTargetPolicy = org.mapstruct.ReportingPolicy.IGNORE, componentModel = org.mapstruct.MappingConstants.ComponentModel.SPRING)SPRING component model (MappingConstants available)
@org.mapstruct.Mapper(unmappedTargetPolicy = org.mapstruct.ReportingPolicy.IGNORE, componentModel = org.mapstruct.MappingConstants.ComponentModel.SPRING)SPRING component model (MappingConstants NOT available)
@org.mapstruct.Mapper(unmappedTargetPolicy = org.mapstruct.ReportingPolicy.IGNORE, componentModel = "spring")CDI component model
@org.mapstruct.Mapper(unmappedTargetPolicy = org.mapstruct.ReportingPolicy.IGNORE, componentModel = "cdi")DEFAULT component model
@org.mapstruct.Mapper(unmappedTargetPolicy = org.mapstruct.ReportingPolicy.IGNORE)Variables
| Variable | Source | Default |
|---|---|---|
| componentModel | SPRING if any spring-boot-starter* / spring-context in presentDeps, else DEFAULT. CDI only when the user explicitly asked for it. | SPRING |
Parent interface extends clause (Java)
Insert Point
Modifies the interface declaration to add extends clause.
Code
skip this fragment (no parent interface by default)When parent interface selected
public interface {className} extends {parentFqn}<{dtoClassFqn}, {entityClassFqn}> {Variables
| Variable | Source | Default |
|---|---|---|
{className} | user choice | {EntityName}Mapper |
{parentFqn} | user choice | — |
{dtoClassFqn} | DTO class FQN | — |
{entityClassFqn} | entity class FQN | — |
Parent interface supertypes clause (Kotlin)
Insert Point
Modifies the abstract class declaration to add supertype.
Code
skip this fragment (no parent interface by default)When parent interface selected
abstract class {className} : {parentFqn}<{dtoClassFqn}, {entityClassFqn}>() {Variables
| Variable | Source | Default |
|---|---|---|
{className} | user choice | {EntityName}Mapper |
{parentFqn} | user choice | — |
{dtoClassFqn} | DTO class FQN | — |
{entityClassFqn} | entity class FQN | — |
partialUpdate MapStruct method (Java)
Insert Point
Abstract method in mapper interface body.
Code
skip by default (only generated when user explicitly requests partial update method)When PARTIAL_UPDATE requested
Always one method, controlled by ONE @BeanMapping annotation. The nullValuePropertyMappingStrategy enum value is selected by the user (SET_TO_NULL / IGNORE / SET_TO_DEFAULT). Do not emit @InheritConfiguration — the inherited mappings are not needed for the canonical Pet/Owner-style DTOs.
@org.mapstruct.BeanMapping(nullValuePropertyMappingStrategy = org.mapstruct.NullValuePropertyMappingStrategy.{strategyEnum})
{entityClassFqn} {methodName}({dtoClassFqn} {dtoParamName}, @org.mapstruct.MappingTarget {entityClassFqn} {entityParamName});Worked examples (one per strategy — only one is emitted)
// SET_TO_NULL (the most common — null in DTO clears the field)
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_NULL)
Owner partialUpdate(OwnerDto ownerDto, @MappingTarget Owner owner);
// IGNORE (null in DTO keeps the existing value)
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
Owner partialUpdate(OwnerDto ownerDto, @MappingTarget Owner owner);
// SET_TO_DEFAULT (null in DTO resets to type default — 0 for int, "" for String, etc.)
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT)
Owner partialUpdate(OwnerDto ownerDto, @MappingTarget Owner owner);Variables
| Variable | Source | Default |
|---|---|---|
{methodName} | naming pattern | partialUpdate |
{strategyEnum} | user choice | SET_TO_NULL |
{dtoParamName} | decapitalized DTO short name | e.g. orderDto |
{entityParamName} | decapitalized entity short name | e.g. order |
{dtoClassFqn} | DTO class FQN | — |
{entityClassFqn} | entity class FQN | — |
partialUpdate MapStruct method (Kotlin)
Insert Point
Abstract function in mapper abstract class body.
Code
skip by default (only generated when user explicitly requests partial update method)When PARTIAL_UPDATE requested
@org.mapstruct.BeanMapping(nullValuePropertyMappingStrategy = org.mapstruct.NullValuePropertyMappingStrategy.IGNORE)
@org.mapstruct.InheritConfiguration(name = "{toEntityMethodName}")
abstract fun {methodName}({dtoParamName}: {dtoClassFqn}, @org.mapstruct.MappingTarget {entityParamName}: {entityClassFqn}): {entityClassFqn}Variables
| Variable | Source | Default |
|---|---|---|
{methodName} | naming pattern | partialUpdate |
{dtoParamName} | decapitalized DTO short name | e.g. orderDto |
{entityParamName} | decapitalized entity short name | e.g. order |
{dtoClassFqn} | DTO class FQN | — |
{entityClassFqn} | entity class FQN | — |
{toEntityMethodName} | name of toEntity method | toEntity |
toDto MapStruct method (Java)
Insert Point
Abstract method in mapper interface body, after toEntity method.
Code
Always generated by default.Without any annotations (all fields map directly) — preferred when applicable
{dtoClassFqn} {methodName}({entityClassFqn} {entityParamName});With @Mapping annotations (default whenever any field needs remapping)
For each entity-field/DTO-field pair that requires a non-default mapping, emit a separate @Mapping line. Use this even when toEntity already has the inverse — see references/mapping-annotations.md for why.
@org.mapstruct.Mapping(source = "{entityFieldName}", target = "{dtoFieldName}")
{dtoClassFqn} {methodName}({entityClassFqn} {entityParamName});With @Mapping(expression) for flat collection fields
Used together with the helper from examples/_fragments/flat-expression/java.md. For each flat collection DTO field, add one of these @Mapping lines on toDto. The helper method itself is added to the same mapper interface as a default method.
@org.mapstruct.Mapping(target = "{flatDtoFieldName}", expression = "java({functionName}({entityParamName}.get{AssocFieldCap}()))")
{dtoClassFqn} {methodName}({entityClassFqn} {entityParamName});Where:
{flatDtoFieldName}— the flat collection field on the DTO (e.g.specialtyIds,petIds){functionName}— the helper method name from the flat-expression fragment (e.g.specialtiesToSpecialtyIds,petsToPetIds){AssocFieldCap}— capitalized association field name on the entity (e.g.Specialties,Pets)
With @Context parameter (aggregate references with context)
{dtoClassFqn} {methodName}({entityClassFqn} {entityParamName}, @org.mapstruct.Context {dtoName}LoadedContext context);With @InheritInverseConfiguration — preferred when toEntity has 2+ flat ToOne mappings
When toEntity carries only invertible @Mapping(source, target) pairs (the dot-notation flat-ToOne case from to-entity-method/java.md), emit @InheritInverseConfiguration on toDto instead of duplicating the annotations. Use this form when:
toEntityhas at least one@Mappingand ALL of its mappings are plain
(source, target) pairs that MapStruct can invert losslessly (no expression, constant, defaultValue, ignore = true, qualifiedBy, or condition).
For the single-@Mapping case the duplication is so cheap that either form is fine; default to inheritance only when there are 2+ mappings, which is where the benefit becomes visible.
@org.mapstruct.InheritInverseConfiguration(name = "{toEntityMethodName}")
{dtoClassFqn} {methodName}({entityClassFqn} {entityParamName});Worked example (paired with the Pet toEntity(PetDto) from to-entity-method/java.md):
@InheritInverseConfiguration(name = "toEntity")
PetDto toPetDto(Pet pet);If toEntity mixes invertible and non-invertible mappings (expression, ignore = true, etc.), do not use inheritance — fall back to the explicit @Mapping form documented above. See references/mapping-annotations.md for the failure modes.
Variables
| Variable | Source | Default |
|---|---|---|
{methodName} | naming pattern to${DTO_NAME} | e.g. toOrderDto |
{entityParamName} | decapitalized entity short name | e.g. order |
{entityClassFqn} | entity class FQN | — |
{dtoClassFqn} | DTO class FQN | — |
{toEntityMethodName} | name of toEntity method | toEntity |
{entityFieldName} | entity field that differs from DTO field | — |
{dtoFieldName} | DTO field that differs from entity field | — |
{dtoName} | DTO short class name | e.g. OrderDto |
toDto MapStruct method (Kotlin)
Insert Point
Abstract function in mapper abstract class body, after toEntity function.
Code
Always generated by default.With @InheritInverseConfiguration (when toEntity was created first and has mappings)
@org.mapstruct.InheritInverseConfiguration(name = "{toEntityMethodName}")
abstract fun {methodName}({entityParamName}: {entityClassFqn}): {dtoClassFqn}With @Mappings annotations (when toEntity was not created or has no mappings)
@org.mapstruct.Mappings(
org.mapstruct.Mapping(source = "{entityFieldName}", target = "{dtoFieldName}")
)
abstract fun {methodName}({entityParamName}: {entityClassFqn}): {dtoClassFqn}With @Context parameter (aggregate references with context)
abstract fun {methodName}({entityParamName}: {entityClassFqn}, @org.mapstruct.Context context: {dtoName}LoadedContext): {dtoClassFqn}Without any annotations (all fields map directly)
abstract fun {methodName}({entityParamName}: {entityClassFqn}): {dtoClassFqn}Variables
| Variable | Source | Default |
|---|---|---|
{methodName} | naming pattern to${DTO_NAME} | e.g. toOrderDto |
{entityParamName} | decapitalized entity short name | e.g. order |
{entityClassFqn} | entity class FQN | — |
{dtoClassFqn} | DTO class FQN | — |
{toEntityMethodName} | name of toEntity method | toEntity |
{entityFieldName} | entity field that differs from DTO field | — |
{dtoFieldName} | DTO field that differs from entity field | — |
{dtoName} | DTO short class name | e.g. OrderDto |
toEntity MapStruct method (Java)
Insert Point
Abstract method in mapper interface body.
Code
Always generated by default.With @Mapping annotations (first method, or no inverse available)
@org.mapstruct.Mapping(source = "{dtoFieldName}", target = "{entityFieldName}")
{entityClassFqn} {methodName}({dtoClassFqn} {dtoParamName});With @Mapping dot-notation for ToOne flat fields — preferred for ToOne FLAT
When DTO has one or more flat fields from a single ToOne association (e.g. PetDto with typeId / typeName flattened from Pet.type), emit one @Mapping per flat field using dot-notation in target. No Java-expression, no helper method. MapStruct itself synthesizes the intermediate new PetType() and the field setters.
@org.mapstruct.Mapping(source = "{flatDtoFieldName}", target = "{assocFieldName}.{nestedFieldName}")
@org.mapstruct.Mapping(source = "{flatDtoFieldName2}", target = "{assocFieldName}.{nestedFieldName2}")
{entityClassFqn} {methodName}({dtoClassFqn} {dtoParamName});Worked example — PetDto(typeId, typeName) flattens Pet.type (which points at PetType):
@Mapping(source = "typeName", target = "type.name")
@Mapping(source = "typeId", target = "type.id")
Pet toEntity(PetDto petDto);Note: the order of @Mapping annotations does not matter functionally. Any consistent ordering is acceptable.
With @InheritInverseConfiguration (when toDto was created first)
@org.mapstruct.InheritInverseConfiguration(name = "{inverseMethodName}")
{entityClassFqn} {methodName}({dtoClassFqn} {dtoParamName});Without any annotations (all fields map directly, no remapping needed)
{entityClassFqn} {methodName}({dtoClassFqn} {dtoParamName});Variables
| Variable | Source | Default |
|---|---|---|
{methodName} | naming pattern | toEntity |
{dtoParamName} | decapitalized DTO short name | e.g. orderDto |
{dtoClassFqn} | DTO class FQN | — |
{entityClassFqn} | entity class FQN | — |
{dtoFieldName} | DTO field that differs from entity field | — |
{entityFieldName} | entity field that differs from DTO field | — |
{inverseMethodName} | name of toDto method | e.g. toOrderDto |
toEntity MapStruct method (Kotlin)
Insert Point
Abstract function in mapper abstract class body.
Code
Always generated by default.With @Mappings annotations (first method, or no inverse available)
@org.mapstruct.Mappings(
org.mapstruct.Mapping(source = "{dtoFieldName}", target = "{entityFieldName}")
)
abstract fun {methodName}({dtoParamName}: {dtoClassFqn}): {entityClassFqn}With @InheritInverseConfiguration (when toDto was created first)
@org.mapstruct.InheritInverseConfiguration(name = "{inverseMethodName}")
abstract fun {methodName}({dtoParamName}: {dtoClassFqn}): {entityClassFqn}Without any annotations (all fields map directly)
abstract fun {methodName}({dtoParamName}: {dtoClassFqn}): {entityClassFqn}Variables
| Variable | Source | Default |
|---|---|---|
{methodName} | naming pattern | toEntity |
{dtoParamName} | decapitalized DTO short name | e.g. orderDto |
{dtoClassFqn} | DTO class FQN | — |
{entityClassFqn} | entity class FQN | — |
{dtoFieldName} | DTO field that differs from entity field | — |
{entityFieldName} | entity field that differs from DTO field | — |
{inverseMethodName} | name of toDto method | e.g. toOrderDto |
@Mapper(uses = ...) attribute (Java)
Insert Point
Modifies existing @Mapper annotation to add uses attribute.
Code
skip this fragment (no sub-mappers by default)When sub-mappers exist for association DTOs
@org.mapstruct.Mapper(unmappedTargetPolicy = org.mapstruct.ReportingPolicy.IGNORE, componentModel = ..., uses = {SubMapper1.class, SubMapper2.class})Variables
| Variable | Source | Default |
|---|---|---|
| sub-mapper FQNs | auto-detected from existing mappers for association entities | — |
@Mapper(uses = ...) attribute (Kotlin)
Insert Point
Modifies existing @Mapper annotation to add uses attribute.
Code
skip this fragment (no sub-mappers by default)When sub-mappers exist for association DTOs
@org.mapstruct.Mapper(unmappedTargetPolicy = org.mapstruct.ReportingPolicy.IGNORE, componentModel = ..., uses = [SubMapper1::class, SubMapper2::class])Variables
| Variable | Source | Default |
|---|---|---|
| sub-mapper FQNs | auto-detected from existing mappers for association entities | — |
Custom mapper class (Java)
Code
package {packageName};
public class {className} {
}Variables
| Variable | Source | Default |
|---|---|---|
{packageName} | project context | — |
{className} | user choice | {EntityName}Mapper |
Custom mapper file (Kotlin)
Code
package {packageName}Variables
| Variable | Source | Default |
|---|---|---|
{packageName} | project context | — |
MapStruct mapper interface (Java)
Code
package {packageName};
public interface {className} {
}Variables
| Variable | Source | Default |
|---|---|---|
{packageName} | project context | — |
{className} | user choice | {EntityName}Mapper |
MapStruct mapper abstract class (Kotlin)
Code
package {packageName}
abstract class {className} {
}Variables
| Variable | Source | Default |
|---|---|---|
{packageName} | project context | — |
{className} | user choice | {EntityName}Mapper |
Custom Mapper -- Java
Skeleton
Read examples/_skeletons/custom-java.md. Write the file to src/main/java/{packagePath}/{className}.java.
Generation order
1. Create file from skeleton (plain class) 2. Add toDto static method from examples/_fragments/custom-to-dto/java.md
- For each DTO field: extract value from entity via getter into a local variable
- Create DTO via constructor call with all extracted variables
- Return the DTO
3. Add toEntity static method from examples/_fragments/custom-to-entity/java.md
- For each direct DTO field: extract value into a local using the DTO accessor (
getX()for class DTOs,x()for record DTOs) - Create new entity instance
- Call entity setters with the locals
- For flat ToOne fields: build a JPA stub of the association entity (
new PetType(); petType.setId(...);) and pass it to the parent setter - Return the entity
4. If UPDATE_WITH_NULL_VALUES requested: add from examples/_fragments/custom-update-with-null/java.md
- Takes both DTO and entity as parameters
- Calls entity setters with DTO getter values
- Returns the entity
Method body construction
toDto method:
- Init expressions:
{DtoFieldType} {entityParam}{FieldCap} = {entityParam}.get{FieldCap}();for each DTO field - Result:
new {DtoClassFqn}({entityParam}{Field1Cap}, {entityParam}{Field2Cap}, ...)
toEntity method:
- For each direct DTO field:
{DtoFieldType} {dtoParam}{DtoFieldCap} = {dtoParam}.{accessor}; {accessor}=get{DtoFieldCap}()for class DTO,{dtoFieldName}()for record DTO- Init:
{EntityClassFqn} {entityParam} = new {EntityClassFqn}(); - For each direct field:
{entityParam}.set{FieldCap}({dtoParam}{DtoFieldCap}); - For each flat ToOne assoc with fields {f1, f2, ...} from
{Assoc}: {Assoc} {assocVar} = new {Assoc}();{assocVar}.set{F1Cap}({dtoParam}.{f1Accessor});{assocVar}.set{F2Cap}({dtoParam}.{f2Accessor});{entityParam}.set{AssocCap}({assocVar});- Return:
{entityParam}
updateWithNull method:
- Body: same setter pattern as toEntity, but entity is a parameter (not new)
- Return:
{entityParam}
Method naming
Same defaults as MapStruct variant (see references/mapstruct-java.md).
Custom Mapper -- Kotlin
Skeleton
Read examples/_skeletons/custom-kotlin.md. Write the file to src/main/kotlin/{packagePath}/{className}.kt.
Note: Kotlin custom mapper is a file with extension functions, not a class.
Generation order
1. Create file from skeleton (just package declaration) 2. Add toDto extension function from examples/_fragments/custom-to-dto/kotlin.md
- Extension on entity class
- Returns DTO via named constructor arguments:
DtoFqn(field1 = this.field1, field2 = this.field2)
3. Add toEntity extension function from examples/_fragments/custom-to-entity/kotlin.md
- Extension on DTO class
- Returns
EntityFqn().also { it.field1 = this.field1; ... }
4. If UPDATE_WITH_NULL_VALUES requested: add from examples/_fragments/custom-update-with-null/kotlin.md
- Extension on entity class with DTO parameter
- Uses
apply { field1 = dtoParam.field1; ... }
Kotlin-specific patterns
- toDto:
fun EntityFqn.toOrderDto() = DtoFqn(field1 = this.field1, field2 = this.field2) - toEntity:
fun DtoFqn.toEntity() = EntityFqn().also { it.field1 = this.field1; it.field2 = this.field2 } - updateWithNull:
fun EntityFqn.updateWithNull(orderDto: DtoFqn) = apply { field1 = orderDto.field1; ... }
Method naming
Same defaults as MapStruct variant (see references/mapstruct-java.md).
@Mapping Annotation Rules
Determining field mappings
Compare entity fields (from get_entity_details) with DTO fields (from list_class_members).
For each DTO field, determine the mapping type:
Direct match
Same name and compatible type in both entity and DTO. No @Mapping annotation needed.
Different name
DTO field name differs from entity field name.
@org.mapstruct.Mapping(source = "{sourceField}", target = "{targetField}")SubDtoType = ID (association mapped to ID only)
DTO has an ID field for an entity association (e.g. Long customerId for Customer customer).
@org.mapstruct.Mapping(source = "{dtoIdField}", target = "{entityAssocField}.id")For toDto (reverse): @Mapping(source = "{entityAssocField}.id", target = "{dtoIdField}")
SubDtoType = FLAT (flattened association fields)
DTO has individual fields extracted from a nested entity (e.g. String customerName from customer.name).
- For non-collection (ToOne): dot-notation in BOTH directions. Note that on
toEntitythe dot is on the target side, ontoDtoit's on the source side. MapStruct synthesizes the intermediatenew {Assoc}()itself. - toEntity:
@Mapping(source = "{dtoFlatField}", target = "{entityAssocField}.{nestedField}") - toDto:
@Mapping(source = "{entityAssocField}.{nestedField}", target = "{dtoFlatField}")— or use@InheritInverseConfiguration(see below) when there are 2+ flat mappings. - For collection (ToMany): uses
expressionwith helper method (see flat-expression fragments). Dot-notation does not work for collections.
SubDtoType = NEW_CLASS / NEW_NESTED_CLASS / EXIST_CLASS
DTO uses another DTO for the association. MapStruct handles this implicitly by generating private nested-mapping methods inside the same mapper interface — no uses= and no separate sub-mapper file are needed by default. Do not emit uses for these cases — mapping will be handled implicitly. Add uses only when a real sibling mapper must be reused or a @Named qualifier is required.
@InheritInverseConfiguration — opt-in only, NOT the default
When toEntity is generated first (always true by default), it is technically possible for toDto to use:
@org.mapstruct.InheritInverseConfiguration(name = "toEntity")This inherits all @Mapping annotations in reverse, avoiding duplication.
However, this is NOT the default in this skill. Reasons to prefer duplicated @Mapping(source, target) pairs on toDto:
1. Robust under refactoring. @InheritInverseConfiguration silently breaks (or starts producing wrong code) the moment toEntity gains any non-invertible mapping: ignore = true, expression = "...", constant = "...", defaultValue, qualifiedBy, dateFormat, numberFormat. None of these have a meaningful inverse. 2. Locally readable. A reviewer reading toDto sees its mappings inline; they do not have to scroll up to toEntity and mentally invert each line. 3. One fewer import (org.mapstruct.InheritInverseConfiguration). 4. Order-independent. If a future edit reorders steps 5 and 6 of the generation order, the duplicated @Mapping form keeps working; @InheritInverseConfiguration(name = "toEntity") would resolve to a forward reference.
Exception — multi-flat-ToOne case. When toEntity carries 2+ plain @Mapping(source, target) annotations (typically the dot-notation flat-ToOne case from to-entity-method/java.md) and ALL of them are losslessly invertible, prefer @InheritInverseConfiguration(name = "toEntity") on toDto. Worked example:
@Mapping(source = "typeName", target = "type.name")
@Mapping(source = "typeId", target = "type.id")
Pet toEntity(PetDto petDto);
@InheritInverseConfiguration(name = "toEntity")
PetDto toPetDto(Pet pet);For the single-@Mapping case the duplication is so cheap that either form is fine — default to duplication for refactor-safety. For 2+ mappings, default to inheritance to reduce duplication.
partialUpdate has its OWN @BeanMapping — no @InheritConfiguration
The partialUpdate method is generated independently and carries exactly ONE annotation:
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.{strategy})
{Entity} partialUpdate({Dto} dto, @MappingTarget {Entity} entity);{strategy} is one of SET_TO_NULL (default) / IGNORE / SET_TO_DEFAULT, picked by the user. Do not add @InheritConfiguration(name = "toEntity") — chasing inherited mappings here only creates surprising failures when toEntity later acquires non-invertible mappings.
Kotlin wrapping rule
In Kotlin, multiple @Mapping annotations must be wrapped:
@org.mapstruct.Mappings(value = [
org.mapstruct.Mapping(source = "field1", target = "field2"),
org.mapstruct.Mapping(source = "field3", target = "field4")
])Single @Mapping can be used directly without wrapping.
MapStruct Mapper -- Java
Skeleton
Read examples/_skeletons/mapstruct-java.md. Write the file to src/main/java/{packagePath}/{className}.java.
Generation order
1. Create file from skeleton 2. Add @Mapper annotation from examples/_fragments/mapper-annotation/java.md
- Determine componentModel: any
spring-boot-starter*orspring-contextinpresentDeps→ SPRING; otherwise → DEFAULT. - CDI is not auto-detected. Use the CDI variant only when the user explicitly asked for
componentModel = "cdi". - For SPRING, use
MappingConstants.ComponentModel.SPRINGform (modern MapStruct).
3. If componentModel = DEFAULT: add MAPPER factory field from examples/_fragments/factory-field/java.md 4. If user selected parent interface: modify declaration from examples/_fragments/parent-interface/java.md 5. Add toEntity method from examples/_fragments/to-entity-method/java.md
- For each DTO field that maps to a different entity field name, add
@Mapping(source, target)annotation - If DTO field has SubDtoType = ID (association ID only), add
@Mapping(source = "dtoField", target = "entityAssoc.id") - If DTO field has SubDtoType = FLAT for a ToOne association (single object, e.g.
typeId/typeNamefromPet.type):
emit one @Mapping(source = "flatDtoField", target = "assoc.nestedField") per flat field — dot-notation, no helper method, no Java-expression. MapStruct synthesizes the intermediate new PetType() and the setters itself.
- If DTO field has SubDtoType = FLAT for a ToMany association (collection), use the helper-method form from
examples/_fragments/flat-expression/java.md(collections cannot use dot-notation).
6. Add toDto method from examples/_fragments/to-dto-method/java.md
- Single `@Mapping` on
toEntity→ emit the symmetric@Mapping(source, target)ontoDto. Duplication is one cheap line and refactor-safe. - Two or more invertible `@Mapping`s on
toEntity(the typical multi-flat-ToOne case) → emit@InheritInverseConfiguration(name = "{toEntityMethodName}")ontoDto. This removes the duplication that grows linearly with flat fields. - Mixed invertible / non-invertible mappings on
toEntity(anyexpression,constant,defaultValue,ignore = true,qualifiedBy, condition) → fall back to explicit duplicated@Mappings ontoDto.@InheritInverseConfigurationsilently breaks in this case. - If entity has AggregateReference attrs with non-ID sub-DTO, add
@Contextparameter
7. If PARTIAL_UPDATE requested: add from examples/_fragments/partial-update-method/java.md
- Pass
{strategyEnum}from the user'spartialUpdateNullStrategyanswer (defaultSET_TO_NULL). - Always emit ONE method with one
@BeanMapping(nullValuePropertyMappingStrategy = ...)— never two methods, never@InheritConfiguration.
8. If UPDATE_WITH_NULL_VALUES requested: add from examples/_fragments/full-update-method/java.md 9. Check entity associations for @AfterMapping need:
- For each non-owner OneToOne or OneToMany with
mappedBywhere DTO uses sub-DTO (not ID, not FLAT): - Add method from
examples/_fragments/after-mapping/java.md
10. If entity has AggregateReference attrs with ID sub-DTO:
- Add
mapToAggregateReferencefromexamples/_fragments/aggregate-ref-to/java.md - Add
mapFromAggregateReferencefromexamples/_fragments/aggregate-ref-from/java.md
11. If DTO has flat collection attributes (SubDtoType = FLAT + collection):
- Add flat expression method from
examples/_fragments/flat-expression/java.md - The toDto method gets
@Mapping(target = "flatField", expression = "java(functionName(entity.getAssoc()))")instead of regular mapping - Known limitation — one-way only. The flat collection helper is
generated only in the entity → DTO direction. The reverse (flatField → entityAssoc) is not generated, and unmappedTargetPolicy = IGNORE will silently leave the collection empty on toEntity. Loading entities by id is the responsibility of the calling service via the JPA repository — do not inject a repository or EntityManager into the mapper. If the user needs the reverse, point them at the service layer instead of trying to generate it. 12. If entity has AggregateReference attrs with non-ID sub-DTO:
- Add context record + helper methods from
examples/_fragments/context-class/java.md
13. `uses = {...}` is rarely needed. MapStruct generates private nested-mapping methods implicitly when both the source and target sub-types are visible to the same mapper interface. Do not emit uses for NEW_CLASS / NEW_NESTED_CLASS sub-DTOs — mapping will be handled implicitly.
- Default: do not add `uses`. Do not call
list_entity_mappersand do not generate a separate sub-mapper file for the association. - Exceptions that DO warrant
uses = {SomeMapper.class}: - the user explicitly asks for an existing sibling mapper to be reused (e.g. complex per-association logic already lives there);
- the association mapping needs a
@Namedqualifier from another mapper; - circular dependencies between mappers that you cannot resolve via implicit generation.
- When you DO need it, use
examples/_fragments/uses-attribute/java.md.
@Mapping annotation rules
For each DTO field, determine mapping type based on entity-DTO field comparison:
- Direct match (same name and type): no
@Mappingneeded - Different name:
@Mapping(source = "dtoFieldName", target = "entityFieldName") - SubDtoType = ID:
@Mapping(source = "dtoField", target = "entityAssoc.id") - SubDtoType = FLAT:
@Mapping(source = "dtoField", target = "assoc.nestedField")or expression for collections - SubDtoType = NEW_CLASS/NEW_NESTED_CLASS/EXIST_CLASS: MapStruct handles via sub-mapper in
uses
Method naming
Default patterns:
- toEntity:
"toEntity" - toDto:
"to{DtoShortName}"(e.g.toOrderDto) - partialUpdate:
"partialUpdate" - updateWithNull:
"updateWithNull" - Collection methods: same as single (LIKE_SINGLE strategy)
MapStruct Mapper -- Kotlin
Skeleton
Read examples/_skeletons/mapstruct-kotlin.md. Write the file to src/main/kotlin/{packagePath}/{className}.kt.
Generation order
1. Create file from skeleton 2. Add @Mapper annotation from examples/_fragments/mapper-annotation/kotlin.md
- Determine componentModel: any
spring-boot-starter*orspring-contextinpresentDeps→ SPRING; otherwise → DEFAULT. - CDI is not auto-detected. Use the CDI variant only when the user explicitly asked for
componentModel = "cdi".
3. If componentModel = DEFAULT: add MAPPER companion object from examples/_fragments/factory-field/kotlin.md 4. If user selected parent interface: modify declaration from examples/_fragments/parent-interface/kotlin.md 5. Add toEntity method from examples/_fragments/to-entity-method/kotlin.md
- Multiple
@Mappingannotations MUST be wrapped in@Mappings(value = [...]) - For each DTO field that maps to a different entity field, add
Mapping(source, target)inside@Mappings
6. Add toDto method from examples/_fragments/to-dto-method/kotlin.md
- If toEntity was created and has
@Mappingannotations, use@InheritInverseConfiguration(name = "toEntity") - Otherwise add individual
@Mapping/@Mappingsannotations - If entity has AggregateReference attrs with non-ID sub-DTO, add
@Contextparameter
7. If PARTIAL_UPDATE requested: add from examples/_fragments/partial-update-method/kotlin.md 8. If UPDATE_WITH_NULL_VALUES requested: add from examples/_fragments/full-update-method/kotlin.md 9. Check entity associations for @AfterMapping need:
- For each non-owner OneToOne or OneToMany with
mappedBywhere DTO uses sub-DTO: - Add method from
examples/_fragments/after-mapping/kotlin.md
10. If entity has AggregateReference attrs with ID sub-DTO:
- Add
mapToAggregateReferencefromexamples/_fragments/aggregate-ref-to/kotlin.md - Add
mapFromAggregateReferencefromexamples/_fragments/aggregate-ref-from/kotlin.md
11. If DTO has flat collection attributes (SubDtoType = FLAT + collection):
- Add flat expression function from
examples/_fragments/flat-expression/kotlin.md
12. If entity has AggregateReference attrs with non-ID sub-DTO:
- Add context data class + helper methods from
examples/_fragments/context-class/kotlin.md
13. Add sub-mapper references to @Mapper(uses = ...) from examples/_fragments/uses-attribute/kotlin.md
- Lazy fetch: for each association entity used by the DTO via a sub-DTO type (NEW_CLASS / NEW_NESTED_CLASS / EXIST_CLASS), call
list_entity_mappers(associationEntityFqn)at this step — not in Step 1 ofSKILL.md. The call needs the association entity FQN, which is only known afterentityDetailsis fetched in Step 2. - Add found mapper FQNs to the
usesattribute. Skip associations with no existing mapper. If no association needs a sub-mapper, skip theusesattribute entirely.
Kotlin-specific differences from Java
- Mapper is
abstract class, notinterface - Methods are
abstract fun, not interface abstract methods - Helper methods (afterMapping, aggregateRef) are regular functions, not
defaultmethods - Multiple
@Mappingannotations MUST be wrapped:@Mappings(value = [Mapping(...), Mapping(...)]) - MAPPER factory uses
companion objectwith@JvmStatic - Parent interface uses
: ParentFqn<Dto, Entity>()(with()) - Custom extension functions use
this.fieldaccess @MappingTargetgoes before param name:@MappingTarget entity: EntityFqn
@Mapping annotation rules
Same as Java (see references/mapstruct-java.md), but annotations wrapped in @Mappings(value = [...]) when multiple.
Method naming
Same as Java (see references/mapstruct-java.md).
Method Naming Conventions
Default patterns
| Method type | Pattern | Example (Entity=Order, DTO=OrderDto) |
|---|---|---|
| toEntity | "toEntity" | toEntity |
| toDto | "to${DTO_NAME}" | toOrderDto |
| partialUpdate | "partialUpdate" | partialUpdate |
| updateWithNull | "updateWithNull" | updateWithNull |
${DTO_NAME} is replaced with the DTO short class name (e.g. OrderDto).
Parameter naming
| Parameter | Rule | Example |
|---|---|---|
| entity param | decapitalized entity short name | order |
| DTO param | decapitalized DTO short name | orderDto |
Collection method naming strategy
Default: LIKE_SINGLE (same name as single-item method).
Other strategies (configurable, but rarely changed):
- PLURALIZE:
toEntities,toOrderDtos - COLLECTION_TYPE:
toEntityList,toOrderDtoList
Custom extension function naming (Kotlin Custom mapper)
- toDto:
fun EntityFqn.toOrderDto()-- extension on entity - toEntity:
fun DtoFqn.toEntity()-- extension on DTO - updateWithNull:
fun EntityFqn.updateWithNull(orderDto: DtoFqn)-- extension on entity with DTO param
Related skills
How it compares
Targeted MapStruct fragment generator—not a full OpenAPI client skill or database migration tool.
FAQ
Who is mapper-creator for?
Independent developers and tiny teams on Spring plus MapStruct who want consistent association wiring in generated mapper interfaces.
When should I use mapper-creator?
During Build/backend work when adding or updating entity-to-DTO mappers that include OneToMany or owning-side OneToOne with mappedBy.
Is mapper-creator safe to install?
It outputs standard MapStruct Java; confirm trust via the Security Audits panel on this Prism catalog page.