# Lombok Remover

> Scan a Java project and replace all Lombok annotations with equivalent hand-written Java code. Use this skill whenever the user wants to remove Lombok, de-lombok a project, replace Lombok annotations, migrate away from Lombok, or eliminate the Lombok dependency. Also trigger when the user says things like "get rid of Lombok", "expand Lombok", or "make Lombok explicit". Handles all common Lombok annotations (@Data, @Builder, @Value, @Getter, @Setter, @ToString, @EqualsAndHashCode, @RequiredArgsConstructor, @AllArgsConstructor, @NoArgsConstructor, @Slf4j, @With, @Accessors, etc.), updates POM files to remove the Lombok dependency, and preserves existing code style throughout.

- Skill: `litsec/lombok-remover` (Agent Skill)
- Install (CLI): `npx skillmds@latest add litsec/lombok-remover`
- Raw SKILL.md: https://api.skillmd.com/api/skills/litsec/lombok-remover/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: litsec (https://skillmd.com/u/litsec)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/litsec/lombok-remover

---


# Lombok Remover

Replaces every Lombok annotation in a Java project with idiomatic, hand-written Java code,
then removes the Lombok dependency from all POM files.

---

## Workflow

### 1. Discover scope

```bash
find . -name "*.java" | xargs grep -l "import lombok\." | sort
find . -name "pom.xml" | sort
```

Collect the full list of affected Java files and POM files upfront.

### 2. Analyse each file before touching it

For every affected Java file, read it fully and build a mental model of:
- Which Lombok annotations are present (may be multiple per class)
- Which fields exist, their types, `final`/non-final, nullability annotations
- Whether the class already has any hand-written methods that would conflict
- Any `@Accessors(chain = true)` or `@Accessors(prefix = ...)` modifiers
- Any `@Builder.Default` field defaults
- Any `@Singular` collections in builders
- Whether the class is a record candidate (all-final fields, no mutation) — note it but
  do NOT convert to a record unless the user explicitly asks

### 3. Generate replacements

Apply the rules in the **Annotation Reference** section below. Generate complete,
correctly indented Java for each annotation removed. Match the file's existing indentation
style (spaces vs tabs, indent width).

### 4. Show a plan, then apply

Before editing any file, print a concise summary:

```
Files to modify: 12
─────────────────────────────────────────────────
 File                          Annotations removed
─────────────────────────────────────────────────
 src/.../UserService.java      @Slf4j
 src/.../User.java             @Data, @Builder
 src/.../AddressDto.java       @Value
 ...
─────────────────────────────────────────────────
POM files: 2  (lombok dependency will be removed)
```

Ask the user to confirm before proceeding. If they say yes, apply all changes.

### 5. Edit files

For each file:

1. Remove all `import lombok.*` and specific `import lombok.XYZ` statements

2. Remove all Lombok annotations from the class / fields

3. Insert generated methods at the **end of the class body**, before the closing `}`,
unless hand-written methods already exist at the bottom (in which case insert before them)

4. For getters and setters, also provide Javadoc according to the style of the project being updated.

5. Do not reformat unrelated code

### 6. Update POMs

In each `pom.xml`, remove the `<dependency>` block for `org.projectlombok:lombok` and
any associated `<annotationProcessorPaths>` entry for Lombok in the
`maven-compiler-plugin` configuration.

### 7. Print a final change log

```
✅ Done
──────────────────────────────────────────────────────────────
Modified 12 Java files, 2 POM files
Removed annotations: @Data ×4, @Builder ×3, @Value ×2, @Slf4j ×6, @Getter ×1
──────────────────────────────────────────────────────────────
Next steps:
  • Run `mvn compile` to verify the project builds cleanly.
  • Review generated equals/hashCode if the class participates in collections or JPA.
```

---

## Annotation Reference

### `@Getter` / `@Setter`

**On a field:**
```java
// @Getter on field  →
public FieldType getFieldName() { return this.fieldName; }

// @Setter on field  →
public void setFieldName(final FieldType fieldName) { this.fieldName = fieldName; }
```

