Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Simple Builders is a Java [annotation processor](https://docs.oracle.com/en/java
- **Collections Support**: Built-in support for collections and maps
- **Annotation Preservation**: Validation annotations are automatically copied to builder methods
- **With Interface Pattern**: Type-safe object modifications using generated With interfaces
- **Jackson Support**: Supporting Jackson deserialization via `@JsonPOJOBuilder` and optional generation of `SimpleModule`s (one per package) (both need to be enabled)

## Requirements

Expand Down Expand Up @@ -268,7 +269,7 @@ The `With` interface provides type-safe setter methods that mirror the builder's

Simple Builders provides extensive configuration options to customize the generated builder code. You can control:

- Field setter generation (Supplier, Provider, Builder patterns)
- Field setter generation (Supplier, Consumer, Builder patterns)
- Conditional logic helpers
- Access modifiers for builders and methods
- Collection helper methods
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,55 @@
*/
OptionState generateWithInterface() default OptionState.UNSET;

/**
* Add Jackson annotations to the generated builder class. <br>
* Adds {@code @JsonPOJOBuilder(withPrefix = "...")} to the builder class. The prefix matches
* the configured {@link #setterSuffix()}.
*
* <p>Example:
*
* <pre>{@code
* @JsonDeserialize(builder = PersonDtoBuilder.class)
* public class PersonDto { ... }
*
* // Generated:
* @JsonPOJOBuilder(withPrefix = "")
* public class PersonDtoBuilder { ... }
* }</pre>
*
* Default: DISABLED <br>
* Compiler option: -Asimplebuilder.usingJacksonDeserializerAnnotation
*/
OptionState usingJacksonDeserializerAnnotation() default OptionState.UNSET;

/**
* Generate a Jackson SimpleModule containing registrations for all generated builders. <br>
* This module allows Jackson to use the generated builders for deserialization without needing
* to annotate the DTO classes.
*
* <p>The generated module class will be named {@code SimpleBuildersJacksonModule} (by default).
* By default, a module is generated in <b>each package</b> containing processed DTOs. To group
* all registrations into a single module, use {@link #jacksonModulePackage()}.
*
* <p>Default: DISABLED <br>
* Compiler option: -Asimplebuilder.generateJacksonModule
*/
OptionState generateJacksonModule() default OptionState.UNSET;

/**
* Specifies the package name where the {@code SimpleBuildersJacksonModule} class will be
* generated. <br>
* This is useful to avoid split-package issues or to group all module registrations into a
* single module.
*
* <p>If not specified, a separate module will be generated in <b>each package</b> containing
* processed DTOs.
*
* <p>Default: "" (empty - generate one module per package) <br>
* Compiler option: -Asimplebuilder.jacksonModulePackage
*/
String jacksonModulePackage() default "";

// === Naming ===
/**
* Suffix to append to the DTO name to generate the builder class name. <br>
Expand Down
191 changes: 153 additions & 38 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,17 +81,25 @@ Create reusable configuration presets with custom template annotations:

```java
@SimpleBuilder.Template(options = @SimpleBuilder.Options(
generateFieldSupplier = false,
generateFieldProvider = false,
generateBuilderProvider = false,
generateConditionalHelper = false,
generateVarArgsHelpers = false,
usingArrayListBuilder = false,
usingArrayListBuilderWithElementBuilders = false,
usingHashSetBuilder = false,
usingHashSetBuilderWithElementBuilders = false,
usingHashMapBuilder = false,
generateWithInterface = false
generateFieldSupplier = OptionState.DISABLED,
generateFieldConsumer = OptionState.DISABLED,
generateBuilderConsumer = OptionState.DISABLED,
generateConditionalHelper = OptionState.DISABLED,
generateVarArgsHelpers = OptionState.DISABLED,
generateStringFormatHelpers = 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
))
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
Expand Down Expand Up @@ -681,6 +689,93 @@ public class PersonDtoBuilder implements IBuilderBase<PersonDto> {

---

#### `usingJacksonDeserializerAnnotation`

**Default**: `DISABLED` | **Compiler Option**: `-Asimplebuilder.usingJacksonDeserializerAnnotation=ENABLED|DISABLED`

Adds `@JsonPOJOBuilder` annotation to the builder class for Jackson deserialization support.

**When ENABLED**:
```java
@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.

---

#### `generateJacksonModule`

**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`](#usingjacksondeserializerannotation). If `generateJacksonModule` is enabled but `usingJacksonDeserializerAnnotation` is disabled, the processor will issue a warning and skip module generation.

**When ENABLED**:
1. The builder is generated as usual.
2. A `SimpleBuildersJacksonModule` class is generated.
3. 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`](#jacksonmodulepackage) option.

**Generated Module Example**:
```java
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**:
```java
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`](#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.

---

#### `jacksonModulePackage`

**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`

---

### Naming

#### `builderSuffix`
Expand Down Expand Up @@ -722,16 +817,27 @@ public class PersonDto {
Generate only essential builder methods:

```java
@SimpleBuilder
@SimpleBuilder.Options(
generateFieldSupplier = OptionState.DISABLED,
generateFieldConsumer = OptionState.DISABLED,
generateBuilderConsumer = OptionState.DISABLED,
generateConditionalHelper = OptionState.DISABLED,
generateVarArgsHelpers = OptionState.DISABLED,
usingArrayListBuilder = OptionState.DISABLED,
usingHashMapBuilder = OptionState.DISABLED,
generateWithInterface = OptionState.DISABLED
@SimpleBuilder(
options = @SimpleBuilder.Options(
generateFieldSupplier = OptionState.DISABLED,
generateFieldConsumer = OptionState.DISABLED,
generateBuilderConsumer = OptionState.DISABLED,
generateConditionalHelper = OptionState.DISABLED,
generateVarArgsHelpers = OptionState.DISABLED,
generateStringFormatHelpers = 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
)
public class MinimalDto {
private String name;
Expand Down Expand Up @@ -770,11 +876,11 @@ Optimize for collection manipulation:
```java
@SimpleBuilder
@SimpleBuilder.Options(
generateVarArgsHelpers = true,
usingArrayListBuilder = true,
usingArrayListBuilderWithElementBuilders = true,
usingHashSetBuilder = true,
usingHashMapBuilder = true
generateVarArgsHelpers = OptionState.ENABLED,
usingArrayListBuilder = OptionState.ENABLED,
usingArrayListBuilderWithElementBuilders = OptionState.ENABLED,
usingHashSetBuilder = OptionState.ENABLED,
usingHashMapBuilder = OptionState.ENABLED
)
public class TeamDto {
private List<String> memberNames;
Expand Down Expand Up @@ -802,17 +908,25 @@ Create a reusable template for lightweight builders:

```java
@SimpleBuilder.Template(options = @SimpleBuilder.Options(
generateFieldSupplier = false,
generateFieldProvider = false,
generateBuilderProvider = false,
generateConditionalHelper = false,
generateVarArgsHelpers = false,
usingArrayListBuilder = false,
usingArrayListBuilderWithElementBuilders = false,
usingHashSetBuilder = false,
usingHashSetBuilderWithElementBuilders = false,
usingHashMapBuilder = false,
generateWithInterface = false
generateFieldSupplier = OptionState.DISABLED,
generateFieldConsumer = OptionState.DISABLED,
generateBuilderConsumer = OptionState.DISABLED,
generateConditionalHelper = OptionState.DISABLED,
generateVarArgsHelpers = OptionState.DISABLED,
generateStringFormatHelpers = 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
))
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
Expand Down Expand Up @@ -882,7 +996,7 @@ Configuration resolution follows these priority rules:
// Global default: true

@SimpleBuilder
@SimpleBuilder.Options(generateFieldSupplier = true) // Annotation wins!
@SimpleBuilder.Options(generateFieldSupplier = OptionState.ENABLED) // Annotation wins!
public class Person {
private String name;
}
Expand Down Expand Up @@ -1115,6 +1229,7 @@ methodAccess = AccessModifier.PRIVATE
implementsBuilderBase = OptionState.ENABLED,
usingGeneratedAnnotation = OptionState.ENABLED,
usingBuilderImplementationAnnotation = OptionState.ENABLED,
usingJacksonDeserializerAnnotation = OptionState.ENABLED,

// Naming
builderSuffix = "Builder",
Expand Down
8 changes: 8 additions & 0 deletions example/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<code-format.version>2.25</code-format.version>
<commons-lang.version>3.18.0</commons-lang.version>
<junit-jupiter.version>5.11.4</junit-jupiter.version>
<jackson-databind.version>2.18.2</jackson-databind.version>

<plugin.maven.compiler.version>3.13.0</plugin.maven.compiler.version>
<plugin.maven.deploy.version>3.1.1</plugin.maven.deploy.version>
Expand Down Expand Up @@ -49,6 +50,13 @@
<scope>test</scope>
</dependency>

<!-- Jackson dependency required for testing Jackson integration features -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson-databind.version}</version>
</dependency>

</dependencies>


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,19 @@
generateConditionalHelper = OptionState.DISABLED,
generateVarArgsHelpers = OptionState.DISABLED,
generateStringFormatHelpers = 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))
usingGeneratedAnnotation = OptionState.DISABLED,
usingBuilderImplementationAnnotation = OptionState.DISABLED,
implementsBuilderBase = OptionState.DISABLED,
usingJacksonDeserializerAnnotation = OptionState.DISABLED))
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface ElementaryBuilder {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.javahelpers.simple.builders.example;

import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
import org.javahelpers.simple.builders.core.enums.OptionState;

@SimpleBuilder(
options =
@SimpleBuilder.Options(
generateJacksonModule = OptionState.ENABLED,
usingJacksonDeserializerAnnotation = OptionState.ENABLED))
public class JacksonIntegrationDto {
private final String name;
private final int age;

// Protected constructor - Jackson can't access this, but the builder can
protected JacksonIntegrationDto(String name, int age) {
this.name = name;
this.age = age;
}

public String name() {
return name;
}

public int age() {
return age;
}
}
Loading
Loading