emvnuel/skills · Archived

mapstruct-patterns

Constructor-based MapStruct mapping for compile-time safety in Jakarta EE. Use when implementing DTO mapping or using @Default annotations.

First seen Jan 27, 2026

Installation

$ npx skills add emvnuel/skills --skill mapstruct-patterns

Stronger alternatives

This repository is archived — consider an actively maintained alternative.

Also in this package

Other skills from emvnuel/skills.

npx skills add emvnuel/skills

Browse all from emvnuel/skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 2
License LICENSE
Default branch main
Open issues 0
Status Archived

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 4,360 B
  • docs SUMMARY.md 165 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 18 installs

SKILL.md

MapStruct Patterns for Jakarta EE

Best practices for using MapStruct with constructor-based mapping to achieve compile-time safety. When constructors change, mappings fail to compile — no runtime surprises.

Core Philosophy

Use constructors, not setters. This gives you compile-time errors when fields change.

Records naturally enforce this. For mutable entities, use the @Default annotation.

CDI Setup

@Mapper(componentModel = "cdi")  // CDI injection
public interface OrderMapper {
    OrderResponse toResponse(Order order);
}

The @Default Annotation Trick

MapStruct uses any annotation named @Default to select the constructor. Create your own:

package com.example.mapstruct;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.CONSTRUCTOR)
@Retention(RetentionPolicy.CLASS)
public @interface Default {
}

Usage on Mutable Entities

@Entity
public class Order {

    @Id @GeneratedValue
    private Long id;
    private String customerId;
    private BigDecimal total;
    private OrderStatus status;

    // JPA needs this
    protected Order() {}

    // MapStruct uses this - CHANGE HERE = COMPILER ERROR in mapper
    @Default
    public Order(String customerId, BigDecimal total, OrderStatus status) {
        this.customerId = customerId;
        this.total = total;
        this.status = status;
    }
}

Records (Ideal Case)

Records automatically work with constructor mapping:

// No @Default needed - single constructor
public record OrderResponse(
    String orderId,
    String customerId,
    String total,
    String status
) {}

@Mapper(componentModel = "cdi")
public interface OrderMapper {

    @Mapping(target = "orderId", source = "id")
    @Mapping(target = "total", expression = "java(order.getTotal().toString())")
    OrderResponse toResponse(Order order);
}

Key Patterns

1. Constructor-Based Mapping

@Mapper(componentModel = "cdi")
public interface CustomerMapper {

    // MapStruct uses Customer constructor, fail if signature changes
    Customer toEntity(CreateCustomerRequest request);

    // MapStruct uses CustomerResponse constructor
    CustomerResponse toResponse(Customer customer);
}

2. Custom @Default for Entities

@Entity
public class Product {

    @Id @GeneratedValue
    private Long id;
    private String name;
    private BigDecimal price;
    private String category;

    protected Product() {}

    @Default  // Your custom annotation
    public Product(String name, BigDecimal price, String category) {
        this.name = name;
        this.price = price;
        this.category = category;
    }
}

Anti-Pattern: Setter-Based Mapping

// ❌ Can add field to DTO, forget mapper, get null at runtime
public class OrderDTO {
    private String id;
    private String status;
    private String newField;  // Added later, no error!

    // Just setters...
}

// ✓ Add field to constructor = compiler error in mapper
public record OrderDTO(String id, String status, String newField) {}

Compile-Time Safety Benefit

// Before: Record has 3 fields
public record OrderResponse(String id, String status, String total) {}

// After: Added customerName field
public record OrderResponse(String id, String status, String total, String customerName) {}

// Mapper now FAILS TO COMPILE until you add the mapping:
@Mapper(componentModel = "cdi")
public interface OrderMapper {
    @Mapping(target = "customerName", source = "customer.name")  // Must add this
    OrderResponse toResponse(Order order);
}

Cookbook Index

Setup & Configuration

  • [cdi-setup](cookbook/cdi-setup.md) - CDI/MicroProfile setup
  • [default-annotation](cookbook/default-annotation.md) - Custom @Default annotation

Mapping Patterns

  • [constructor-mapping](cookbook/constructor-mapping.md) - Constructor-based mapping
  • [record-mapping](cookbook/record-mapping.md) - Java Records mapping
  • [entity-mapping](cookbook/entity-mapping.md) - JPA entity mapping