**On a class:** generate getter (and setter for non-final) for every non-static field.

Boolean fields: use `isX()` instead of `getX()` when the type is `boolean` (primitive).

Access levels: `@Getter(AccessLevel.PROTECTED)` etc. → use the matching Java modifier.
`AccessLevel.NONE` → skip that field entirely.

Provide Javadoc for inserted getters and setters according to the style of the project being updated.

---

### `@ToString`

```java
@Override
public String toString() {
    return "ClassName{" +
            "field1=" + field1 +
            ", field2='" + field2 + ''' +
            '}';
}
```

- `@ToString(exclude = {"field2"})` / `@ToString.Exclude` → omit those fields
- `@ToString(onlyExplicitlyIncluded = true)` + `@ToString.Include` → include only marked fields
- `@ToString(callSuper = true)` → prepend `super.toString() + ", "`

Quote String fields with `'` as shown; for arrays use `Arrays.toString(field)`.

---

### `@EqualsAndHashCode`

```java
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    ClassName other = (ClassName) o;
    return Objects.equals(field1, other.field1) &&
           Objects.equals(field2, other.field2);
}

@Override
public int hashCode() {
    return Objects.hash(field1, field2);
}
```

- `@EqualsAndHashCode(exclude = {...})` / `@EqualsAndHashCode.Exclude` → omit those fields
- `@EqualsAndHashCode(onlyExplicitlyIncluded = true)` + `@EqualsAndHashCode.Include` → only marked
- `@EqualsAndHashCode(callSuper = true)` → chain to `super.equals()` / `super.hashCode()`
- Primitive fields: use `==` instead of `Objects.equals`

Add `import java.util.Objects;` if not already present.

---

### `@NoArgsConstructor`

```java
public ClassName() {
}
```

`@NoArgsConstructor(access = AccessLevel.PROTECTED)` → use `protected`.
`@NoArgsConstructor(force = true)` → initialize final fields to their default zero values.

---

### `@AllArgsConstructor`

```java
public ClassName(Type1 field1, Type2 field2) {
    this.field1 = field1;
    this.field2 = field2;
}
```

Include all non-static fields in declaration order. Respect `access` attribute.

---

### `@RequiredArgsConstructor`

Generate a constructor for all `final` fields and all fields annotated `@NonNull`
(that are not initialized at declaration). Same body pattern as `@AllArgsConstructor`.

---

### `@Data`

Equivalent to: `@Getter` + `@Setter` (non-final fields) + `@ToString` + `@EqualsAndHashCode` + `@RequiredArgsConstructor`.

Generate all of those in this order: constructor, getters, setters, equals, hashCode, toString.

---

### `@Value`

Equivalent to: make class `final`, make all fields `private final`, `@Getter` + `@ToString` + `@EqualsAndHashCode` + `@AllArgsConstructor`.

No setters. The class declaration gains `final` if not already present.

---

### `@Builder`

Generate a static inner `Builder` class:

```java
public static Builder builder() {
    return new Builder();
}

public static final class Builder {
    private Type1 field1;
    private Type2 field2;

    private Builder() {}

    public Builder field1(Type1 field1) {
        this.field1 = field1;
        return this;
    }

    public Builder field2(Type2 field2) {
        this.field2 = field2;
        return this;
    }

    public ClassName build() {
        return new ClassName(field1, field2);
    }
}

// Private all-args constructor used by Builder:
private ClassName(Type1 field1, Type2 field2) {
    this.field1 = field1;
    this.field2 = field2;
}
```

**`@Builder.Default`**: the field's declared initializer becomes the default in the Builder
field declaration (e.g. `private List<String> tags = new ArrayList<>();`).

**`@Singular`**: generate an `add` method and a `addAll` method, plus initialize the field
to an empty mutable list/set/map in the Builder, and make the `build()` call
`Collections.unmodifiableList(new ArrayList<>(tags))`.

**`@Builder(toBuilder = true)`**: add a `toBuilder()` instance method that pre-populates
a new Builder from `this`.

---

### `@SuperBuilder`

