diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/Ignore4BuilderGeneration.java b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/Ignore4BuilderGeneration.java
new file mode 100644
index 00000000..5e70c7b0
--- /dev/null
+++ b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/Ignore4BuilderGeneration.java
@@ -0,0 +1,46 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2026 Andreas Igel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.javahelpers.simple.builders.core.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Placed on a class/record to exclude it from builder generation, even if {@code @SimpleBuilder} or
+ * a {@code @SimpleBuilder.Template} annotation is inherited from a parent type.
+ *
+ *
A type marked with this annotation is treated as having no builder available: other builders
+ * never reference a builder for it and fall back to plain setters.
+ *
+ *
This annotation is intentionally not {@code @Inherited}. It only suppresses builder
+ * generation for the exact type it is placed on and does not cascade to further subclasses, which
+ * may therefore still get a builder from an inherited {@code @SimpleBuilder} or template
+ * annotation.
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.CLASS)
+public @interface Ignore4BuilderGeneration {}
diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java
index 09111033..193ebd45 100644
--- a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java
+++ b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java
@@ -54,8 +54,19 @@
*
*
Use {@link Template} to create reusable configuration presets.
*
+ *
Related annotations:
+ *
+ *
+ * - {@link IgnoreInBuilder} - exclude individual setters/constructors from builder generation
+ *
- {@link Ignore4BuilderGeneration} - exclude a class/record from builder generation, even
+ * when an inherited {@code @SimpleBuilder} or {@code @SimpleBuilder.Template} would otherwise
+ * trigger it
+ *
+ *
* @see Options
* @see Template
+ * @see IgnoreInBuilder
+ * @see Ignore4BuilderGeneration
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md
index 7534ad7b..ee10d325 100644
--- a/docs/CONFIGURATION.md
+++ b/docs/CONFIGURATION.md
@@ -7,6 +7,7 @@ Simple-builders supports fine-grained configuration through the `@SimpleBuilder.
- [Overview](#overview)
- [Annotation Configuration](#annotation-configuration)
- [Template Annotations](#template-annotations)
+- [Excluding Types from Builder Generation](#excluding-types-from-builder-generation)
- [Compiler Options](#compiler-options)
- [Maven Configuration](#maven-configuration)
- [Gradle Configuration](#gradle-configuration)
@@ -117,6 +118,22 @@ public class PersonDto {
}
```
+## Excluding Types from Builder Generation
+
+You can opt a whole DTO out of builder generation with `@Ignore4BuilderGeneration`. This is useful when a class inherits `@SimpleBuilder` or an `@SimpleBuilder.Template` annotation from a parent and you do not want a builder for that specific subclass.
+
+```java
+import org.javahelpers.simple.builders.core.annotations.Ignore4BuilderGeneration;
+
+@Ignore4BuilderGeneration
+public class IgnoredDto extends ParentDto {
+ // No builder will be generated for IgnoredDto, even if ParentDto carries
+ // @SimpleBuilder or an @Inherited template annotation.
+}
+```
+
+A type marked with `@Ignore4BuilderGeneration` is treated as having **no builder available**. Other builders that reference it will fall back to plain setters instead of emitting nested-builder consumers. The annotation is intentionally **not** `@Inherited`, so it only suppresses the exact type it is placed on and does not cascade to further subclasses.
+
## Compiler Options
Set project-wide defaults via compiler options. These apply to all builders unless overridden by annotations.
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 1c5786d0..5efaf2bd 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -32,6 +32,7 @@
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
+import java.util.Optional;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
@@ -39,10 +40,13 @@
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.SourceVersion;
+import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
+import org.javahelpers.simple.builders.core.annotations.Ignore4BuilderGeneration;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilder.Template;
+import org.javahelpers.simple.builders.processor.analysis.JavaLangAnalyser;
import org.javahelpers.simple.builders.processor.classgen.roaster.RoasterCodeGenerator;
import org.javahelpers.simple.builders.processor.exceptions.BuilderException;
import org.javahelpers.simple.builders.processor.generators.integration.JacksonModuleGenerator;
@@ -150,6 +154,23 @@ public boolean process(Set extends TypeElement> annotations, RoundEnvironment
elementsToProcess.addAll(roundEnv.getElementsAnnotatedWith(annotation));
}
+ // Filter out elements explicitly opted out via @Ignore4BuilderGeneration.
+ // Only directly-declared annotations are checked: the type itself must carry
+ // the opt-out; a parent carrying it does not cascade, matching the fact that
+ // @Ignore4BuilderGeneration is intentionally NOT @Inherited.
+ elementsToProcess.removeIf(
+ element -> {
+ Optional ignoreAnnotation =
+ JavaLangAnalyser.findAnnotation(element, Ignore4BuilderGeneration.class);
+ if (ignoreAnnotation.isPresent()) {
+ context.debug(
+ "Skipping element '%s' due to @Ignore4BuilderGeneration opt-out.",
+ element.getSimpleName());
+ return true;
+ }
+ return false;
+ });
+
context.info("simple-builders: PROCESSING ROUND START");
context.debug(
"simple-builders: Processing round started. Found %d annotated elements.",
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangMapper.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangMapper.java
index fbe566b0..d7610c4e 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangMapper.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangMapper.java
@@ -43,6 +43,7 @@
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.util.SimpleTypeVisitor14;
+import org.javahelpers.simple.builders.core.annotations.Ignore4BuilderGeneration;
import org.javahelpers.simple.builders.core.enums.AccessModifier;
import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto;
import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto;
@@ -214,6 +215,11 @@ private static void setBuilderAndConstructorInfo(
*/
private static void setBuilderTypeIfAnnotated(
TypeName typeName, TypeElement typeElement, ProcessingContext context) {
+ // Types explicitly opted out must never be referenced as builders by other DTOs.
+ if (JavaLangAnalyser.findAnnotation(typeElement, Ignore4BuilderGeneration.class).isPresent()) {
+ return;
+ }
+
Optional foundBuilderAnnotation =
JavaLangAnalyser.findAnnotation(
typeElement, org.javahelpers.simple.builders.core.annotations.SimpleBuilder.class);
@@ -270,6 +276,12 @@ private static void setElementBuilderTypeForGenericCollections(
return;
}
+ // Opted-out element types must never be referenced as element builders.
+ if (JavaLangAnalyser.findAnnotation(elementTypeElement, Ignore4BuilderGeneration.class)
+ .isPresent()) {
+ return;
+ }
+
// Element type must have @SimpleBuilder annotation
if (!hasSimpleBuilderAnnotation(elementTypeElement)) {
return;
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/Ignore4BuilderGenerationTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/Ignore4BuilderGenerationTest.java
new file mode 100644
index 00000000..58b9b9ff
--- /dev/null
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/Ignore4BuilderGenerationTest.java
@@ -0,0 +1,219 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2026 Andreas Igel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.javahelpers.simple.builders.processor;
+
+import static com.google.testing.compile.CompilationSubject.assertThat;
+import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.assertContaining;
+import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.assertGenerationSucceeded;
+import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.contains;
+import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.notContains;
+import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.loadGeneratedSource;
+
+import com.google.testing.compile.Compilation;
+import javax.tools.JavaFileObject;
+import org.javahelpers.simple.builders.processor.testing.ProcessorAsserts;
+import org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/** Tests for the {@code @Ignore4BuilderGeneration} opt-out annotation. */
+class Ignore4BuilderGenerationTest {
+
+ private Compilation compile(JavaFileObject... sourceFiles) {
+ return ProcessorTestUtils.createCompiler().compile(sourceFiles);
+ }
+
+ /**
+ * (a) A subclass that inherits a template annotation from its parent and is annotated with
+ * {@code @Ignore4BuilderGeneration} must NOT get a builder, while the parent's builder is still
+ * generated.
+ */
+ @Test
+ void subclassWithInheritedTemplateAndOptOutGeneratesParentButNotChild() {
+ JavaFileObject templateAnnotation =
+ ProcessorTestUtils.forSource(
+ """
+ package test;
+
+ import java.lang.annotation.ElementType;
+ import java.lang.annotation.Inherited;
+ import java.lang.annotation.Retention;
+ import java.lang.annotation.RetentionPolicy;
+ import java.lang.annotation.Target;
+ import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
+
+ @SimpleBuilder.Template(options = @SimpleBuilder.Options())
+ @Inherited
+ @Retention(RetentionPolicy.CLASS)
+ @Target(ElementType.TYPE)
+ public @interface InheritedTemplate {}
+ """);
+
+ JavaFileObject parentSource =
+ ProcessorTestUtils.forSource(
+ """
+ package test;
+
+ @InheritedTemplate
+ public class ParentDto {
+ private String name;
+
+ public String getName() { return name; }
+ public void setName(String name) { this.name = name; }
+ }
+ """);
+
+ JavaFileObject childSource =
+ ProcessorTestUtils.forSource(
+ """
+ package test;
+
+ import org.javahelpers.simple.builders.core.annotations.Ignore4BuilderGeneration;
+
+ @Ignore4BuilderGeneration
+ public class ChildDto extends ParentDto { }
+ """);
+
+ Compilation compilation = compile(templateAnnotation, parentSource, childSource);
+
+ assertThat(compilation).succeededWithoutWarnings();
+
+ String parentBuilder = loadGeneratedSource(compilation, "ParentDtoBuilder");
+ assertGenerationSucceeded(compilation, "ParentDtoBuilder", parentBuilder);
+ assertContaining(parentBuilder, "public ParentDtoBuilder name(String name)");
+
+ Assertions.assertTrue(
+ compilation.generatedSourceFiles().stream()
+ .noneMatch(f -> f.getName().endsWith("ChildDtoBuilder.java")),
+ "ChildDto builder should not have been generated due to @Ignore4BuilderGeneration");
+ }
+
+ /**
+ * (b) A DTO field whose type is annotated with {@code @Ignore4BuilderGeneration} must not trigger
+ * nested-builder consumer generation in the referencing DTO. The referencing builder falls back
+ * to a plain setter and must not reference a non-existent builder class.
+ */
+ @Test
+ void referencedOptedOutDtoFallsBackToPlainSetter() {
+ JavaFileObject ignoredDto =
+ ProcessorTestUtils.forSource(
+ """
+ package test;
+
+ import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
+ import org.javahelpers.simple.builders.core.annotations.Ignore4BuilderGeneration;
+
+ @SimpleBuilder
+ @Ignore4BuilderGeneration
+ public class IgnoredDto {
+ private String value;
+
+ public String getValue() { return value; }
+ public void setValue(String value) { this.value = value; }
+ }
+ """);
+
+ JavaFileObject containerDto =
+ ProcessorTestUtils.forSource(
+ """
+ package test;
+
+ import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
+
+ @SimpleBuilder
+ public class ContainerDto {
+ private IgnoredDto ignored;
+
+ public IgnoredDto getIgnored() { return ignored; }
+ public void setIgnored(IgnoredDto ignored) { this.ignored = ignored; }
+ }
+ """);
+
+ Compilation compilation = compile(ignoredDto, containerDto);
+
+ assertThat(compilation).succeededWithoutWarnings();
+
+ Assertions.assertTrue(
+ compilation.generatedSourceFiles().stream()
+ .noneMatch(f -> f.getName().endsWith("IgnoredDtoBuilder.java")),
+ "IgnoredDto builder should not have been generated");
+
+ String containerBuilder = loadGeneratedSource(compilation, "ContainerDtoBuilder");
+ assertGenerationSucceeded(compilation, "ContainerDtoBuilder", containerBuilder);
+
+ ProcessorAsserts.assertingResult(
+ containerBuilder,
+ contains("public ContainerDtoBuilder ignored(IgnoredDto ignored)"),
+ notContains("IgnoredDtoBuilder"),
+ notContains("Consumer"),
+ notContains("Consumer"));
+ }
+
+ /**
+ * (c) Regression: a normally annotated DTO without the opt-out still generates its builder and is
+ * still referenced by other DTOs.
+ */
+ @Test
+ void normalDtoStillGeneratesBuilderAndIsReferencedByOthers() {
+ JavaFileObject normalDto =
+ ProcessorTestUtils.simpleBuilderClass(
+ "test",
+ "NormalDto",
+ """
+ private String value;
+
+ public String getValue() { return value; }
+ public void setValue(String value) { this.value = value; }
+ """);
+
+ JavaFileObject containerDto =
+ ProcessorTestUtils.forSource(
+ """
+ package test;
+
+ import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
+
+ @SimpleBuilder
+ public class ReferenceDto {
+ private NormalDto normal;
+
+ public NormalDto getNormal() { return normal; }
+ public void setNormal(NormalDto normal) { this.normal = normal; }
+ }
+ """);
+
+ Compilation compilation = compile(normalDto, containerDto);
+
+ assertThat(compilation).succeededWithoutWarnings();
+
+ String normalBuilder = loadGeneratedSource(compilation, "NormalDtoBuilder");
+ assertGenerationSucceeded(compilation, "NormalDtoBuilder", normalBuilder);
+
+ String referenceBuilder = loadGeneratedSource(compilation, "ReferenceDtoBuilder");
+ assertGenerationSucceeded(compilation, "ReferenceDtoBuilder", referenceBuilder);
+ assertContaining(
+ referenceBuilder, "public ReferenceDtoBuilder normal(Consumer");
+ }
+}