# Mapper Creator

> 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).

- Skill: `amplicode/mapper-creator` (Agent Skill, multi-file: 43 files)
- Install (CLI): `npx skillmds@latest add amplicode/mapper-creator`
- Raw SKILL.md: https://api.skillmd.com/api/skills/amplicode/mapper-creator/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- Author: amplicode (https://skillmd.com/u/amplicode)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/amplicode/mapper-creator

---


# 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-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`)

