Simple-builders supports fine-grained configuration through the @SimpleBuilder.Options annotation and compiler options.
- Overview
- Annotation Configuration
- Template Annotations
- Excluding Types from Builder Generation
- Generating Builders for External Types
- Compiler Options
- Configuration Options
- Examples
- Priority Rules
- AccessModifier Enum
- Troubleshooting
- Best Practices
- Reference
Configuration follows a priority system:
- Annotation values - Highest priority
- Compiler options - Medium priority
- Default values - Lowest priority
This allows you to set project-wide defaults while still being able to override them per-class when needed.
Configure individual builders using @SimpleBuilder with @SimpleBuilder.Options:
@SimpleBuilder
@SimpleBuilder.Options(
generateFieldSupplier = OptionState.ENABLED,
generateFieldConsumer = OptionState.ENABLED,
generateBuilderConsumer = OptionState.ENABLED,
generateConditionalHelper = OptionState.ENABLED,
builderAccess = AccessModifier.PUBLIC,
methodAccess = AccessModifier.PUBLIC,
generateVarArgsHelpers = OptionState.ENABLED,
usingArrayListBuilder = OptionState.ENABLED,
usingHashMapBuilder = OptionState.ENABLED,
generateWithInterface = OptionState.ENABLED
)
public class PersonDto {
private String name;
private int age;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
}@SimpleBuilder.Template is a meta-annotation: it is placed on a custom annotation declaration (@interface), not directly on a class or record. Use it to create reusable configuration presets that can be applied to many classes with a single custom annotation. @SimpleBuilder itself is the built-in template: it is meta-annotated with @SimpleBuilder.Template and can be used directly on a class for one-off builder generation, or you can define your own custom template annotations.
| Need | Use |
|---|---|
| Generate a builder for a single class/record | @SimpleBuilder on the class/record |
| Generate a minimal builder (no optional features) | @SimpleMinimalBuilder on the class/record |
| Share the same configuration across many classes | @SimpleBuilder.Template on a custom @interface, then the custom annotation on each class |
Create reusable configuration presets with custom template annotations. The built-in @SimpleMinimalBuilder already disables every optional feature; use the following pattern only when you need a differently named or customized template:
@SimpleBuilder.Template(options = @SimpleBuilder.Options(
generateFieldSupplier = OptionState.DISABLED,
generateFieldConsumer = OptionState.DISABLED,
generateBuilderConsumer = OptionState.DISABLED,
generateConditionalHelper = OptionState.DISABLED,
generateVarArgsHelpers = OptionState.DISABLED,
generateStringFormatHelpers = OptionState.DISABLED,
generateUpdateHelpers = OptionState.DISABLED,
generateAddToCollectionHelpers = OptionState.DISABLED,
generateUnboxedOptional = OptionState.DISABLED,
copyTypeAnnotations = OptionState.DISABLED,
usingArrayListBuilder = OptionState.DISABLED,
usingArrayListBuilderWithElementBuilders = OptionState.DISABLED,
usingHashSetBuilder = OptionState.DISABLED,
usingHashSetBuilderWithElementBuilders = OptionState.DISABLED,
usingHashMapBuilder = OptionState.DISABLED,
generateWithInterface = OptionState.DISABLED,
usingGeneratedAnnotation = OptionState.DISABLED,
usingBuilderImplementationAnnotation = OptionState.DISABLED,
implementsBuilderBase = OptionState.DISABLED,
usingJacksonDeserializerAnnotation = OptionState.DISABLED,
generateJacksonModule = OptionState.DISABLED,
generateJavaDoc = OptionState.DISABLED
))
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface MinimalBuilder {}Then use your template:
@MinimalBuilder // No need for @SimpleBuilder - template includes it!
public class PersonDto {
private String name;
}To make a template annotation propagate to unannotated subclasses, add @Inherited to it. @SimpleBuilder itself is @Inherited, and @SimpleBuilder.Template is @Inherited as well, but the custom annotation must also declare @Inherited for the processor to pick up subclasses:
@SimpleBuilder.Template(options = @SimpleBuilder.Options(...))
@Inherited
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface MinimalBuilder {}
@MinimalBuilder
public class ParentDto { ... }
// ChildDto also gets a builder, because @MinimalBuilder is @Inherited.
public class ChildDto extends ParentDto { ... }Note: Inherited subclasses get a builder that also uses the options declared on the parent's
@SimpleBuilder(options = ...)or template. If the subclass declares its own@SimpleBuilderor template annotation, those options take precedence.
You can opt a whole DTO out of builder generation with @Ignore4BuilderGeneration. This is useful when a class inherits @SimpleBuilder (which is itself @Inherited) or an @SimpleBuilder.Template-based annotation from a parent and you do not want a builder for that specific subclass.
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.
@SimpleBuilder has to be placed on the type itself, which is not possible for types you cannot modify - for example classes or records from third-party libraries. @SimpleBuilderFor covers this case: declare it on the package itself in package-info.java (or on a dedicated provider class in your own code) and list the types a builder is generated for.
// package-info.java - generates ExternalUserFactory and ExternalOrderFactory into this package
@SimpleBuilderFor(
value = {ExternalUser.class, ExternalOrder.class},
options = @SimpleBuilder.Options(builderSuffix = "Factory"))
package com.example;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor;The generated builders are placed in the annotated package - or in the package of the provider class when a class is annotated instead. options reuses @SimpleBuilder.Options and is optional - compiler defaults apply when omitted.
sourcePackages lists whole packages to scan instead of naming types one by one: every top-level constructible type declared in a listed package gets a builder, combined with the types of value (an explicit value entry wins over the same type found by scanning; a sourcePackages-only declaration without value is valid). It names the packages the types come from - not to be confused with options.packageName, the package builders are generated into:
// Generates a builder for every top-level class and record in com.thirdparty.users
@SimpleBuilderFor(
sourcePackages = "com.thirdparty.users",
options = @SimpleBuilder.Options(packageName = "com.example.generated"))
public class ExternalBuildersProvider {
}Interfaces, abstract classes, enums and annotation types are skipped (a debug log lists each skip), and package scanning only sees top-level types - nested types keep requiring an explicit value entry. Only packages visible to the compilation can be resolved: an unresolvable package name produces a warning and is skipped. Types a package contributes are ordered by qualified name, so the generated set is deterministic.
The target type must be constructible through accessible Java APIs from the builder's package: it needs a visible type and an accessible constructor, otherwise generation fails with a compile-time diagnostic. The target type's own annotations are not consulted - the explicit declaration wins, so @Ignore4BuilderGeneration on the target does not suppress generation either. @SimpleBuilderFor is not @Inherited, and the provider class or package itself never gets a builder.
@SimpleBuilderFor is @Repeatable: several declarations may be placed on the same holder or package-info.java when the listed types need different options - for example a different packageName per declaration:
@SimpleBuilderFor(
value = ExternalUser.class,
options = @SimpleBuilder.Options(packageName = "com.example.users"))
@SimpleBuilderFor(
value = ExternalOrder.class,
options = @SimpleBuilder.Options(packageName = "com.example.orders"))
public class ExternalBuildersProvider {
}Set project-wide defaults via compiler options. These apply to all builders unless overridden by annotations.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
<annotationProcessorPaths>
<path>
<groupId>io.github.java-helpers</groupId>
<artifactId>simple-builders-processor</artifactId>
<version>${simple-builders.version}</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>-Asimplebuilder.generateFieldSupplier=ENABLED</arg>
<arg>-Asimplebuilder.generateFieldConsumer=ENABLED</arg>
<arg>-Asimplebuilder.builderAccess=PUBLIC</arg>
<arg>-Asimplebuilder.usingArrayListBuilder=ENABLED</arg>
</compilerArgs>
</configuration>
</plugin>dependencies {
annotationProcessor "io.github.java-helpers:simple-builders-processor:${simpleBuildersVersion}"
}
compileJava {
options.compilerArgs += [
"-Asimplebuilder.generateFieldSupplier=ENABLED",
"-Asimplebuilder.generateFieldConsumer=ENABLED",
"-Asimplebuilder.builderAccess=PUBLIC"
]
}- Go to Settings → Build, Execution, Deployment → Compiler → Java Compiler
- Add to Additional command line parameters:
-Asimplebuilder.generateFieldSupplier=true -Asimplebuilder.builderAccess=PUBLIC
Maven and Gradle accept the options as JVM system properties without a build-file change, for example mvn compile -Dsimplebuilder.generateJavaDoc=DISABLED or gradle compileJava -Dsimplebuilder.generateJavaDoc=DISABLED.
The precedence is @SimpleBuilder.Options > -D system property > -A compiler argument > default.
The system property wins so you can override options configured in the build file from the command line without editing it.
This relies on javac running in the build tool's JVM (the default for Maven and Gradle); use the -A form for forked compilation (<fork>true</fork> / options.fork = true) and IDE builds. Gradle also accepts systemProp.simplebuilder.<option>=... in gradle.properties; note that Gradle does not track the property as a task input, so run clean or --rerun-tasks after changing it.
All options use OptionState enum with values: ENABLED, DISABLED, or UNSET (uses default/compiler arg).
Default: ENABLED | Compiler Option: -Asimplebuilder.generateFieldSupplier=ENABLED|DISABLED
Generates setter methods that accept Supplier<T> for lazy field value initialization.
When ENABLED:
// Generated method
public PersonDtoBuilder name(Supplier<String> nameSupplier) {
this.name = changedValue(nameSupplier.get());
return this;
}
// Usage
PersonDto person = PersonDtoBuilder.create()
.name(() -> expensiveNameComputation())
.build();When DISABLED: No Supplier<> setter methods are generated.
Default: ENABLED | Compiler Option: -Asimplebuilder.generateFieldConsumer=ENABLED|DISABLED
Generates setter methods that accept Consumer<StringBuilder> for String fields, allowing fluent string building.
When ENABLED:
// Generated method for String fields
public PersonDtoBuilder name(Consumer<StringBuilder> nameConsumer) {
StringBuilder builder = new StringBuilder();
nameConsumer.accept(builder);
this.name = changedValue(builder.toString());
return this;
}
// Usage
PersonDto person = PersonDtoBuilder.create()
.name(sb -> sb.append("Dr. ").append(firstName).append(" ").append(lastName))
.build();When DISABLED: No Consumer<StringBuilder> methods are generated.
Default: ENABLED | Compiler Option: -Asimplebuilder.generateBuilderConsumer=ENABLED|DISABLED
Generates setter methods that accept Consumer<Builder> for complex nested objects and collections.
When ENABLED:
// Generated method for collection fields
public PersonDtoBuilder tags(Consumer<ArrayListBuilder<String>> tagsConsumer) {
ArrayListBuilder<String> builder = new ArrayListBuilder<>();
tagsConsumer.accept(builder);
this.tags = changedValue(builder.build());
return this;
}
// Usage
PersonDto person = PersonDtoBuilder.create()
.tags(list -> list.add("java").add("kotlin").add("scala"))
.build();When DISABLED: No builder consumer methods are generated.
Default: ENABLED | Compiler Option: -Asimplebuilder.generateConditionalHelper=ENABLED|DISABLED
Generates conditional helper methods for fluent conditional logic in builder chains.
When ENABLED:
// Generated methods
public PersonDtoBuilder conditional(BooleanSupplier condition,
Consumer<PersonDtoBuilder> trueCase,
Consumer<PersonDtoBuilder> falseCase) { ... }
public PersonDtoBuilder conditional(BooleanSupplier condition,
Consumer<PersonDtoBuilder> yesCondition) { ... }
// Usage
PersonDto person = PersonDtoBuilder.create()
.name("John")
.conditional(() -> isPremiumUser,
builder -> builder.premiumFeatures(true),
builder -> builder.premiumFeatures(false))
.build();When DISABLED: No conditional helper methods are generated.
Default: PUBLIC | Compiler Option: -Asimplebuilder.builderAccess=PUBLIC|PACKAGE_PRIVATE
Controls the visibility of the generated builder class.
Supported Values:
PUBLIC- Builder accessible from anywhere (default, recommended for public APIs)PACKAGE_PRIVATE- Builder only accessible within the same package (good for internal APIs)
PRIVATE is not allowed for builderAccess. If you try to use it, builder generation will fail with an error message explaining that Java does not allow private top-level classes. The builder will not be generated, but other DTOs in your project will continue processing normally.
Example with PACKAGE_PRIVATE:
// Generated builder
class PersonDtoBuilder implements IBuilderBase<PersonDto> { // No 'public' keyword
// ... only accessible within the same package
}Use case: Use PACKAGE_PRIVATE for DTOs that are internal to your package and shouldn't have their builders exposed publicly.
Default: PUBLIC | Compiler Option: -Asimplebuilder.builderConstructorAccess=PUBLIC|PACKAGE_PRIVATE|PRIVATE
Controls the visibility of the builder's constructors.
Supported Values:
PUBLIC- Constructors accessible from anywhere (default)PACKAGE_PRIVATE- Constructors only accessible within the same packagePRIVATE- Constructors only accessible via static factory methods ✅ Recommended pattern
Example with PRIVATE (recommended for API design):
// Generated constructors
private PersonDtoBuilder() { }
private PersonDtoBuilder(PersonDto instance) { ... }
// Usage - forced to use static factory methods
PersonDtoBuilder builder = PersonDtoBuilder.create(); // ✅ OK
new PersonDtoBuilder() // ❌ Compilation error - constructor is privateUse case: Use PRIVATE constructors to enforce using the static create() factory method, preventing direct instantiation and ensuring consistent builder creation patterns.
Default: PUBLIC | Compiler Option: -Asimplebuilder.methodAccess=PUBLIC|PACKAGE_PRIVATE
Controls the visibility of all generated setter methods.
Supported Values:
PUBLIC- Methods accessible from anywhere (default, recommended)PACKAGE_PRIVATE- Methods only accessible within the same package
PRIVATE is not allowed for methodAccess. If you try to use it, builder generation will fail with an error message explaining that all setter methods would be inaccessible. The builder will not be generated, but other DTOs in your project will continue processing normally.
Example with PACKAGE_PRIVATE:
// Generated methods without 'public' modifier
PersonDtoBuilder name(String name) { // Package-private
this.name = changedValue(name);
return this;
}Use case: Rarely needed. Consider using PACKAGE_PRIVATE only when the entire builder API should be internal to the package.
Default: ENABLED | Compiler Option: -Asimplebuilder.generateVarArgsHelpers=ENABLED|DISABLED
Generates varargs methods for List and Set fields for convenient multi-value initialization.
When ENABLED:
// Generated method
public PersonDtoBuilder tags(String... tags) {
this.tags = changedValue(Arrays.asList(tags));
return this;
}
// Usage
PersonDto person = PersonDtoBuilder.create()
.tags("java", "kotlin", "scala") // Varargs syntax
.build();When DISABLED: No varargs methods are generated; must use collection directly.
Default: ENABLED | Compiler Option: -Asimplebuilder.generateStringFormatHelpers=ENABLED|DISABLED
Generates String.format() helper methods for String and Optional fields.
When ENABLED:
// For String title field:
public BookDtoBuilder title(String format, Object... args) {
this.title = changedValue(String.format(format, args));
return this;
}
// For Optional<String> subtitle field:
public BookDtoBuilder subtitle(String format, Object... args) {
this.subtitle = changedValue(Optional.of(String.format(format, args)));
return this;
}
// Usage
BookDto book = BookDtoBuilder.create()
.title("The %s Guide", "Complete")
.subtitle("A comprehensive %s tutorial for %s", "Java", "beginners")
.build();When DISABLED: No format helper methods are generated.
Default: ENABLED | Compiler Option: -Asimplebuilder.generateUpdateHelpers=ENABLED|DISABLED
Generates xyzUpdate(UnaryOperator<T>) methods for every builder field. The update helper transforms the
current value and returns the builder for continued fluent chaining.
PersonDto person = PersonDtoBuilder.create()
.name(" bob ")
.nameUpdate(String::trim)
.quantity(10)
.quantityUpdate(Math::abs)
.build();The field must be set before its update helper is called. Otherwise, the update helper throws
IllegalStateException. Update helpers also work with With copy-and-modify operations because
values copied from an existing instance count as set.
A null result is stored as-is and validated by build() like any other value.
If a DTO field already has a setter with the same signature as a generated update helper, the setter wins and the update helper is omitted with a conflict warning.
When DISABLED: No update methods are generated.
Default: ENABLED | Compiler Option: -Asimplebuilder.generateAddToCollectionHelpers=ENABLED|DISABLED
Generates add2FieldName() helper methods for List and Set fields to add single elements.
When ENABLED:
// Generated methods
public PersonDtoBuilder add2Nicknames(String element) {
List<String> newCollection;
if (this.nicknames.isSet()) {
newCollection = new ArrayList<>(this.nicknames.value());
} else {
newCollection = new ArrayList<>();
}
newCollection.add(element);
this.nicknames = changedValue(newCollection);
return this;
}
// Usage
PersonDto person = PersonDtoBuilder.create()
.name("John")
.add2Nicknames("Johnny")
.add2Nicknames("JD")
.add2Tags("developer")
.build();When DISABLED: No add2 helper methods are generated; must use collection setters or consumer methods.
Default: ENABLED | Compiler Option: -Asimplebuilder.generateUnboxedOptional=ENABLED|DISABLED
Generates methods that accept Optional<T> and automatically unwrap them.
When ENABLED:
// Generated method
public PersonDtoBuilder name(Optional<String> nameOptional) {
nameOptional.ifPresent(value -> this.name = changedValue(value));
return this;
}
// Usage
Optional<String> maybeName = findName();
PersonDto person = PersonDtoBuilder.create()
.name(maybeName) // Automatically unwrapped
.build();When DISABLED: Must unwrap Optional manually before passing to builder.
Default: ENABLED | Compiler Option: -Asimplebuilder.copyTypeAnnotations=ENABLED|DISABLED
Copies type annotations (TYPE_USE) from the DTO fields to the builder fields and methods. This is useful for validation annotations (e.g. @NotNull, @Size) or other metadata that should be preserved.
When ENABLED:
// DTO
private List<@NotNull String> items;
// Generated Builder
private TrackedValue<List<@NotNull String>> items;
public Builder items(List<@NotNull String> items) { ... }When DISABLED: Type annotations are stripped from the builder.
Default: ENABLED | Compiler Option: -Asimplebuilder.usingArrayListBuilder=ENABLED|DISABLED
Generates methods using ArrayListBuilder for fluent List construction.
When ENABLED:
// Generated method
public PersonDtoBuilder tags(Consumer<ArrayListBuilder<String>> consumer) {
ArrayListBuilder<String> builder = new ArrayListBuilder<>();
consumer.accept(builder);
this.tags = changedValue(builder.build());
return this;
}
// Usage
.tags(list -> list.add("tag1").add("tag2").addAll(otherTags))When DISABLED: Basic List setter only; no fluent list building.
Default: ENABLED | Compiler Option: -Asimplebuilder.usingArrayListBuilderWithElementBuilders=ENABLED|DISABLED
Generates methods using ArrayListBuilderWithElementBuilders for fluent construction of Lists containing complex objects that have their own builders.
When ENABLED:
// For List<PersonDto> where PersonDto has a builder
public TeamDtoBuilder members(Consumer<ArrayListBuilderWithElementBuilders<PersonDto, PersonDtoBuilder>> consumer) {
ArrayListBuilderWithElementBuilders<PersonDto, PersonDtoBuilder> builder = ...;
consumer.accept(builder);
this.members = changedValue(builder.build());
return this;
}
// Usage - build complex nested objects inline
.members(list -> list
.add(person -> person.name("Alice").age(30))
.add(person -> person.name("Bob").age(25)))When DISABLED: No builder consumer methods for complex list elements; must construct objects separately.
Default: ENABLED | Compiler Option: -Asimplebuilder.usingHashSetBuilder=ENABLED|DISABLED
Generates methods using HashSetBuilder for fluent Set construction.
When ENABLED:
// Generated method
public PersonDtoBuilder tags(Consumer<HashSetBuilder<String>> consumer) {
HashSetBuilder<String> builder = new HashSetBuilder<>();
consumer.accept(builder);
this.tags = changedValue(builder.build());
return this;
}
// Usage
.tags(set -> set.add("tag1").add("tag2").addAll(otherTags))When DISABLED: Basic Set setter only; no fluent set building.
Default: ENABLED | Compiler Option: -Asimplebuilder.usingHashSetBuilderWithElementBuilders=ENABLED|DISABLED
Generates methods using HashSetBuilderWithElementBuilders for fluent construction of Sets containing complex objects that have their own builders.
When ENABLED:
// For Set<PersonDto> where PersonDto has a builder
public TeamDtoBuilder uniqueMembers(Consumer<HashSetBuilderWithElementBuilders<PersonDto, PersonDtoBuilder>> consumer) {
HashSetBuilderWithElementBuilders<PersonDto, PersonDtoBuilder> builder = ...;
consumer.accept(builder);
this.uniqueMembers = changedValue(builder.build());
return this;
}
// Usage - build complex nested objects inline
.uniqueMembers(set -> set
.add(person -> person.name("Alice").email("alice@example.com"))
.add(person -> person.name("Bob").email("bob@example.com")))When DISABLED: No builder consumer methods for complex set elements; must construct objects separately.
Default: ENABLED | Compiler Option: -Asimplebuilder.usingHashMapBuilder=ENABLED|DISABLED
Generates methods using HashMapBuilder for fluent Map construction.
Default: "" (empty, unchanged behavior) | Compiler Option:
-Asimplebuilder.builderGenerationPackages=package1,package2
Restricts builder generation to annotated DTOs in the listed packages. Packages are comma-separated, each listed package includes all of its subpackages, and matching ignores case. Types in the generation scope are trusted to have their builders generated in the current compilation, so references to their builders do not require a type search.
This option is an allowlist for whole packages: once set, builders are generated only inside
the listed packages and every other package is excluded implicitly. Excluding a single
package while generating everywhere else is not expressible; to exclude a single class,
use @Ignore4BuilderGeneration instead. The scope also applies to types that carry
@SimpleBuilder only through inheritance or a template annotation.
Default: "" (empty, unchanged behavior) | Compiler Option:
-Asimplebuilder.builderUsagePackages=package1,package2
Controls which packages may provide builders as nested builder helpers. Packages are comma-separated, each listed package includes all of its subpackages, and matching ignores case.
Before constructing a candidate name, the processor checks the referenced type
itself for an anchored builder — the type's own declaration wins over the
same-package candidate below. Accessible nested types satisfying the contract
are checked first (e.g. Person.PersonBuilder or Person.Builder), then a
static parameterless factory on the type — builder() preferred — whose return
type must be accessible and declare a no-arg build() method (Lombok/Immutables
style). The factory itself is the empty-instantiation path; the prefilled path is
the builder's own contract path, a static builder(T)-style method, an
instance method on the value like toBuilder(), or — with
usingFieldFunctionPrefill enabled — prefilling by field functions.
The processor constructs the candidate builder name using builderUsageSuffix
(or builderSuffix if not configured) and verifies the builder contract: a way
to create an empty builder (a no-arg constructor or a static parameterless
factory like create()), a way to create one prefilled with the value (a
constructor accepting the referenced type or a static factory like create(T)
or of(T)), and a no-arg build() method returning it — each accessible from
the generated builder's package. Generated code calls a factory instead of
new when the builder offers one. With usingFieldFunctionPrefill enabled and
no prefilled path found, the builder is tried against field-function prefilling:
the generated builder emits a private static prefill<Builder> method that
prefills each readable property having a matching field function (name(v),
setName(v), or a detected convention like withName(v)); the convention is
trusted once it covers the majority of the properties, uncovered ones stay
unset. With less coverage or the option disabled, the builder still resolves,
but helpers that would silently drop state (builder consumers) are skipped and
a debug note is logged. Any class with the expected name and a
matching contract qualifies, allowing references to builders generated with
custom template annotations, external tools, or different suffixes. If the
candidate builder cannot be found, the field falls back to a plain setter.
Packages listed in builderGenerationPackages are automatically included in the usage
scope — their builders are generated in the same compilation and don't need to be listed
here.
When builderUsagePackages is empty, builders from any package may be referenced (the
behavior before scoping existed). When set, only packages listed in builderUsagePackages
(and generation-scope types) may provide builder helpers — types outside both scopes fall
back to a plain setter.
The example module contains a runnable demo in package
org.javahelpers.simple.builders.example.scoping (ScopedOwnerDto.java).
It demonstrates a generation-scope builder consumer, a usage-scope missing-builder fallback, and
an out-of-scope plain setter in ScopedOwnerDtoBuilder.java.
Default: ENABLED | Compiler Option: -Asimplebuilder.usingExistingBuilders=DISABLED
Controls whether builders that were not generated by this processor may be
reused — both the classpath lookup described above (usage scope, in-type
anchors like Person.builder() or a nested Person.Builder) and any other
existing builder satisfying the contract. When disabled, only builders
generated in the current processing round qualify and every other referenced
type falls back to a plain setter, opting out of false-positive detections on
types that coincidentally look like builders. @SimpleMinimalBuilder disables
this option.
Default: ENABLED | Compiler Option: -Asimplebuilder.usingFieldFunctionPrefill=DISABLED
Prefills a reused builder that offers no value-accepting creation path by
calling one field function per readable property of the referenced type: the
generated builder emits a private static prefill<Builder>(T value) method
that obtains an empty instance and calls the field function matching each
property. The naming convention is detected on the builder — fluent name(v)
and JavaBeans setName(v) first, then any other camel-case prefix shared by
several methods (e.g. withName(v)); a convention is trusted once it covers
the majority of the properties, matched param-type-exact, void- or
fluent-returning. Only consulted when usingExistingBuilders is enabled; the
per-type analysis (property enumeration plus method scan) is a separate option
because it adds compile cost. @SimpleMinimalBuilder disables this option.
Default: "" (empty) | Compiler Option: -Asimplebuilder.deactivateGenerationComponents=pattern1,pattern2,...
Deactivates specific method generators and builder enhancers by class name pattern. This allows you to override default generators/enhancers with your own custom implementations.
Primary Use Case: Override Default Components
When you want to replace a built-in generator or enhancer with your own custom implementation, you first deactivate the default component, then register your custom one via ServiceLoader.
For detailed instructions on creating custom generators and enhancers, see CUSTOMIZING.md.
Pattern Matching:
- Exact match:
ConditionalEnhancer- deactivates exactly this class - Wildcard suffix:
*HelperGenerator- deactivates all classes ending with HelperGenerator - Wildcard prefix:
String*- deactivates all classes starting with String - Wildcard anywhere:
*Consumer*- deactivates all classes containing Consumer
Feature Toggling vs Component Override:
- Feature toggling: Use
@SimpleBuilder.Options(generateConditionalHelper = DISABLED) - Component override: Use
deactivateGenerationComponents+ custom ServiceLoader implementation
See CUSTOMIZING.md for complete implementation examples and best practices.
Default: ENABLED | Compiler Option: -Asimplebuilder.generateWithInterface=ENABLED|DISABLED
Generates a With interface that can be implemented by your DTO to enable fluent modification methods.
When ENABLED:
// Generated interface inside the builder
public interface With {
default PersonDto with(Consumer<PersonDtoBuilder> modifications) {
PersonDtoBuilder builder = new PersonDtoBuilder((PersonDto) this);
modifications.accept(builder);
return builder.build();
}
default PersonDtoBuilder with() {
return new PersonDtoBuilder((PersonDto) this);
}
}
// Your DTO can implement it
public class PersonDto implements PersonDtoBuilder.With {
// ...
}
// Usage - create modified copies
PersonDto original = new PersonDto();
PersonDto modified = original.with(p -> p.name("New Name").age(30));When DISABLED: No With interface is generated. DTOs cannot use the fluent modification pattern.
Default: ENABLED | Compiler Option: -Asimplebuilder.implementsBuilderBase=ENABLED|DISABLED
Makes the generated builder implement IBuilderBase<T> interface for framework integration.
When ENABLED:
public class PersonDtoBuilder implements IBuilderBase<PersonDto> {
// Can be used with generic builder frameworks
}When DISABLED: Builder is a standalone class without interface.
Default: ENABLED | Compiler Option: -Asimplebuilder.usingGeneratedAnnotation=ENABLED|DISABLED
Adds @Generated annotation to the builder class for tooling and code coverage exclusion.
When ENABLED:
@Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor")
public class PersonDtoBuilder implements IBuilderBase<PersonDto> {
// ...
}When DISABLED: No @Generated annotation. Useful if you want the builder counted in code coverage.
Default: ENABLED | Compiler Option: -Asimplebuilder.usingBuilderImplementationAnnotation=ENABLED|DISABLED
Adds @BuilderImplementation annotation linking the builder to its target class.
When ENABLED:
@BuilderImplementation(forClass = PersonDto.class)
public class PersonDtoBuilder implements IBuilderBase<PersonDto> {
// ...
}When DISABLED: No @BuilderImplementation annotation.
Default: DISABLED | Compiler Option: -Asimplebuilder.usingJacksonDeserializerAnnotation=ENABLED|DISABLED
Adds @JsonPOJOBuilder annotation to the builder class for Jackson deserialization support.
When ENABLED:
@JsonPOJOBuilder(withPrefix = "")
public class PersonDtoBuilder {
// ...
}When DISABLED: No @JsonPOJOBuilder annotation.
Note: This requires com.fasterxml.jackson.core:jackson-databind on the classpath during compilation. If missing, the annotation is skipped with a warning.
Default: DISABLED | Compiler Option: -Asimplebuilder.generateJacksonModule=ENABLED|DISABLED
Generates a Jackson SimpleModule (named SimpleBuildersJacksonModule) that registers all generated builders via MixIns. This allows deserialization without annotating your DTOs with @JsonDeserialize.
Requirement: You MUST also enable usingJacksonDeserializerAnnotation. If generateJacksonModule is enabled but usingJacksonDeserializerAnnotation is disabled, the processor will issue a warning and skip module generation.
When ENABLED:
- The builder is generated as usual.
- A
SimpleBuildersJacksonModuleclass is generated. - The module registers a MixIn for the DTO that points to the Builder.
Package Name:
By default, a SimpleBuildersJacksonModule is generated in each package that contains DTOs configured for Jackson module generation. This ensures deterministic behavior.
To specify a single fixed package name for all generated modules (grouping them into one), use the jacksonModulePackage option.
Generated Module Example:
package com.example.project.dto; // Generated in the same package as DTOs (by default)
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
public class SimpleBuildersJacksonModule extends SimpleModule {
public SimpleBuildersJacksonModule() {
setMixInAnnotation(PersonDto.class, PersonDtoMixin.class);
}
@JsonDeserialize(builder = PersonDtoBuilder.class)
private interface PersonDtoMixin {}
}Usage:
ObjectMapper mapper = new ObjectMapper();
// Register the module for your package
// Note: If you have DTOs in multiple packages and use the default strategy,
// you need to register the generated module for each package.
mapper.registerModule(new com.example.project.dto.SimpleBuildersJacksonModule());
PersonDto dto = mapper.readValue(json, PersonDto.class);Tip: Use the jacksonModulePackage option to generate a single module for your entire project, making registration easier:
mapper.registerModule(new com.example.project.config.SimpleBuildersJacksonModule());
Note: This requires com.fasterxml.jackson.core:jackson-databind on the classpath.
Default: null (uses package of each processed DTO) | Compiler Option: -Asimplebuilder.jacksonModulePackage=com.your.package
Specifies the package name where the SimpleBuildersJacksonModule class will be generated.
This is highly recommended to ensure deterministic output location and avoid split-package issues.
Note: If not specified, a separate SimpleBuildersJacksonModule will be generated in each package containing processed DTOs.
Example:
-Asimplebuilder.jacksonModulePackage=com.example.project.config
Default: ENABLED | Compiler Option: -Asimplebuilder.usingMapStructIntegration=ENABLED|DISABLED
Controls the MapStruct SPI adapters bundled in the processor jar (MapStructBuilderProvider and MapStructAccessorNamingStrategy, registered via META-INF/services). They make MapStruct auto-detect generated builders when simple-builders-processor and mapstruct-processor share the annotation processor path — no annotation attribute exists because the SPIs are discovered globally by MapStruct itself.
The SPIs read this option from the options map MapStruct hands them, with the same -D > -A > bare-option precedence as every option. MapStruct only forwards declared options into SPI environments, so the processor jar also ships an AdditionalSupportedOptionsProvider declaring both spellings — -Asimplebuilder.usingMapStructIntegration and the bare -AusingMapStructIntegration reach the SPIs, and -D works too since system properties are JVM-global (the only mechanism when only the SPI jar is on the processor path).
When DISABLED: The provider returns no builder candidates and the naming strategy keeps the stock MapStruct behaviour, so generated builders are treated like ordinary classes.
Scope note: BuilderProcessor generates in exactly one round — the first round carrying its annotations. Beans whose types first appear in a later round (emitted by other processors after that round) get no builder, and only beans the processor plans in that round are paired with their builder through the SPIs; builders produced by earlier compilations are not discovered.
Example:
<compilerArgs>
<arg>-Asimplebuilder.usingMapStructIntegration=DISABLED</arg>
</compilerArgs>Default: ENABLED | Compiler Option: -Asimplebuilder.generateJavaDoc=ENABLED|DISABLED
Controls whether the processor emits Javadoc comments on the generated builder class, its fields, constructors and methods.
When ENABLED (default):
The generated builder contains Javadoc blocks explaining the purpose of the class, setters, build(), create() and any helper methods.
When DISABLED: No Javadoc is emitted. This produces smaller generated source files and is useful when generated code is committed to version control and Javadoc noise is undesirable.
Example:
@SimpleBuilder(options = @SimpleBuilder.Options(generateJavaDoc = OptionState.DISABLED))
public class PersonDto {
private String name;
}Default: "Builder" | Compiler Option: -Asimplebuilder.builderSuffix=CustomSuffix
Customizes the suffix appended to the DTO class name to create the builder class name.
Example:
@SimpleBuilder.Options(builderSuffix = "Factory")
public class PersonDto { }
// Generated class name: PersonDtoFactory (instead of PersonDtoBuilder)Default: "" (empty — falls back to builderSuffix) | Compiler Option:
-Asimplebuilder.builderUsageSuffix=CustomSuffix
Customizes the suffix used when looking up builders from the usage scope. The processor
constructs the candidate builder name as referencedType.getSimpleName() + builderUsageSuffix.
If empty, builderSuffix is used instead. This allows referencing builders that were generated
with a different suffix (e.g. by another module using "Factory" as suffix) without changing
the suffix used for own builder generation.
The candidate class must provide a way to create an empty builder (a no-arg
constructor or a static parameterless factory like create()), a way to create
one prefilled with the value (a constructor accepting the referenced type or a
static factory like create(T) or of(T)), and a no-arg build() method
returning it — each accessible from the generated builder's package. Generated
code calls a factory instead of new when the builder offers one. The contract
check is annotation-agnostic, so builders generated with custom template
annotations or external tools are supported. If the candidate class does not
exist or does not satisfy this contract, the field falls back to a plain setter.
Example:
@SimpleBuilder.Options(
builderSuffix = "Builder", // own builders: *Builder
builderUsageSuffix = "Factory" // usage-scope builders: *Factory
)
public class OwnerDto {
private ExternalDto external;
}
// Own builder: OwnerDtoBuilder
// External builder looked up as: ExternalDtoFactory (not ExternalDtoBuilder)Default: "" (empty — the package of the annotated type, resp. of the element carrying
@SimpleBuilderFor) | Annotation only — a global -Asimplebuilder.packageName is
deliberately not supported: it would collapse all generated builders into a single package.
Places generated builder classes into the given package instead of the default package.
Useful to collect builders in a central package, or to keep a third-party target's builder
in your own namespace when using @SimpleBuilderFor.
Only members of the target type that are accessible from the configured package are used for builder generation — package-private and protected constructors, setters and getters are then not reachable and are treated as if absent. The value must be a valid Java package name; an invalid value produces a compile-time diagnostic.
Example:
@SimpleBuilderFor(
value = ExternalUser.class,
options = @SimpleBuilder.Options(packageName = "com.example.generated"))
public class ExternalBuilders { }
// Generated class: com.example.generated.ExternalUserBuilderDefault: "" (empty) | Compiler Option: -Asimplebuilder.setterPrefix=customPrefix
Adds a prefix to all setter method names.
Example:
@SimpleBuilder.Options(setterPrefix = "with")
public class PersonDto {
private String name;
}
// Generated method: withName(String name) instead of name(String name)Default: DISABLED | Compiler Option: -Asimplebuilder.strict=ENABLED|DISABLED
When enabled, builder and Jackson-module generation failures are promoted from compiler warnings to errors that fail the build. By default, strict mode is disabled and generation failures are reported as warnings so compilation can continue.
Default: false | Compiler Option: -Asimplebuilder.performanceTracking=true|false
Enables performance tracking during annotation processing. When enabled, the processor measures execution times for each processing phase, method generator, builder enhancer, and per-class processing. A summary report is logged to the compiler output at the end of processing.
When enabled: A hierarchical performance report is printed to the compiler log, including:
- Total processing time and average time per class
- Phase breakdown (Element Collection, Configuration Resolution, Builder Definition Extraction, DTO Mapping, Code Generation)
- Top 20 slowest classes with field and collection counts
- Top 5 slowest MethodGenerators and BuilderEnhancers
When disabled (default): No performance tracking occurs. The NoOpPerformanceTracker is used,
which has zero overhead as the JIT compiler eliminates all tracking calls.
Example:
# Maven
mvn compile -Dsimplebuilder.performanceTracking=true
# Or via compiler arg
-Asimplebuilder.performanceTracking=trueDefault: (empty, no file output) | Compiler Option: -Asimplebuilder.performanceOutputFile=path/to/report.json
Specifies a file path where the performance report is written as structured JSON. This is useful for automated performance analysis and comparison across multiple runs.
When set: In addition to the log output, a JSON file is written containing:
timestamp— ISO-8601 timestamp of the reporttotalClasses— number of classes processedtotalProcessingTimeNanos/totalProcessingTimeSeconds— total processing timeaveragePerClassMs— average processing time per classphaseBreakdown— hierarchical phase timings with elapsed nanos, seconds, and percentagesclassMetrics— per-class metrics (name, elapsed nanos/ms, field count, collection count), sorted by elapsed time descendinggeneratorStats— per-generator stats (name, elapsed nanos, call count, avg ms/call)enhancerStats— per-enhancer stats (name, elapsed nanos, call count, avg ms/call)
When not set (default): Only the log report is generated; no JSON file is written.
Example:
# Maven
mvn compile \
-Dsimplebuilder.performanceTracking=true \
-Dsimplebuilder.performanceOutputFile=target/performance-report.json
# Or via compiler arg
-Asimplebuilder.performanceTracking=true
-Asimplebuilder.performanceOutputFile=target/performance-report.jsonNote: performanceTracking must be enabled for performanceOutputFile to have any effect.
Default: false | Compiler Option: -Asimplebuilder.verbose=true|false
Note: This is a processor-level option only. It cannot be set per-annotation via
@SimpleBuilder.Options.
Enables debug logging during annotation processing, providing detailed tracing of field discovery, method analysis, and code generation steps. See DEBUG_LOGGING.md for detailed information on log levels, output format, and configuration examples.
Default: jdt | Compiler Option: -Asimplebuilder.formattingMode=JDT|LIGHTWEIGHT|NONE
| Annotation Option: @SimpleBuilder.Options(formattingMode = "lightweight")
Controls how generated source code is post-processed before being written to disk:
| Mode | Description | Trade-offs |
|---|---|---|
JDT (default) |
Full Eclipse JDT formatter | Highest quality; slowest (~40% of generation time) |
LIGHTWEIGHT |
Minimal cosmetic fixes (tabs→spaces, blank line collapse, javadoc prefixes) | Fast; no line wrapping or import ordering |
NONE |
Raw Roaster output, no post-processing | Fastest; no indentation, not suitable for committed code |
Example:
# Maven
mvn compile -Dsimplebuilder.formattingMode=lightweight
# Or via compiler arg
-Asimplebuilder.formattingMode=lightweightPer-annotation override:
@SimpleBuilder(options = @SimpleBuilder.Options(
formattingMode = "lightweight"
))
public class PersonDto { ... }Default: eclipse-java-format.xml (bundled) | Compiler Option: -Asimplebuilder.formatterProfile=path/to/profile.xml
Note: This is a processor-level option only. It cannot be set per-annotation via
@SimpleBuilder.Options.
Specifies an external Eclipse formatter profile for JDT formatting. This option only affects JDT formatting; it is ignored for LIGHTWEIGHT and NONE. A file system path is tried first, then a classpath resource. If neither can be loaded, a warning is logged and the bundled eclipse-java-format.xml profile is used.
Example:
# Maven
mvn compile -Dsimplebuilder.formatterProfile=config/eclipse-formatter.xml
# Or via compiler arg
-Asimplebuilder.formatterProfile=config/eclipse-formatter.xmlUse the built-in @SimpleMinimalBuilder template to generate only essential builder methods with a single annotation:
import org.javahelpers.simple.builders.core.annotations.SimpleMinimalBuilder;
@SimpleMinimalBuilder
public class MinimalDto {
private String name;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}Generated: Only basic builder methods (create(), field setters, build()).
For a concrete generated example, see CustomerDtoBuilder.java produced from CustomerDto.java.
If you need a different name or extra customization, you can still build a custom @SimpleBuilder.Template with all optional features disabled.
Create builders for internal use only:
@SimpleBuilder
@SimpleBuilder.Options(
builderAccess = AccessModifier.PACKAGE_PRIVATE,
methodAccess = AccessModifier.PACKAGE_PRIVATE
)
public class InternalConfig {
private String secretKey;
public String getSecretKey() { return secretKey; }
public void setSecretKey(String secretKey) { this.secretKey = secretKey; }
}Generated: Package-private builder and methods, only accessible within the same package
Optimize for collection manipulation:
@SimpleBuilder
@SimpleBuilder.Options(
generateVarArgsHelpers = OptionState.ENABLED,
usingArrayListBuilder = OptionState.ENABLED,
usingArrayListBuilderWithElementBuilders = OptionState.ENABLED,
usingHashSetBuilder = OptionState.ENABLED,
usingHashMapBuilder = OptionState.ENABLED
)
public class TeamDto {
private List<String> memberNames;
private Set<PersonDto> members;
private Map<String, PersonDto> memberMap;
}Generated: Chained collection builders for fluent collection manipulation
Example usage:
TeamDto team = TeamDtoBuilder.create()
.memberNames(list -> list.add("Alice").add("Bob"))
.members(set -> set
.add(person -> person.name("Alice").age(30))
.add(person -> person.name("Bob").age(25)))
.memberMap(map -> map.put("Alice", alice).put("Bob", bob))
.build();The built-in @SimpleMinimalBuilder is the simplest way to get a lightweight builder. If you need a differently named template or want to build your own preset, create a custom annotation meta-annotated with @SimpleBuilder.Template:
@SimpleBuilder.Template(options = @SimpleBuilder.Options(
generateFieldSupplier = OptionState.DISABLED,
generateFieldConsumer = OptionState.DISABLED,
generateBuilderConsumer = OptionState.DISABLED,
generateConditionalHelper = OptionState.DISABLED,
generateVarArgsHelpers = OptionState.DISABLED,
generateStringFormatHelpers = OptionState.DISABLED,
generateUpdateHelpers = OptionState.DISABLED,
generateAddToCollectionHelpers = OptionState.DISABLED,
generateUnboxedOptional = OptionState.DISABLED,
copyTypeAnnotations = OptionState.DISABLED,
usingArrayListBuilder = OptionState.DISABLED,
usingArrayListBuilderWithElementBuilders = OptionState.DISABLED,
usingHashSetBuilder = OptionState.DISABLED,
usingHashSetBuilderWithElementBuilders = OptionState.DISABLED,
usingHashMapBuilder = OptionState.DISABLED,
generateWithInterface = OptionState.DISABLED,
usingGeneratedAnnotation = OptionState.DISABLED,
usingBuilderImplementationAnnotation = OptionState.DISABLED,
implementsBuilderBase = OptionState.DISABLED,
usingJacksonDeserializerAnnotation = OptionState.DISABLED,
generateJacksonModule = OptionState.DISABLED,
generateJavaDoc = OptionState.DISABLED
))
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface MinimalBuilder {}Use everywhere:
@MinimalBuilder
public class CustomerDto {
private String name;
private List<OrderDto> orders;
}Set sensible defaults for your entire project:
<!-- Maven compiler plugin configuration -->
<compilerArgs>
<!-- Enable all field setter variants -->
<arg>-Asimplebuilder.generateFieldSupplier=ENABLED</arg>
<arg>-Asimplebuilder.generateFieldConsumer=ENABLED</arg>
<arg>-Asimplebuilder.generateBuilderConsumer=ENABLED</arg>
<!-- Make builders package-private for internal APIs -->
<arg>-Asimplebuilder.builderAccess=PACKAGE_PRIVATE</arg>
<!-- Enable collection helpers -->
<arg>-Asimplebuilder.usingArrayListBuilder=ENABLED</arg>
<arg>-Asimplebuilder.usingHashMapBuilder=ENABLED</arg>
</compilerArgs>Override per-class when needed:
@SimpleBuilder
@SimpleBuilder.Options(builderAccess = AccessModifier.PUBLIC) // Override: make this one public
public class PublicApiDto {
private String data;
}
@SimpleBuilder // Uses project defaults: package-private
public class InternalDto {
private String data;
}Configuration resolution follows these priority rules:
- Annotation values (highest priority)
- Values in
@SimpleBuilder.Options(...)always win
- Values in
- Compiler options (medium priority)
- Used when no annotation value is specified
- Default values (lowest priority)
- Used when neither annotation nor compiler option is specified
// Compiler option: -Asimple.builders.generateFieldSupplier=false
// Global default: true
@SimpleBuilder
@SimpleBuilder.Options(generateFieldSupplier = OptionState.ENABLED) // Annotation wins!
public class Person {
private String name;
}
@SimpleBuilder // Uses compiler option (false)
public class Company {
private String name;
}
// No compiler option set
@SimpleBuilder // Uses global default (true)
public class Product {
private String name;
}The AccessModifier enum provides type-safe access control:
public enum AccessModifier {
PUBLIC, // Accessible from anywhere
PROTECTED, // Accessible within same package and subclasses
PACKAGE_PRIVATE, // Accessible only within same package (default Java visibility)
PRIVATE // Accessible only within same class
}Use in annotations:
@SimpleBuilder.Options(
builderAccess = AccessModifier.PACKAGE_PRIVATE,
methodAccess = AccessModifier.PUBLIC
)Or in compiler options:
-Asimplebuilder.builderAccess=PACKAGE_PRIVATE
- Check option names: Ensure you're using the full option name (e.g.,
-Asimplebuilder.generateFieldSupplier); the same name also works as a JVM system property (-Dsimplebuilder.<option>), which takes precedence over-A - Verify processor is running: Ensure annotation processor is configured correctly
- Check IDE configuration: Some IDEs need special configuration for compiler options
- Clean and rebuild: Run
mvn clean compileto ensure fresh build
- Verify annotation import: Import
org.javahelpers.simple.builders.core.annotations.SimpleBuilder - Check annotation placement: Use
@SimpleBuilderon the class,@SimpleBuilder.Optionson the same class - Verify compilation: Recompile after changing annotations
- Check for syntax errors: Ensure AccessModifier enum values are correct
- Package-private builders: Ensure DTO and builder are in the same package
- Private builders: May cause issues with reflection-based frameworks
- Protected builders: Only accessible to subclasses
- AccessModifier import: Import
org.javahelpers.simple.builders.core.enums.AccessModifier
- Check @SimpleBuilder.Template: Ensure template annotation has
@SimpleBuilder.Template - Verify options parameter: Template can specify
options = @SimpleBuilder.Options(...)or rely on the built-in defaults (omitting it is equivalent to an empty@Options()) - Retention and Target: Add
@Retention(RetentionPolicy.CLASS)and@Target(ElementType.TYPE) - Combining with @SimpleBuilder:
@SimpleBuilderis itself a built-in template. If both@SimpleBuilderand a custom template are present on the same class,@SimpleBuildertakes precedence in that scope. To use a custom template, place only the custom annotation on the class. - Subclasses not getting a builder: Add
@Inheritedto the custom template annotation so it propagates to unannotated subclasses (see Template Annotations above). Without@Inherited, only the exact type carrying the annotation gets a builder. - Subclass builder has wrong options: Ensure the custom template annotation is
@Inheritedand that the parent annotation declares the desired@SimpleBuilder.Options. Inherited options are applied to subclass builders; a subclass's own annotation overrides the inherited options.
If you see warnings like "Failed to generate builder" with access modifier messages:
Problem: Used PRIVATE for builderAccess or methodAccess
@SimpleBuilder.Options(builderAccess = AccessModifier.PRIVATE) // ❌ ERRORSolution: Use PUBLIC or PACKAGE_PRIVATE instead
@SimpleBuilder.Options(builderAccess = AccessModifier.PACKAGE_PRIVATE) // ✅ OKNote: Only builderConstructorAccess = PRIVATE is valid - this enforces using the create() factory method.
Error Messages:
builderAccess=PRIVATE→ "Java does not allow private top-level classes"methodAccess=PRIVATE→ "Makes all setter methods inaccessible"
- Use templates for common patterns: Define reusable templates for your project
- Set project-wide defaults: Configure sensible defaults via compiler options
- Override sparingly: Only override when truly necessary
- Document templates: Add JavaDoc to custom template annotations
- Use type-safe enums: Prefer
AccessModifierenum over string values - Test configurations: Verify generated code meets expectations
- Consider team preferences: Choose configurations that work for everyone
Recommended Combinations:
✅ Public API Builder (most common):
@SimpleBuilder.Options(
builderAccess = AccessModifier.PUBLIC, // ✅ Accessible everywhere
builderConstructorAccess = AccessModifier.PRIVATE, // ✅ Forces use of create()
methodAccess = AccessModifier.PUBLIC // ✅ Accessible everywhere
)✅ Internal/Package-Private Builder:
@SimpleBuilder.Options(
builderAccess = AccessModifier.PACKAGE_PRIVATE, // ✅ Internal to package
builderConstructorAccess = AccessModifier.PRIVATE, // ✅ Forces use of create()
methodAccess = AccessModifier.PACKAGE_PRIVATE // ✅ Internal to package
)❌ Invalid Combinations (Will Cause Builder Generation to Fail):
// ❌ ERROR: Private builder class causes generation failure
builderAccess = AccessModifier.PRIVATE
// Error: "Java does not allow private top-level classes"
// Result: Builder NOT generated, other DTOs continue processing
// ❌ ERROR: Private methods cause generation failure
methodAccess = AccessModifier.PRIVATE
// Error: "Makes all setter methods inaccessible"
// Result: Builder NOT generated, other DTOs continue processing
// ❌ ERROR: Both invalid configurations
builderAccess = AccessModifier.PRIVATE,
methodAccess = AccessModifier.PRIVATE
// Result: Builder NOT generated, other DTOs continue processingWhy PRIVATE constructors are different:
- ✅
builderConstructorAccess = PRIVATEIS ALLOWED - Enforces usingcreate()factory method (recommended pattern) - ❌
builderAccess = PRIVATECAUSES ERROR - Java doesn't allow private top-level classes - ❌
methodAccess = PRIVATECAUSES ERROR - Makes all methods inaccessible and builder unusable
What happens when validation fails:
- Builder generation for that DTO is skipped
- A clear warning message is logged explaining the problem
- Compilation continues and succeeds
- Other DTOs in your project still get their builders generated
- No invalid Java code is produced
# Field Setter Generation
-Asimplebuilder.generateFieldSupplier=ENABLED|DISABLED
-Asimplebuilder.generateFieldConsumer=ENABLED|DISABLED
-Asimplebuilder.generateBuilderConsumer=ENABLED|DISABLED
# Conditional Logic
-Asimplebuilder.generateConditionalHelper=ENABLED|DISABLED
# Access Control
-Asimplebuilder.builderAccess=PUBLIC|PACKAGE_PRIVATE # PRIVATE not recommended (unusable builder)
-Asimplebuilder.builderConstructorAccess=PUBLIC|PACKAGE_PRIVATE|PRIVATE # PRIVATE recommended for factory pattern
-Asimplebuilder.methodAccess=PUBLIC|PACKAGE_PRIVATE # PRIVATE not recommended (unusable methods)
# Helper Methods
-Asimplebuilder.generateVarArgsHelpers=ENABLED|DISABLED
-Asimplebuilder.generateStringFormatHelpers=ENABLED|DISABLED
-Asimplebuilder.generateUpdateHelpers=ENABLED|DISABLED
-Asimplebuilder.generateAddToCollectionHelpers=ENABLED|DISABLED
-Asimplebuilder.generateUnboxedOptional=ENABLED|DISABLED
-Asimplebuilder.copyTypeAnnotations=ENABLED|DISABLED
# Collection Helpers
-Asimplebuilder.usingArrayListBuilder=ENABLED|DISABLED
-Asimplebuilder.usingArrayListBuilderWithElementBuilders=ENABLED|DISABLED
-Asimplebuilder.usingHashSetBuilder=ENABLED|DISABLED
-Asimplebuilder.usingHashSetBuilderWithElementBuilders=ENABLED|DISABLED
-Asimplebuilder.usingHashMapBuilder=ENABLED|DISABLED
# Component Filtering
-Asimplebuilder.deactivateGenerationComponents=pattern1,pattern2,...
# Builder Scoping
-Asimplebuilder.builderGenerationPackages=package1,package2
-Asimplebuilder.builderUsagePackages=package1,package2
# Integration & Annotations
-Asimplebuilder.generateWithInterface=ENABLED|DISABLED
-Asimplebuilder.implementsBuilderBase=ENABLED|DISABLED
-Asimplebuilder.usingGeneratedAnnotation=ENABLED|DISABLED
-Asimplebuilder.usingBuilderImplementationAnnotation=ENABLED|DISABLED
-Asimplebuilder.usingJacksonDeserializerAnnotation=ENABLED|DISABLED
-Asimplebuilder.generateJacksonModule=ENABLED|DISABLED
-Asimplebuilder.jacksonModulePackage=com.your.package
-Asimplebuilder.usingMapStructIntegration=ENABLED|DISABLED
# Documentation
-Asimplebuilder.generateJavaDoc=ENABLED|DISABLED
# Naming
-Asimplebuilder.builderSuffix=CustomSuffix
-Asimplebuilder.setterPrefix=customPrefix
# Reliability
-Asimplebuilder.strict=ENABLED|DISABLED
# Debug Logging
-Asimplebuilder.verbose=true|false
# Performance Tracking
-Asimplebuilder.performanceTracking=true|false
-Asimplebuilder.performanceOutputFile=path/to/report.json
# Performance Optimization
-Asimplebuilder.formattingMode=JDT|LIGHTWEIGHT|NONE
-Asimplebuilder.formatterProfile=path/to/profile.xml
@SimpleBuilder
@SimpleBuilder.Options(
// Field Setter Generation
generateFieldSupplier = OptionState.ENABLED,
generateFieldConsumer = OptionState.ENABLED,
generateBuilderConsumer = OptionState.ENABLED,
// Conditional Logic
generateConditionalHelper = OptionState.ENABLED,
// Access Control
builderAccess = AccessModifier.PUBLIC,
builderConstructorAccess = AccessModifier.PUBLIC,
methodAccess = AccessModifier.PUBLIC,
// Helper Methods
generateVarArgsHelpers = OptionState.ENABLED,
generateStringFormatHelpers = OptionState.ENABLED,
generateUpdateHelpers = OptionState.ENABLED,
generateUnboxedOptional = OptionState.ENABLED,
// Collection Helpers
usingArrayListBuilder = OptionState.ENABLED,
usingArrayListBuilderWithElementBuilders = OptionState.ENABLED,
usingHashSetBuilder = OptionState.ENABLED,
usingHashSetBuilderWithElementBuilders = OptionState.ENABLED,
usingHashMapBuilder = OptionState.ENABLED,
// Builder Scoping
builderGenerationPackages = "com.example.dto",
builderUsagePackages = "com.example.library",
// Integration & Annotations
generateWithInterface = OptionState.ENABLED,
implementsBuilderBase = OptionState.ENABLED,
usingGeneratedAnnotation = OptionState.ENABLED,
usingBuilderImplementationAnnotation = OptionState.ENABLED,
usingJacksonDeserializerAnnotation = OptionState.ENABLED,
// Documentation
generateJavaDoc = OptionState.ENABLED,
// Naming
builderSuffix = "Builder",
setterPrefix = ""
)
public class ExampleDto {
private String name;
}Related: README.md