diff --git a/README.md b/README.md
index 21b72b7e..0c9558e7 100644
--- a/README.md
+++ b/README.md
@@ -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
@@ -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
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 78c30449..ccc79376 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
@@ -568,6 +568,55 @@
*/
OptionState generateWithInterface() default OptionState.UNSET;
+ /**
+ * Add Jackson annotations to the generated builder class.
+ * Adds {@code @JsonPOJOBuilder(withPrefix = "...")} to the builder class. The prefix matches
+ * the configured {@link #setterSuffix()}.
+ *
+ *
Example:
+ *
+ *
{@code
+ * @JsonDeserialize(builder = PersonDtoBuilder.class)
+ * public class PersonDto { ... }
+ *
+ * // Generated:
+ * @JsonPOJOBuilder(withPrefix = "")
+ * public class PersonDtoBuilder { ... }
+ * }
+ *
+ * Default: DISABLED
+ * Compiler option: -Asimplebuilder.usingJacksonDeserializerAnnotation
+ */
+ OptionState usingJacksonDeserializerAnnotation() default OptionState.UNSET;
+
+ /**
+ * Generate a Jackson SimpleModule containing registrations for all generated builders.
+ * This module allows Jackson to use the generated builders for deserialization without needing
+ * to annotate the DTO classes.
+ *
+ * The generated module class will be named {@code SimpleBuildersJacksonModule} (by default).
+ * By default, a module is generated in each package containing processed DTOs. To group
+ * all registrations into a single module, use {@link #jacksonModulePackage()}.
+ *
+ *
Default: DISABLED
+ * Compiler option: -Asimplebuilder.generateJacksonModule
+ */
+ OptionState generateJacksonModule() default OptionState.UNSET;
+
+ /**
+ * Specifies the package name where the {@code SimpleBuildersJacksonModule} class will be
+ * generated.
+ * This is useful to avoid split-package issues or to group all module registrations into a
+ * single module.
+ *
+ *
If not specified, a separate module will be generated in each package containing
+ * processed DTOs.
+ *
+ *
Default: "" (empty - generate one module per package)
+ * Compiler option: -Asimplebuilder.jacksonModulePackage
+ */
+ String jacksonModulePackage() default "";
+
// === Naming ===
/**
* Suffix to append to the DTO name to generate the builder class name.
diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md
index 103166a9..372f524c 100644
--- a/docs/CONFIGURATION.md
+++ b/docs/CONFIGURATION.md
@@ -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)
@@ -681,6 +689,93 @@ public class PersonDtoBuilder implements IBuilderBase {
---
+#### `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`
@@ -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;
@@ -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 memberNames;
@@ -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)
@@ -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;
}
@@ -1115,6 +1229,7 @@ methodAccess = AccessModifier.PRIVATE
implementsBuilderBase = OptionState.ENABLED,
usingGeneratedAnnotation = OptionState.ENABLED,
usingBuilderImplementationAnnotation = OptionState.ENABLED,
+ usingJacksonDeserializerAnnotation = OptionState.ENABLED,
// Naming
builderSuffix = "Builder",
diff --git a/example/pom.xml b/example/pom.xml
index 50d19132..e58291dc 100644
--- a/example/pom.xml
+++ b/example/pom.xml
@@ -17,6 +17,7 @@
2.25
3.18.0
5.11.4
+ 2.18.2
3.13.0
3.1.1
@@ -49,6 +50,13 @@
test
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ ${jackson-databind.version}
+
+
diff --git a/example/src/main/java/org/javahelpers/simple/builders/example/ElementaryBuilder.java b/example/src/main/java/org/javahelpers/simple/builders/example/ElementaryBuilder.java
index 24d1b4d2..88037b35 100644
--- a/example/src/main/java/org/javahelpers/simple/builders/example/ElementaryBuilder.java
+++ b/example/src/main/java/org/javahelpers/simple/builders/example/ElementaryBuilder.java
@@ -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 {}
diff --git a/example/src/main/java/org/javahelpers/simple/builders/example/JacksonIntegrationDto.java b/example/src/main/java/org/javahelpers/simple/builders/example/JacksonIntegrationDto.java
new file mode 100644
index 00000000..f0f7e190
--- /dev/null
+++ b/example/src/main/java/org/javahelpers/simple/builders/example/JacksonIntegrationDto.java
@@ -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;
+ }
+}
diff --git a/example/src/test/java/org/javahelpers/simple/builders/example/JacksonIntegrationTest.java b/example/src/test/java/org/javahelpers/simple/builders/example/JacksonIntegrationTest.java
new file mode 100644
index 00000000..f5859f29
--- /dev/null
+++ b/example/src/test/java/org/javahelpers/simple/builders/example/JacksonIntegrationTest.java
@@ -0,0 +1,49 @@
+package org.javahelpers.simple.builders.example;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class JacksonIntegrationTest {
+
+ @Test
+ void shouldFailWithoutJacksonModule() {
+ // Given
+ ObjectMapper mapper = new ObjectMapper();
+ // NOTE: NOT registering the generated module
+
+ String json = "{\"name\":\"Alice\",\"age\":30}";
+
+ // When & Then
+ InvalidDefinitionException exception = assertThrows(
+ InvalidDefinitionException.class,
+ () -> mapper.readValue(json, JacksonIntegrationDto.class)
+ );
+
+ // Should fail because Jackson can't access protected constructor
+ assertNotNull(exception.getMessage());
+ assertTrue(exception.getMessage().contains("no Creators, like default constructor, exist"));
+ }
+
+ @Test
+ void shouldSucceedWithJacksonModule() throws Exception {
+ // Given
+ ObjectMapper mapper = new ObjectMapper();
+ // Register the generated module
+ mapper.registerModule(new SimpleBuildersJacksonModule());
+
+ String json = "{\"name\":\"Alice\",\"age\":30}";
+
+ // When
+ JacksonIntegrationDto dto = mapper.readValue(json, JacksonIntegrationDto.class);
+
+ // Then
+ assertNotNull(dto);
+ assertEquals("Alice", dto.name());
+ assertEquals(30, dto.age());
+ }
+}
diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java
index 3b8dbb0b..704c96fd 100644
--- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java
+++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java
@@ -7,25 +7,18 @@
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
-import java.util.ArrayList;
-import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.javahelpers.simple.builders.core.annotations.BuilderImplementation;
-import org.javahelpers.simple.builders.core.interfaces.IBuilderBase;
import org.javahelpers.simple.builders.core.util.BuilderToStringStyle;
import org.javahelpers.simple.builders.core.util.TrackedValue;
/**
* Builder for {@code org.javahelpers.simple.builders.example.BookDto}.
*/
-@BuilderImplementation(
- forClass = BookDto.class
-)
-public class BookDtoBuilder implements IBuilderBase {
+public class BookDtoBuilder {
/**
* Tracked value for title: the book title to set.
*/
@@ -178,42 +171,6 @@ public BookDtoBuilder(BookDto instance) {
public BookDtoBuilder() {
}
- /**
- * Adds a single element to genres.
- *
- * @param element the element to add
- * @return current instance of builder
- */
- public BookDtoBuilder add2Genres(String element) {
- Set newCollection;
- if (this.genres.isSet()) {
- newCollection = new HashSet<>(this.genres.value());
- } else {
- newCollection = new HashSet<>();
- }
- newCollection.add(element);
- this.genres = changedValue(newCollection);
- return this;
- }
-
- /**
- * Adds a single element to tags.
- *
- * @param element the element to add
- * @return current instance of builder
- */
- public BookDtoBuilder add2Tags(String element) {
- List newCollection;
- if (this.tags.isSet()) {
- newCollection = new ArrayList<>(this.tags.value());
- } else {
- newCollection = new ArrayList<>();
- }
- newCollection.add(element);
- this.tags = changedValue(newCollection);
- return this;
- }
-
/**
* Sets the value for author.
*
@@ -423,7 +380,6 @@ public BookDtoBuilder title(String title) {
return this;
}
- @Override
public BookDto build() {
if (this.pages.isSet() && this.pages.value() == null) {
throw new IllegalStateException("Field 'pages' is marked as non-null but null value was provided");
diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SimpleBuildersJacksonModule.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SimpleBuildersJacksonModule.java
new file mode 100644
index 00000000..5766b3ae
--- /dev/null
+++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SimpleBuildersJacksonModule.java
@@ -0,0 +1,16 @@
+package org.javahelpers.simple.builders.example;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+
+public class SimpleBuildersJacksonModule extends SimpleModule {
+ public SimpleBuildersJacksonModule() {
+ setMixInAnnotation(JacksonIntegrationDto.class, JacksonIntegrationDtoMixin.class);
+ }
+
+ @JsonDeserialize(
+ builder = JacksonIntegrationDtoBuilder.class
+ )
+ private interface JacksonIntegrationDtoMixin {
+ }
+}
diff --git a/processor/pom.xml b/processor/pom.xml
index 5a741268..02dc3a64 100644
--- a/processor/pom.xml
+++ b/processor/pom.xml
@@ -59,6 +59,7 @@
4.5.0
6.0.1
0.23.0
+ 2.18.2
17
@@ -142,6 +143,14 @@
${google-compile-testing.version}
test
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ ${jackson-databind.version}
+ test
+
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 98054a55..042db372 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
@@ -47,6 +47,7 @@
import org.javahelpers.simple.builders.processor.exceptions.BuilderException;
import org.javahelpers.simple.builders.processor.util.BuilderConfigurationReader;
import org.javahelpers.simple.builders.processor.util.CompilerArgumentsReader;
+import org.javahelpers.simple.builders.processor.util.JacksonModuleGenerator;
import org.javahelpers.simple.builders.processor.util.JavaCodeGenerator;
import org.javahelpers.simple.builders.processor.util.ProcessingContext;
import org.javahelpers.simple.builders.processor.util.ProcessingLogger;
@@ -61,6 +62,7 @@
public class BuilderProcessor extends AbstractProcessor {
private ProcessingContext context;
private JavaCodeGenerator codeGenerator;
+ private JacksonModuleGenerator jacksonModuleGenerator;
private boolean supportedJdk = true;
@Override
@@ -76,7 +78,10 @@ public synchronized void init(ProcessingEnvironment processingEnv) {
new ProcessingContext(
processingEnv.getElementUtils(), processingEnv.getTypeUtils(), logger, globalConfig);
context.debug("Loaded global configuration from compiler arguments: %s", globalConfig);
- this.codeGenerator = new JavaCodeGenerator(processingEnv.getFiler(), logger);
+ this.codeGenerator =
+ new JavaCodeGenerator(processingEnv.getFiler(), processingEnv.getElementUtils(), logger);
+ this.jacksonModuleGenerator =
+ new JacksonModuleGenerator(processingEnv.getElementUtils(), logger);
SourceVersion current = processingEnv.getSourceVersion();
this.supportedJdk = isAtLeastJava17(current);
@@ -94,6 +99,15 @@ public boolean process(Set extends TypeElement> annotations, RoundEnvironment
return false;
}
+ // Generate Jackson Module if processing is over and feature is enabled
+ if (roundEnv.processingOver()) {
+ var modules = jacksonModuleGenerator.getModuleDefinitions();
+ for (var module : modules) {
+ codeGenerator.generateJacksonModule(module);
+ }
+ return false;
+ }
+
BuilderConfigurationReader reader = context.getConfigurationReader();
// Find all elements to process:
@@ -163,6 +177,9 @@ private void process(Element annotatedElement, BuilderConfiguration config)
context.initConfigurationForProcessingTarget(config);
BuilderDefinitionDto builderDef = extractFromElement(annotatedElement, context);
codeGenerator.generateBuilder(builderDef);
+
+ // Collect info for Jackson Module if enabled
+ jacksonModuleGenerator.addEntry(builderDef, annotatedElement);
}
/**
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java
index 3f3f4b93..87def7aa 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java
@@ -27,6 +27,7 @@
import static org.javahelpers.simple.builders.core.enums.AccessModifier.*;
import static org.javahelpers.simple.builders.core.enums.OptionState.*;
+import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
@@ -86,6 +87,9 @@ public record BuilderConfiguration(
OptionState usingBuilderImplementationAnnotation,
OptionState implementsBuilderBase,
OptionState generateWithInterface,
+ OptionState usingJacksonDeserializerAnnotation,
+ OptionState generateJacksonModule,
+ String jacksonModulePackage,
String builderSuffix,
String setterSuffix) {
@@ -112,6 +116,9 @@ public record BuilderConfiguration(
.usingBuilderImplementationAnnotation(ENABLED)
.implementsBuilderBase(ENABLED)
.generateWithInterface(ENABLED)
+ .usingJacksonDeserializerAnnotation(DISABLED)
+ .generateJacksonModule(DISABLED)
+ .jacksonModulePackage(null)
.builderSuffix("Builder")
.setterSuffix("")
.build();
@@ -137,6 +144,14 @@ public boolean shouldGenerateWithInterface() {
return generateWithInterface == ENABLED;
}
+ public boolean shouldUseJacksonDeserializerAnnotation() {
+ return usingJacksonDeserializerAnnotation == ENABLED;
+ }
+
+ public boolean shouldGenerateJacksonModule() {
+ return generateJacksonModule == ENABLED;
+ }
+
public boolean shouldGenerateVarArgsHelpers() {
return generateVarArgsHelpers == ENABLED;
}
@@ -202,6 +217,10 @@ public AccessModifier getMethodAccess() {
return methodAccess;
}
+ public String getJacksonModulePackage() {
+ return jacksonModulePackage;
+ }
+
public String getBuilderSuffix() {
return builderSuffix;
}
@@ -268,6 +287,12 @@ public BuilderConfiguration merge(BuilderConfiguration other) {
mergeOptionState(other.implementsBuilderBase, this.implementsBuilderBase))
.generateWithInterface(
mergeOptionState(other.generateWithInterface, this.generateWithInterface))
+ .usingJacksonDeserializerAnnotation(
+ mergeOptionState(
+ other.usingJacksonDeserializerAnnotation, this.usingJacksonDeserializerAnnotation))
+ .generateJacksonModule(
+ mergeOptionState(other.generateJacksonModule, this.generateJacksonModule))
+ .jacksonModulePackage(mergeString(other.jacksonModulePackage, this.jacksonModulePackage))
.builderSuffix(mergeString(other.builderSuffix, this.builderSuffix))
.setterSuffix(mergeString(other.setterSuffix, this.setterSuffix))
.build();
@@ -361,6 +386,15 @@ public String toString() {
if (generateWithInterface != UNSET) {
builder.append("generateWithInterface", generateWithInterface);
}
+ if (usingJacksonDeserializerAnnotation != UNSET) {
+ builder.append("usingJacksonDeserializerAnnotation", usingJacksonDeserializerAnnotation);
+ }
+ if (generateJacksonModule != UNSET) {
+ builder.append("generateJacksonModule", generateJacksonModule);
+ }
+ if (jacksonModulePackage != null) {
+ builder.append("jacksonModulePackage", jacksonModulePackage);
+ }
if (builderSuffix != null && !builderSuffix.equals("Builder")) {
builder.append("builderSuffix", builderSuffix);
}
@@ -406,6 +440,9 @@ public static class Builder {
// === Integration ===
private OptionState implementsBuilderBase = OptionState.UNSET;
private OptionState generateWithInterface = OptionState.UNSET;
+ private OptionState usingJacksonDeserializerAnnotation = OptionState.UNSET;
+ private OptionState generateJacksonModule = OptionState.UNSET;
+ private String jacksonModulePackage = null;
// === Naming ===
private String builderSuffix = null;
@@ -462,6 +499,31 @@ public Builder generateWithInterface(boolean value) {
return this;
}
+ public Builder usingJacksonDeserializerAnnotation(OptionState value) {
+ this.usingJacksonDeserializerAnnotation = value;
+ return this;
+ }
+
+ public Builder usingJacksonDeserializerAnnotation(boolean value) {
+ this.usingJacksonDeserializerAnnotation = value ? ENABLED : DISABLED;
+ return this;
+ }
+
+ public Builder generateJacksonModule(OptionState value) {
+ this.generateJacksonModule = value;
+ return this;
+ }
+
+ public Builder generateJacksonModule(boolean value) {
+ this.generateJacksonModule = value ? ENABLED : DISABLED;
+ return this;
+ }
+
+ public Builder jacksonModulePackage(String value) {
+ this.jacksonModulePackage = StringUtils.trimToNull(value);
+ return this;
+ }
+
public Builder generateVarArgsHelpers(OptionState value) {
this.generateVarArgsHelpers = value;
return this;
@@ -655,6 +717,9 @@ public BuilderConfiguration build() {
usingBuilderImplementationAnnotation,
implementsBuilderBase,
generateWithInterface,
+ usingJacksonDeserializerAnnotation,
+ generateJacksonModule,
+ jacksonModulePackage,
builderSuffix,
setterSuffix);
}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/JacksonModuleDefinitionDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/JacksonModuleDefinitionDto.java
new file mode 100644
index 00000000..b1acfdce
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/JacksonModuleDefinitionDto.java
@@ -0,0 +1,58 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025 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.dtos;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Definition for generating a Jackson SimpleModule. Contains the target package and the entries to
+ * be registered in the module.
+ */
+public class JacksonModuleDefinitionDto {
+ private final String targetPackage;
+ private final Set entries = new HashSet<>();
+
+ public JacksonModuleDefinitionDto(String targetPackage) {
+ this.targetPackage = targetPackage;
+ }
+
+ public String getTargetPackage() {
+ return targetPackage;
+ }
+
+ public Set getEntries() {
+ return entries;
+ }
+
+ public void addEntry(JacksonModuleEntryDto entry) {
+ this.entries.add(entry);
+ }
+
+ public void addAllEntries(Collection entries) {
+ this.entries.addAll(entries);
+ }
+}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/JacksonModuleEntryDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/JacksonModuleEntryDto.java
new file mode 100644
index 00000000..53a236a9
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/JacksonModuleEntryDto.java
@@ -0,0 +1,33 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025 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.dtos;
+
+/**
+ * Represents an entry for a Jackson module registration. Maps a DTO type to its builder type.
+ *
+ * @param dtoType the TypeName of the DTO
+ * @param builderType the TypeName of the builder
+ */
+public record JacksonModuleEntryDto(TypeName dtoType, TypeName builderType) {}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java
index 61d93b32..0957e6d5 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java
@@ -107,6 +107,15 @@ public enum CompilerArgumentsEnum {
/** Option for With interface generation. */
GENERATE_WITH_INTERFACE("generateWithInterface"),
+ /** Option for Jackson support. */
+ USING_JACKSON_DESERIALIZER_ANNOTATION("usingJacksonDeserializerAnnotation"),
+
+ /** Option for Jackson Module generation. */
+ GENERATE_JACKSON_MODULE("generateJacksonModule"),
+
+ /** Option for Jackson Module package name. */
+ JACKSON_MODULE_PACKAGE("jacksonModulePackage"),
+
// === Naming ===
/** Option for builder class name suffix. */
BUILDER_SUFFIX("builderSuffix"),
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java
index 70b113e9..1cfcc8d5 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java
@@ -192,6 +192,11 @@ private BuilderConfiguration parseOptionsFromMirror(AnnotationMirror optionsMirr
builder.implementsBuilderBase(OptionState.valueOf(enumValue));
case "generateWithInterface" ->
builder.generateWithInterface(OptionState.valueOf(enumValue));
+ case "usingJacksonDeserializerAnnotation" ->
+ builder.usingJacksonDeserializerAnnotation(OptionState.valueOf(enumValue));
+ case "generateJacksonModule" ->
+ builder.generateJacksonModule(OptionState.valueOf(enumValue));
+ case "jacksonModulePackage" -> builder.jacksonModulePackage(value.toString());
case "builderSuffix" -> builder.builderSuffix(value.toString());
case "setterSuffix" -> builder.setterSuffix(value.toString());
default ->
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java
index 585883f4..51bd87aa 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java
@@ -169,6 +169,10 @@ public BuilderConfiguration readBuilderConfiguration() {
readOptionState(CompilerArgumentsEnum.USING_BUILDER_IMPLEMENTATION_ANNOTATION))
.implementsBuilderBase(readOptionState(CompilerArgumentsEnum.IMPLEMENTS_BUILDER_BASE))
.generateWithInterface(readOptionState(CompilerArgumentsEnum.GENERATE_WITH_INTERFACE))
+ .usingJacksonDeserializerAnnotation(
+ readOptionState(CompilerArgumentsEnum.USING_JACKSON_DESERIALIZER_ANNOTATION))
+ .generateJacksonModule(readOptionState(CompilerArgumentsEnum.GENERATE_JACKSON_MODULE))
+ .jacksonModulePackage(readValue(CompilerArgumentsEnum.JACKSON_MODULE_PACKAGE))
.builderSuffix(readValue(CompilerArgumentsEnum.BUILDER_SUFFIX))
.setterSuffix(readValue(CompilerArgumentsEnum.SETTER_SUFFIX))
.build();
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JacksonModuleGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JacksonModuleGenerator.java
new file mode 100644
index 00000000..fdf36740
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JacksonModuleGenerator.java
@@ -0,0 +1,136 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025 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.util;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import javax.lang.model.element.Element;
+import javax.lang.model.util.Elements;
+import org.apache.commons.collections4.SetValuedMap;
+import org.apache.commons.collections4.multimap.HashSetValuedHashMap;
+import org.javahelpers.simple.builders.processor.dtos.BuilderConfiguration;
+import org.javahelpers.simple.builders.processor.dtos.BuilderDefinitionDto;
+import org.javahelpers.simple.builders.processor.dtos.JacksonModuleDefinitionDto;
+import org.javahelpers.simple.builders.processor.dtos.JacksonModuleEntryDto;
+
+/**
+ * Generates Jackson SimpleModules to register all generated builders.
+ *
+ * The generated modules include MixIn interfaces to map DTOs to their Builders using
+ * {@code @JsonDeserialize(builder = ...)}. One module is generated per target package.
+ */
+public class JacksonModuleGenerator {
+
+ private final Elements elementUtils;
+ private final ProcessingLogger logger;
+ private final SetValuedMap entriesByPackage =
+ new HashSetValuedHashMap<>();
+ private final boolean jacksonAvailable;
+
+ public JacksonModuleGenerator(Elements elementUtils, ProcessingLogger logger) {
+ this.elementUtils = elementUtils;
+ this.logger = logger;
+ this.jacksonAvailable =
+ this.elementUtils.getTypeElement("com.fasterxml.jackson.databind.module.SimpleModule")
+ != null;
+ }
+
+ public void addEntry(BuilderDefinitionDto builderDef, Element sourceElement) {
+ BuilderConfiguration config = builderDef.getConfiguration();
+
+ if (!validateForModuleGeneration(config, sourceElement)) {
+ return;
+ }
+
+ String targetPackage = getTargetPackage(config, builderDef);
+
+ entriesByPackage.put(
+ targetPackage,
+ new JacksonModuleEntryDto(
+ builderDef.getBuildingTargetTypeName(), builderDef.getBuilderTypeName()));
+ }
+
+ private String getTargetPackage(BuilderConfiguration config, BuilderDefinitionDto builderDef) {
+ String targetPackage = config.getJacksonModulePackage();
+ if (targetPackage == null) {
+ targetPackage = builderDef.getBuildingTargetTypeName().getPackageName();
+ }
+ return targetPackage;
+ }
+
+ private boolean validateForModuleGeneration(BuilderConfiguration config, Element sourceElement) {
+ if (!config.shouldGenerateJacksonModule()) {
+ return false;
+ }
+
+ if (!jacksonAvailable) {
+ logger.warning(
+ sourceElement,
+ "simple-builders: generateJacksonModule is enabled for %s, but Jackson dependencies (com.fasterxml.jackson.databind.module.SimpleModule) are not found on the classpath. Module generation skipped.",
+ sourceElement.getSimpleName());
+ return false;
+ }
+
+ if (!config.shouldUseJacksonDeserializerAnnotation()) {
+ logger.warning(
+ sourceElement,
+ "simple-builders: generateJacksonModule is enabled but usingJacksonDeserializerAnnotation is disabled. "
+ + "This is a misconfiguration. Jackson Module will NOT be generated for %s.",
+ sourceElement.getSimpleName());
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns the definitions for the Jackson Modules to be generated.
+ *
+ * Logs status and clears state after retrieval.
+ *
+ * @return list of JacksonModuleDefinitionDto
+ */
+ public List getModuleDefinitions() {
+ List definitions = new ArrayList<>();
+ if (!entriesByPackage.isEmpty()) {
+ logger.info(
+ "simple-builders: Processing OVER. JacksonModuleEnabled: true, Packages: %d",
+ entriesByPackage.keySet().size());
+
+ for (String packageName : entriesByPackage.keySet()) {
+ Set moduleEntries = entriesByPackage.get(packageName);
+ JacksonModuleDefinitionDto definition = new JacksonModuleDefinitionDto(packageName);
+ definition.addAllEntries(moduleEntries);
+ definitions.add(definition);
+ }
+ }
+ clear();
+ return definitions;
+ }
+
+ private void clear() {
+ entriesByPackage.clear();
+ }
+}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java
index e4c2b58a..db6856c5 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java
@@ -44,6 +44,7 @@
import javax.annotation.processing.Filer;
import javax.annotation.processing.Generated;
import javax.lang.model.element.Modifier;
+import javax.lang.model.util.Elements;
import org.apache.commons.lang3.StringUtils;
import org.javahelpers.simple.builders.core.annotations.BuilderImplementation;
import org.javahelpers.simple.builders.core.interfaces.IBuilderBase;
@@ -58,6 +59,7 @@ public class JavaCodeGenerator {
private static final String THROW_EXCEPTION_FORMAT = "throw new $T($S)";
private final Filer filer;
+ private final Elements elementUtils;
/** Logger for debug output during code generation. */
private final ProcessingLogger logger;
@@ -67,10 +69,12 @@ public class JavaCodeGenerator {
*
* @param filer Util class for source code generation of type {@code
* javax.annotation.processing.Filer}
+ * @param elementUtils Util class for operating on program elements
* @param logger Logger for debug output
*/
- public JavaCodeGenerator(Filer filer, ProcessingLogger logger) {
+ public JavaCodeGenerator(Filer filer, Elements elementUtils, ProcessingLogger logger) {
this.filer = filer;
+ this.elementUtils = elementUtils;
this.logger = logger;
}
@@ -210,17 +214,28 @@ public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderExcep
if (builderDef.getConfiguration().shouldUseBuilderImplementationAnnotation()) {
classBuilder.addAnnotation(createAnnotationBuilderImplementation(dtoBaseClass));
}
+ if (builderDef.getConfiguration().shouldUseJacksonDeserializerAnnotation()) {
+ if (elementUtils.getTypeElement("com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder")
+ != null) {
+ classBuilder.addAnnotation(
+ createAnnotationJsonPOJOBuilder(builderDef.getConfiguration().getSetterSuffix()));
+ } else {
+ logger.warning(
+ "Jackson support enabled but 'com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder' not found on classpath. Annotation skipped.");
+ }
+ }
logger.debug(
"Writing builder class to file: %s.%s",
builderDef.getBuilderTypeName().getPackageName(),
builderDef.getBuilderTypeName().getClassName());
- writeClassToFile(builderDef.getBuilderTypeName().getPackageName(), classBuilder.build());
+ writeBuilderClassToFile(builderDef.getBuilderTypeName().getPackageName(), classBuilder.build());
logger.debug(
"Successfully generated builder: %s", builderDef.getBuilderTypeName().getClassName());
}
- private void writeClassToFile(String packageName, TypeSpec typeSpec) throws BuilderException {
+ private void writeBuilderClassToFile(String packageName, TypeSpec typeSpec)
+ throws BuilderException {
try {
JavaFile.builder(packageName, typeSpec)
.skipJavaLangImports(true)
@@ -234,6 +249,15 @@ private void writeClassToFile(String packageName, TypeSpec typeSpec) throws Buil
}
}
+ private void writeSimpleClassToFile(String packageName, TypeSpec typeSpec)
+ throws BuilderException {
+ try {
+ JavaFile.builder(packageName, typeSpec).skipJavaLangImports(true).build().writeTo(filer);
+ } catch (IOException ex) {
+ throw new BuilderException(null, ex);
+ }
+ }
+
/**
* Resolves method conflicts by keeping only the highest priority method for each unique
* signature. This prevents compilation errors when methods from different fields have the same
@@ -317,6 +341,14 @@ private AnnotationSpec createAnnotationBuilderImplementation(ClassName dtoClass)
.build();
}
+ private AnnotationSpec createAnnotationJsonPOJOBuilder(String setterPrefix) {
+ ClassName jsonPojoBuilderClass =
+ ClassName.get("com.fasterxml.jackson.databind.annotation", "JsonPOJOBuilder");
+ return AnnotationSpec.builder(jsonPojoBuilderClass)
+ .addMember("withPrefix", "$S", setterPrefix == null ? "" : setterPrefix)
+ .build();
+ }
+
private MethodSpec createEmptyConstructor(ClassName dtoClass, Modifier accessModifier) {
MethodSpec.Builder constructorBuilder =
MethodSpec.constructorBuilder()
@@ -705,4 +737,68 @@ private ParameterSpec createParameter(MethodParameterDto paramDto) {
}
return paramBuilder.build();
}
+
+ /**
+ * Generates a Jackson SimpleModule based on the provided definition.
+ *
+ * @param moduleDef the definition of the Jackson module to generate
+ */
+ public void generateJacksonModule(JacksonModuleDefinitionDto moduleDef) {
+ String packageName = moduleDef.getTargetPackage();
+ String moduleClassName = "SimpleBuildersJacksonModule";
+
+ logger.info("Generating Jackson Module '%s' in package '%s'", moduleClassName, packageName);
+
+ ClassName simpleModuleClass =
+ ClassName.get("com.fasterxml.jackson.databind.module", "SimpleModule");
+ ClassName jsonDeserializeClass =
+ ClassName.get("com.fasterxml.jackson.databind.annotation", "JsonDeserialize");
+
+ // Create the constructor
+ MethodSpec.Builder constructorBuilder =
+ MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC);
+
+ // Create the class
+ TypeSpec.Builder classBuilder =
+ TypeSpec.classBuilder(moduleClassName)
+ .addModifiers(Modifier.PUBLIC)
+ .superclass(simpleModuleClass);
+
+ for (JacksonModuleEntryDto entry : moduleDef.getEntries()) {
+ ClassName dtoClass =
+ ClassName.get(entry.dtoType().getPackageName(), entry.dtoType().getClassName());
+ ClassName builderClass =
+ ClassName.get(entry.builderType().getPackageName(), entry.builderType().getClassName());
+
+ // Create MixIn interface name: DtoNameMixin
+ String mixinName = entry.dtoType().getClassName() + "Mixin";
+
+ // Create MixIn interface with @JsonDeserialize(builder = Builder.class)
+ TypeSpec mixinInterface =
+ TypeSpec.interfaceBuilder(mixinName)
+ .addModifiers(Modifier.PRIVATE)
+ .addAnnotation(
+ AnnotationSpec.builder(jsonDeserializeClass)
+ .addMember("builder", "$T.class", builderClass)
+ .build())
+ .build();
+
+ classBuilder.addType(mixinInterface);
+
+ // Add registration to constructor: setMixInAnnotation(Dto.class, Mixin.class)
+ constructorBuilder.addStatement(
+ "setMixInAnnotation($T.class, $N.class)", dtoClass, mixinName);
+ }
+
+ classBuilder.addMethod(constructorBuilder.build());
+
+ // Write file
+ try {
+ writeSimpleClassToFile(packageName, classBuilder.build());
+ } catch (BuilderException e) {
+ logger.warning(
+ "simple-builders: Error generating Jackson module for package %s: %s\n%s",
+ packageName, e.getMessage(), java.util.Arrays.toString(e.getStackTrace()));
+ }
+ }
}
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java
index c84a0e09..fa3cccdc 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java
@@ -420,6 +420,7 @@ void resolveConfiguration_AllLayersTogether_CompleteChain() {
usingHashMapBuilder = OptionState.DISABLED,
usingGeneratedAnnotation = OptionState.DISABLED,
usingBuilderImplementationAnnotation = OptionState.DISABLED,
+ usingJacksonDeserializerAnnotation = OptionState.DISABLED,
builderSuffix = "MinimalBuilder",
setterSuffix = "with"
))
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java
index 6ddaf5fd..4393af6f 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java
@@ -79,6 +79,8 @@ void allConfigurationOptions_MustBeSettableViaBuilder() {
// Integration
.implementsBuilderBase(OptionState.ENABLED)
.generateWithInterface(OptionState.ENABLED)
+ .usingJacksonDeserializerAnnotation(OptionState.ENABLED)
+ .generateJacksonModule(OptionState.ENABLED)
// Naming
.builderSuffix("Builder")
.setterSuffix("")
@@ -106,6 +108,8 @@ void allConfigurationOptions_MustBeSettableViaBuilder() {
assertEquals(OptionState.ENABLED, config.usingBuilderImplementationAnnotation());
assertEquals(OptionState.ENABLED, config.implementsBuilderBase());
assertEquals(OptionState.ENABLED, config.generateWithInterface());
+ assertEquals(OptionState.ENABLED, config.usingJacksonDeserializerAnnotation());
+ assertEquals(OptionState.ENABLED, config.generateJacksonModule());
assertEquals("Builder", config.getBuilderSuffix());
assertEquals("", config.getSetterSuffix());
}
@@ -210,6 +214,7 @@ public Address() {}
"-Asimplebuilder.usingBuilderImplementationAnnotation=false",
"-Asimplebuilder.implementsBuilderBase=false",
"-Asimplebuilder.generateWithInterface=false",
+ "-Asimplebuilder.usingJacksonDeserializerAnnotation=false",
"-Asimplebuilder.builderSuffix=CustomBuilder",
"-Asimplebuilder.setterSuffix=with")
.compile(nestedDto, addressDto, source);
@@ -285,6 +290,9 @@ public Address() {}
ProcessorAsserts.assertNotContaining(
generatedCode, "implements IBuilderBase", "@Override public MinimalDto build()");
+ // With usingJacksonDeserializerAnnotation=false, NO @JsonPOJOBuilder annotation should be used
+ ProcessorAsserts.assertNotContaining(generatedCode, "@JsonPOJOBuilder");
+
// With builderAccess=PACKAGE_PRIVATE, builder class should NOT have public modifier
ProcessorAsserts.assertNotContaining(generatedCode, "public class MinimalDtoCustomBuilder");
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleMultiPackageTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleMultiPackageTest.java
new file mode 100644
index 00000000..3cd791e5
--- /dev/null
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleMultiPackageTest.java
@@ -0,0 +1,145 @@
+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 com.google.testing.compile.Compilation;
+import javax.tools.JavaFileObject;
+import org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils;
+import org.junit.jupiter.api.Test;
+
+class JacksonModuleMultiPackageTest {
+
+ private static String extractContent(JavaFileObject fileObject) {
+ try {
+ return fileObject.getCharContent(false).toString();
+ } catch (Exception e) {
+ return fileObject.toString();
+ }
+ }
+
+ @Test
+ void generateJacksonModule_WithMultiplePackages_ShouldGenerateMultipleModules() {
+ // Given: Two DTOs in different packages, default configuration
+ JavaFileObject dto1 =
+ ProcessorTestUtils.simpleBuilderClass(
+ "pkg.one",
+ "DtoOne",
+ """
+ private String name;
+ public String getName() { return name; }
+ public void setName(String name) { this.name = name; }
+ """);
+
+ JavaFileObject dto2 =
+ ProcessorTestUtils.simpleBuilderClass(
+ "pkg.two",
+ "DtoTwo",
+ """
+ private int value;
+ public int getValue() { return value; }
+ public void setValue(int value) { this.value = value; }
+ """);
+
+ // When: Compile with Jackson module generation enabled, no global package set
+ Compilation compilation =
+ ProcessorTestUtils.createCompiler()
+ .withOptions(
+ "-Asimplebuilder.generateJacksonModule=true",
+ "-Asimplebuilder.usingJacksonDeserializerAnnotation=true")
+ .compile(dto1, dto2);
+
+ // Then: Should generate TWO modules, one in each package
+ assertThat(compilation).succeeded();
+
+ // Verify module in pkg.one
+ JavaFileObject module1 =
+ compilation.generatedSourceFile("pkg.one.SimpleBuildersJacksonModule").orElseThrow();
+ String pkgOneContent = extractContent(module1);
+ assertContaining(
+ pkgOneContent,
+ "package pkg.one;",
+ "import com.fasterxml.jackson.databind.annotation.JsonDeserialize;",
+ "import com.fasterxml.jackson.databind.module.SimpleModule;",
+ "public class SimpleBuildersJacksonModule extends SimpleModule",
+ "setMixInAnnotation(DtoOne.class, DtoOneMixin.class)",
+ "@JsonDeserialize",
+ "builder = DtoOneBuilder.class",
+ "private interface DtoOneMixin");
+
+ // Verify module in pkg.two
+ JavaFileObject module2 =
+ compilation.generatedSourceFile("pkg.two.SimpleBuildersJacksonModule").orElseThrow();
+ String pkgTwoContent = extractContent(module2);
+ assertContaining(
+ pkgTwoContent,
+ "package pkg.two;",
+ "import com.fasterxml.jackson.databind.annotation.JsonDeserialize;",
+ "import com.fasterxml.jackson.databind.module.SimpleModule;",
+ "public class SimpleBuildersJacksonModule extends SimpleModule",
+ "setMixInAnnotation(DtoTwo.class, DtoTwoMixin.class)",
+ "@JsonDeserialize",
+ "builder = DtoTwoBuilder.class",
+ "private interface DtoTwoMixin");
+ }
+
+ @Test
+ void generateJacksonModule_WithAnnotationOverride_ShouldRespectOverride() {
+ // Given: DTO with specific package override via annotation
+ // Note: We simulate this by having one DTO use default (its package) and another potentially
+ // using a different one if we could mocking the annotation value,
+ // but here we test the "default package" vs "configured package" behavior.
+ // Actually, testing annotation override specifically requires a DTO with
+ // @SimpleBuilder.Options(jacksonModulePackage="...")
+ // But SimpleBuilder.Options doesn't expose jacksonModulePackage yet in the annotation
+ // definition?
+ // Wait, we added it to BuilderConfiguration but did we add it to the @SimpleBuilder.Options
+ // annotation?
+ // Checking SimpleBuilder.java...
+
+ // We need to construct the source file manually to include the annotation with options
+ // because simpleBuilderClass doesn't support adding options easily
+ JavaFileObject sourceWithAnnotation =
+ ProcessorTestUtils.forSource(
+ """
+ package pkg.source;
+
+ 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,
+ jacksonModulePackage = "pkg.target"
+ ))
+ public class DtoWithOverride {
+ private String name;
+ public String getName() { return name; }
+ public void setName(String name) { this.name = name; }
+ }
+ """);
+
+ // When
+ Compilation compilation = ProcessorTestUtils.createCompiler().compile(sourceWithAnnotation);
+
+ // Then
+ assertThat(compilation).succeeded();
+
+ // Verify module in pkg.target (not pkg.source)
+ JavaFileObject module3 =
+ compilation.generatedSourceFile("pkg.target.SimpleBuildersJacksonModule").orElseThrow();
+ String pkgTargetContent = extractContent(module3);
+ assertContaining(
+ pkgTargetContent,
+ "package pkg.target;",
+ "import com.fasterxml.jackson.databind.annotation.JsonDeserialize;",
+ "import com.fasterxml.jackson.databind.module.SimpleModule;",
+ "import pkg.source.DtoWithOverride;",
+ "import pkg.source.DtoWithOverrideBuilder;",
+ "public class SimpleBuildersJacksonModule extends SimpleModule",
+ "setMixInAnnotation(DtoWithOverride.class, DtoWithOverrideMixin.class)",
+ "@JsonDeserialize",
+ "builder = DtoWithOverrideBuilder.class",
+ "private interface DtoWithOverrideMixin");
+ }
+}
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleTest.java
new file mode 100644
index 00000000..52e57297
--- /dev/null
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleTest.java
@@ -0,0 +1,92 @@
+package org.javahelpers.simple.builders.processor;
+
+import static com.google.testing.compile.CompilationSubject.assertThat;
+
+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.Test;
+
+class JacksonModuleTest {
+
+ @Test
+ void generateJacksonModule_WhenEnabled_ShouldGenerateModuleClass() {
+ // Given
+ JavaFileObject dto1 =
+ ProcessorTestUtils.simpleBuilderClass(
+ "test",
+ "FirstDto",
+ """
+ private String name;
+ public String getName() { return name; }
+ public void setName(String name) { this.name = name; }
+ """);
+
+ JavaFileObject dto2 =
+ ProcessorTestUtils.simpleBuilderClass(
+ "test",
+ "SecondDto",
+ """
+ private int age;
+ public int getAge() { return age; }
+ public void setAge(int age) { this.age = age; }
+ """);
+
+ // When
+ Compilation compilation =
+ ProcessorTestUtils.createCompiler()
+ .withOptions(
+ "-Asimplebuilder.generateJacksonModule=true",
+ "-Asimplebuilder.usingJacksonDeserializerAnnotation=true")
+ .compile(dto1, dto2);
+
+ // Then
+ assertThat(compilation).succeeded();
+
+ // Check for generated module class
+ // It should be in the same package as the first DTO ("test")
+ String generatedModule =
+ ProcessorTestUtils.loadGeneratedSource(compilation, "SimpleBuildersJacksonModule");
+
+ ProcessorAsserts.assertContaining(
+ generatedModule,
+ "package test;",
+ "public class SimpleBuildersJacksonModule extends SimpleModule",
+ "setMixInAnnotation(FirstDto.class, FirstDtoMixin.class)",
+ "setMixInAnnotation(SecondDto.class, SecondDtoMixin.class)",
+ "@JsonDeserialize(builder = FirstDtoBuilder.class)",
+ "@JsonDeserialize(builder = SecondDtoBuilder.class)");
+ }
+
+ @Test
+ void generateJacksonModule_WhenDisabled_ShouldNotGenerateModuleClass() {
+ // Given
+ JavaFileObject dto =
+ ProcessorTestUtils.simpleBuilderClass(
+ "test",
+ "ModuleDisabledDto",
+ """
+ private String name;
+ public String getName() { return name; }
+ public void setName(String name) { this.name = name; }
+ """);
+
+ // When
+ Compilation compilation =
+ ProcessorTestUtils.createCompiler()
+ .withOptions("-Asimplebuilder.generateJacksonModule=false")
+ .compile(dto);
+
+ // Then
+ assertThat(compilation).succeeded();
+
+ // Verify file is NOT generated
+ try {
+ ProcessorTestUtils.loadGeneratedSource(compilation, "SimpleBuildersJacksonModule");
+ throw new AssertionError("SimpleBuildersJacksonModule should not have been generated");
+ } catch (AssertionError e) {
+ // Expected
+ }
+ }
+}
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleWarningTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleWarningTest.java
new file mode 100644
index 00000000..7f871006
--- /dev/null
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleWarningTest.java
@@ -0,0 +1,75 @@
+package org.javahelpers.simple.builders.processor;
+
+import static com.google.testing.compile.CompilationSubject.assertThat;
+import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.createCompiler;
+import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.simpleBuilderClass;
+
+import com.google.testing.compile.Compilation;
+import javax.tools.JavaFileObject;
+import org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils;
+import org.junit.jupiter.api.Test;
+
+class JacksonModuleWarningTest {
+
+ @Test
+ void generateJacksonModuleEnabled_ButAnnotationDisabled_ShouldEmitWarningAndSkipGeneration() {
+ // Given
+ JavaFileObject dto =
+ simpleBuilderClass(
+ "pkg.test",
+ "MisconfiguredDto",
+ """
+ private String name;
+ public String getName() { return name; }
+ public void setName(String name) { this.name = name; }
+ """);
+
+ // When
+ // Enable generateJacksonModule but NOT usingJacksonDeserializerAnnotation (which defaults to
+ // DISABLED)
+ Compilation compilation =
+ createCompiler().withOptions("-Asimplebuilder.generateJacksonModule=true").compile(dto);
+
+ // Then
+ assertThat(compilation).succeeded();
+
+ // Should have a warning
+ assertThat(compilation)
+ .hadWarningContaining(
+ "simple-builders: generateJacksonModule is enabled but usingJacksonDeserializerAnnotation is disabled");
+
+ // Should NOT have generated the Jackson module
+ // We verify this by ensuring the warning is present, which implies the generation branch was
+ // skipped.
+ // Testing explicitly for "file not generated" is tricky with compile-testing without complex
+ // custom assertions.
+ }
+
+ @Test
+ void generateJacksonModule_WhenEnabledAndDeserializerEnabled_ShouldGenerate() {
+ // Given
+ JavaFileObject dto =
+ ProcessorTestUtils.simpleBuilderClass(
+ "test",
+ "SuccessDto",
+ """
+ private String name;
+ public String getName() { return name; }
+ public void setName(String name) { this.name = name; }
+ """);
+
+ // When
+ Compilation compilation =
+ ProcessorTestUtils.createCompiler()
+ .withOptions(
+ "-Asimplebuilder.generateJacksonModule=true",
+ "-Asimplebuilder.usingJacksonDeserializerAnnotation=true")
+ .compile(dto);
+
+ // Then
+ assertThat(compilation).succeeded();
+
+ // Verify file IS generated
+ ProcessorTestUtils.loadGeneratedSource(compilation, "SimpleBuildersJacksonModule");
+ }
+}
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonSupportTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonSupportTest.java
new file mode 100644
index 00000000..e77850dc
--- /dev/null
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonSupportTest.java
@@ -0,0 +1,128 @@
+package org.javahelpers.simple.builders.processor;
+
+import static com.google.testing.compile.CompilationSubject.assertThat;
+
+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.Test;
+
+class JacksonSupportTest {
+
+ @Test
+ void usingJacksonDeserializerAnnotation_WhenEnabled_ShouldGenerateAnnotation() {
+ // Given
+ JavaFileObject source =
+ ProcessorTestUtils.simpleBuilderClass(
+ "test",
+ "JacksonDto",
+ """
+ private String name;
+ public String getName() { return name; }
+ public void setName(String name) { this.name = name; }
+ """);
+
+ // When
+ Compilation compilation =
+ ProcessorTestUtils.createCompiler()
+ .withOptions("-Asimplebuilder.usingJacksonDeserializerAnnotation=true")
+ .compile(source);
+
+ // Then
+ assertThat(compilation).succeeded();
+ String generatedCode = ProcessorTestUtils.loadGeneratedSource(compilation, "JacksonDtoBuilder");
+
+ ProcessorAsserts.assertContaining(
+ generatedCode,
+ "import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;",
+ "@JsonPOJOBuilder(withPrefix = \"\")");
+ }
+
+ @Test
+ void
+ usingJacksonDeserializerAnnotation_WhenEnabledWithCustomPrefix_ShouldGenerateAnnotationWithPrefix() {
+ // Given
+ JavaFileObject source =
+ ProcessorTestUtils.simpleBuilderClass(
+ "test",
+ "JacksonCustomDto",
+ """
+ private String name;
+ public String getName() { return name; }
+ public void setName(String name) { this.name = name; }
+ """);
+
+ // When
+ Compilation compilation =
+ ProcessorTestUtils.createCompiler()
+ .withOptions(
+ "-Asimplebuilder.usingJacksonDeserializerAnnotation=true",
+ "-Asimplebuilder.setterSuffix=with")
+ .compile(source);
+
+ // Then
+ assertThat(compilation).succeeded();
+ String generatedCode =
+ ProcessorTestUtils.loadGeneratedSource(compilation, "JacksonCustomDtoBuilder");
+
+ ProcessorAsserts.assertContaining(generatedCode, "@JsonPOJOBuilder(withPrefix = \"with\")");
+ }
+
+ @Test
+ void usingJacksonDeserializerAnnotation_WhenDisabled_ShouldNotGenerateAnnotation() {
+ // Given
+ JavaFileObject source =
+ ProcessorTestUtils.simpleBuilderClass(
+ "test",
+ "NoJacksonDto",
+ """
+ private String name;
+ public String getName() { return name; }
+ public void setName(String name) { this.name = name; }
+ """);
+
+ // When
+ Compilation compilation =
+ ProcessorTestUtils.createCompiler()
+ // usingJacksonDeserializerAnnotation is disabled by default, but we can explicitly
+ // disable
+ // it
+ .withOptions("-Asimplebuilder.usingJacksonDeserializerAnnotation=false")
+ .compile(source);
+
+ // Then
+ assertThat(compilation).succeeded();
+ String generatedCode =
+ ProcessorTestUtils.loadGeneratedSource(compilation, "NoJacksonDtoBuilder");
+
+ ProcessorAsserts.assertNotContaining(generatedCode, "JsonPOJOBuilder");
+ }
+
+ @Test
+ void usingJacksonDeserializerAnnotation_WhenNotConfigured_ShouldDefaultToDisabled() {
+ // Given
+ JavaFileObject source =
+ ProcessorTestUtils.simpleBuilderClass(
+ "test",
+ "DefaultJacksonDto",
+ """
+ private String name;
+ public String getName() { return name; }
+ public void setName(String name) { this.name = name; }
+ """);
+
+ // When
+ Compilation compilation =
+ ProcessorTestUtils.createCompiler()
+ // No options provided
+ .compile(source);
+
+ // Then
+ assertThat(compilation).succeeded();
+ String generatedCode =
+ ProcessorTestUtils.loadGeneratedSource(compilation, "DefaultJacksonDtoBuilder");
+
+ ProcessorAsserts.assertNotContaining(generatedCode, "JsonPOJOBuilder");
+ }
+}