>
---
name: mapper-creator
description: >
Creates a mapper between an entity and a DTO (MapStruct or custom converter).
Use this skill when a mapper/converter between entity and DTO needs to be created,
either standalone or as part of a larger task (e.g. after DTO creation, during CRUD setup).
---
# Preflight: Amplicode MCP
This skill is part of the **Spring Agent Toolkit** and is designed to work with the **Amplicode MCP server** (provided by the Amplicode IntelliJ plugin). Before doing anything else, check your tool list for any Amplicode 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-install` is 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.
- `header` is a 12-char chip label (e.g. "Mapper", "Methods", "Package").
- Each option has a `description` explaining 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*` or `spring-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}Mapper`
- `packageName` = same package as the DTO
- `partialUpdate` = no
- `updateWithNull` = 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 (see `references/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}")` (apply `kotlin("kapt")` plugin if not present) or `ksp(...)` 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 (`compile` for `mapstruct`, processor configured via `maven-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 `import` lines were added after `package` (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 — `@InheritInverseConfiguration` only when the user explicitly asked for it
- [ ] Flat collection helper is generated only in `toDto` direction; `toEntity` does NOT try to load entities by id (no repository injection)
- [ ] Flat collection helper name follows `{assocFieldName}To{capitalize(flatDtoFieldName)}` (e.g. `petsToPetIds`, not `petsToId`)
Creator's repository · amplicode/spring-skills