Same as `@Builder` but uses an abstract base builder pattern to support inheritance.
This is complex — generate the abstract `BaseBranchBuilder` / concrete builder pair
as Lombok does. Flag it clearly in the change log as requiring extra review.

---

### `@With` / `@Wither`

For each annotated field, generate a copy-constructor-style "wither":

```java
public ClassName withFieldName(FieldType fieldName) {
    return new ClassName(/* all fields, replacing fieldName */);
}
```

Requires an all-args constructor to be present (or generated).

---

### `@Slf4j` / `@Log` / `@Log4j2` / `@CommonsLog`

| Annotation      | Generated field |
|-----------------|-----------------|
| `@Slf4j`        | `private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ClassName.class);` |
| `@Log4j2`       | `private static final org.apache.logging.log4j.Logger log = org.apache.logging.log4j.LogManager.getLogger(ClassName.class);` |
| `@Log`          | `private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(ClassName.class.getName());` |
| `@CommonsLog`   | `private static final org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(ClassName.class);` |

Insert as the first field in the class body. Add the corresponding import.

---

### `@Accessors`

- `chain = true` → setters return `this` instead of `void`
- `prefix = {"m_"}` → strip the prefix when computing getter/setter names
- `fluent = true` → getters and setters use the bare field name with no `get`/`set` prefix,
  and setters return `this`

Apply these modifiers to every getter/setter generated for this class.

---

### `@NonNull`

On a parameter or field: insert a null check at the top of the constructor/setter body:
```java
if (fieldName == null) throw new NullPointerException("fieldName is marked non-null but is null");
```

Keep any JSR-305 / JetBrains / Jakarta `@NonNull` annotations that are used for static
analysis — only remove the `lombok.NonNull` import/annotation.

---

### `@Cleanup`

```java
// @Cleanup InputStream in = new FileInputStream(file);
// becomes:
InputStream in = new FileInputStream(file);
try {
    // ... rest of the original block ...
} finally {
    if (in != null) in.close();
}
```

---

### `@SneakyThrows`

Wrap the method body in a try-catch that rethrows via an unchecked wrapper:

```java
try {
    // original body
} catch (Exception e) {
    throw new RuntimeException(e);
}
```

Add `// TODO: review exception handling (was @SneakyThrows)` comment.

---

### `@Delegate`

Implement every method of the delegated type by forwarding to the delegate field.
This may generate many methods — note the count in the change log and add a
`// Generated by lombok-remover: @Delegate` comment block.

---

## Edge Cases

- **Existing hand-written methods**: if a getter/setter/toString/equals/hashCode already
  exists, skip generating that one and log a note.
- **Inheritance**: if the class extends another class, check whether `callSuper` should
  default to `true` for `@EqualsAndHashCode` / `@ToString`. Lombok's default is `false`
  — preserve that unless `callSuper = true` was explicitly set.
- **JPA entities**: add a `// TODO: review equals/hashCode for JPA entity` comment when
  the class has `@Entity` or `@Table`.
- **Records**: do not convert to records unless asked.
- **Interfaces / abstract classes**: handle appropriately — e.g. `@Getter` on an
  abstract class generates abstract getters only if the field is abstract (unusual).
- **Lombok config (`lombok.config`)**: check if a `lombok.config` file overrides defaults
  (e.g. `lombok.addLombokGeneratedAnnotation`, `lombok.accessors.chain`). Read it if present.

---

## POM Cleanup

Remove:
```xml
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    ...
</dependency>
```

And if present in `maven-compiler-plugin`:
```xml
<annotationProcessorPaths>
    <path>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </path>
</annotationProcessorPaths>
```

If the `<annotationProcessorPaths>` block becomes empty after removal, remove the block
too. If `<compilerArgs>` only contained Lombok-related args, clean those up as well.

---

## Required Imports

When generating code, ensure all required imports are present and no unused Lombok imports
remain. Common imports to add:

- `java.util.Objects` — for `equals` / `hashCode`
- `java.util.Arrays` — for array fields in `toString`
- `java.util.Collections` — for `@Singular` builder fields
- Logger class imports — for log field generation

