Skip to content

Commit 408c108

Browse files
committed
Update Jackson support to support both Jackson 2 and 3
Introduces a dedicated Jackson option structure. JSONPojoBuilder support can be enabled with a boolean flag. Detects available jackson version on the classpath and adds the respective annotations when enabled. Jackson version can be set to AUTO (adds annotation for every found version) or to JACKSON_2 or JACKSON_3 (only adds annotations for defined version, fails the build when library is not on the classpath).
1 parent 8be6b98 commit 408c108

8 files changed

Lines changed: 359 additions & 37 deletions

File tree

options.md

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,6 @@ The names used for generated methods, classes, etc. can be changed via the follo
5454
| `@RecordBuilder.Options(fileIndent = " ")` | Return the file indent to use. |
5555
| `@RecordBuilder.Options(prefixEnclosingClassNames = true/false)` | If the record is declared inside another class, the outer class's name will be prefixed to the builder name if this returns true. The default is `true`. |
5656

57-
## Jackson Support
58-
59-
| option | details |
60-
|--------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
61-
| `@RecordBuilder.Options(addJacksonAnnotations = true/false)` | If true, builders will be annotated with `@JsonPOJOBuilder` Jackson annotations which can be used in combination with `@JsonDeserialize(builder = ...)`. See [TestJacksonAnnotations](./record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestJacksonAnnotations.java) for an example. The default is `false`. |
62-
6357
## Miscellaneous
6458

6559
| option | details |
@@ -122,3 +116,58 @@ Special handling for collections. See the project test classes for usage.
122116
| `@RecordBuilder.Options(useUnmodifiableCollections = true/false)` | Adds special handling for collection record components. The default is `false`. |
123117
| `@RecordBuilder.Options(allowNullableCollections = true/false)` | Adds special null handling for record collectioncomponents. The default is `false`. |
124118
| `@RecordBuilder.Options(addSingleItemCollectionBuilders = true/false)` | Adds special handling for record collectioncomponents. The default is `false`. |
119+
120+
## Jackson Support
121+
122+
RecordBuilder can automatically add Jackson annotations to generated builders, supporting both Jackson 2.x and 3.x. Configuration is done via the nested `@JacksonConfig` annotation.
123+
124+
### Basic Example
125+
126+
```java
127+
@RecordBuilder
128+
@RecordBuilder.Options(
129+
jackson = @RecordBuilder.JacksonConfig(jsonPOJOBuilder = true)
130+
)
131+
@JsonDeserialize(builder = UserRecordBuilder.class)
132+
record UserRecord(String name, int age) {}
133+
```
134+
135+
### Configuration Options
136+
137+
| option | details |
138+
|--------|---------|
139+
| `jackson = @JacksonConfig(...)` | Configures Jackson annotation support for the generated builder. By default, no Jackson annotations are added. |
140+
141+
### JacksonConfig Properties
142+
143+
| property | details |
144+
|----------|---------|
145+
| `jsonPOJOBuilder` | **boolean** (default: `false`) - When `true`, adds `@JsonPOJOBuilder` annotation to the generated builder. This annotation works with `@JsonDeserialize(builder = ...)` on the record. |
146+
| `version` | **JacksonVersion** (default: `AUTO`) - Specifies which Jackson version to use:<br/>• `AUTO` - Automatically detect Jackson version(s) on classpath and add all found annotations. If both Jackson 2.x and 3.x are present, annotations for both versions will be added.<br/>• `JACKSON_2` - Only add Jackson 2.x annotations (`com.fasterxml.jackson.*`). Fails if Jackson 2.x is not found.<br/>• `JACKSON_3` - Only add Jackson 3.x annotations (`tools.jackson.*`). Fails if Jackson 3.x is not found. |
147+
148+
### Examples
149+
150+
#### Auto-detect Jackson version (default)
151+
```java
152+
@RecordBuilder.Options(jackson = @RecordBuilder.JacksonConfig(jsonPOJOBuilder = true))
153+
```
154+
155+
#### Explicit Jackson 2.x
156+
```java
157+
@RecordBuilder.Options(
158+
jackson = @RecordBuilder.JacksonConfig(
159+
jsonPOJOBuilder = true,
160+
version = JacksonVersion.JACKSON_2
161+
)
162+
)
163+
```
164+
165+
#### With custom setter prefix
166+
```java
167+
@RecordBuilder.Options(
168+
jackson = @RecordBuilder.JacksonConfig(jsonPOJOBuilder = true),
169+
setterPrefix = "set"
170+
)
171+
```
172+
173+
See [TestJacksonAnnotations](./record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestJacksonAnnotations.java) for complete examples.

pom.xml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@
6464
<hibernate-validator-version>6.2.0.Final</hibernate-validator-version>
6565
<jakarta-validation-api-version>3.1.0</jakarta-validation-api-version>
6666
<javax-el-version>3.0.1-b09</javax-el-version>
67-
<jackson-version>2.19.0</jackson-version>
67+
<jackson2-version>2.21.0</jackson2-version>
68+
<jackson3-version>3.0.4</jackson3-version>
6869
<central-publishing-maven-plugin-version>0.7.0</central-publishing-maven-plugin-version>
6970
<jspecify-version>1.0.0</jspecify-version>
7071
<lombok-version>1.18.42</lombok-version>
@@ -171,7 +172,12 @@
171172
<dependency>
172173
<groupId>com.fasterxml.jackson.core</groupId>
173174
<artifactId>jackson-databind</artifactId>
174-
<version>${jackson-version}</version>
175+
<version>${jackson2-version}</version>
176+
</dependency>
177+
<dependency>
178+
<groupId>tools.jackson.core</groupId>
179+
<artifactId>jackson-databind</artifactId>
180+
<version>${jackson3-version}</version>
175181
</dependency>
176182

177183
<dependency>

record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilder.java

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,30 @@
360360
*/
361361
boolean defaultNotNull() default false;
362362

363-
boolean addJacksonAnnotations() default false;
363+
/**
364+
* Configuration for Jackson annotation support on generated builders.
365+
*
366+
* @see JacksonConfig
367+
*/
368+
JacksonConfig jackson() default @JacksonConfig;
369+
}
370+
371+
/**
372+
* Configuration for Jackson annotation support on generated builders.
373+
*/
374+
@Retention(RetentionPolicy.CLASS)
375+
@Target(ElementType.ANNOTATION_TYPE)
376+
@interface JacksonConfig {
377+
/**
378+
* Add {@code @JsonPOJOBuilder} annotation to generated builder. This annotation works with
379+
* {@code @JsonDeserialize(builder = ...)} on the record.
380+
*/
381+
boolean jsonPOJOBuilder() default false;
382+
383+
/**
384+
* Which Jackson version to use for annotations.
385+
*/
386+
JacksonVersion version() default JacksonVersion.AUTO;
364387
}
365388

366389
@Retention(RetentionPolicy.CLASS)
@@ -380,6 +403,29 @@ enum ConcreteSettersForOptionalMode {
380403
DISABLED, ENABLED, ENABLED_WITH_NULLABLE_ANNOTATION,
381404
}
382405

406+
/**
407+
* Specifies which Jackson version(s) to use when generating builder annotations.
408+
*/
409+
enum JacksonVersion {
410+
/**
411+
* Automatically detect Jackson version(s) on classpath and add all found annotations. If both Jackson 2.x and
412+
* 3.x are present, both annotations will be added.
413+
*/
414+
AUTO,
415+
416+
/**
417+
* Only add Jackson 2.x annotations (com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder). Fails if
418+
* Jackson 2.x is not found on classpath.
419+
*/
420+
JACKSON_2,
421+
422+
/**
423+
* Only add Jackson 3.x annotations (tools.jackson.databind.annotation.JsonPOJOBuilder). Fails if Jackson 3.x is
424+
* not found on classpath.
425+
*/
426+
JACKSON_3,
427+
}
428+
383429
/**
384430
* Apply to record components to specify a field initializer for the generated builder
385431
*/

record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/InternalRecordBuilderProcessor.java

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ class InternalRecordBuilderProcessor {
8989
builder.addAnnotation(recordBuilderGeneratedAnnotation);
9090
}
9191

92-
addJacksonAnnotations();
92+
new JacksonSupport(processingEnv).addJacksonAnnotations(metaData, builder);
9393

9494
if (!validateMethodNameConflicts(processingEnv, recordFacade.element())) {
9595
builderType = Optional.empty();
@@ -201,18 +201,6 @@ private void addVisibility(boolean builderIsInRecordPackage, Set<Modifier> modif
201201
}
202202
}
203203

204-
private void addJacksonAnnotations() {
205-
if (!metaData.addJacksonAnnotations()) {
206-
return;
207-
}
208-
209-
final var annotationSpec = AnnotationSpec
210-
.builder(ClassName.get("com.fasterxml.jackson.databind.annotation", "JsonPOJOBuilder"))
211-
.addMember("withPrefix", "$S", metaData.setterPrefix()).build();
212-
213-
builder.addAnnotation(annotationSpec);
214-
}
215-
216204
private void addOnceOnlySupport() {
217205
if (recordComponents.isEmpty()) {
218206
return;
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright 2019 The original author or authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.soabase.recordbuilder.processor;
17+
18+
import com.palantir.javapoet.AnnotationSpec;
19+
import com.palantir.javapoet.ClassName;
20+
import com.palantir.javapoet.TypeSpec;
21+
import io.soabase.recordbuilder.core.RecordBuilder;
22+
23+
import javax.annotation.processing.ProcessingEnvironment;
24+
25+
import static javax.tools.Diagnostic.Kind.ERROR;
26+
27+
class JacksonSupport {
28+
private static final String JACKSON_2_ANNOTATION_PACKAGE = "com.fasterxml.jackson.databind.annotation";
29+
private static final String JACKSON_3_ANNOTATION_PACKAGE = "tools.jackson.databind.annotation";
30+
31+
private static final String JSON_POJO_BUILDER = "JsonPOJOBuilder";
32+
33+
private final ProcessingEnvironment processingEnv;
34+
private final boolean jackson2Present;
35+
private final boolean jackson3Present;
36+
37+
JacksonSupport(ProcessingEnvironment processingEnv) {
38+
this.processingEnv = processingEnv;
39+
jackson2Present = isAnnotationClassPresent(JACKSON_2_ANNOTATION_PACKAGE, JSON_POJO_BUILDER);
40+
jackson3Present = isAnnotationClassPresent(JACKSON_3_ANNOTATION_PACKAGE, JSON_POJO_BUILDER);
41+
}
42+
43+
private boolean isAnnotationClassPresent(String packageName, String className) {
44+
return processingEnv.getElementUtils().getTypeElement(packageName + "." + className) != null;
45+
}
46+
47+
public void addJacksonAnnotations(RecordBuilder.Options metaData, TypeSpec.Builder builder) {
48+
// return without further processing if no annotation is enabled
49+
if (!anyJacksonAnnotationEnabled(metaData)) {
50+
return;
51+
}
52+
53+
switch (metaData.jackson().version()) {
54+
case AUTO -> {
55+
if (!jackson2Present && !jackson3Present) {
56+
processingEnv.getMessager().printMessage(ERROR,
57+
"jackson.jsonPOJOBuilder is enabled but Jackson is not found on classpath. "
58+
+ "Add jackson-databind dependency or disable jsonPOJOBuilder.");
59+
return;
60+
}
61+
62+
if (jackson2Present) {
63+
addJacksonAnnotations(metaData, builder, JACKSON_2_ANNOTATION_PACKAGE);
64+
}
65+
66+
if (jackson3Present) {
67+
addJacksonAnnotations(metaData, builder, JACKSON_3_ANNOTATION_PACKAGE);
68+
}
69+
}
70+
71+
case JACKSON_2 -> {
72+
if (!jackson2Present) {
73+
processingEnv.getMessager().printMessage(ERROR,
74+
"jackson.version is set to JACKSON_2 but Jackson 2.x is not found on classpath. "
75+
+ "Add jackson-databind 2.x dependency or change version to AUTO.");
76+
return;
77+
}
78+
79+
addJacksonAnnotations(metaData, builder, JACKSON_2_ANNOTATION_PACKAGE);
80+
}
81+
82+
case JACKSON_3 -> {
83+
if (!jackson3Present) {
84+
processingEnv.getMessager().printMessage(ERROR,
85+
"jackson.version is set to JACKSON_3 but Jackson 3.x is not found on classpath. "
86+
+ "Add jackson-databind 3.x dependency or change version to AUTO.");
87+
return;
88+
}
89+
90+
addJacksonAnnotations(metaData, builder, JACKSON_3_ANNOTATION_PACKAGE);
91+
}
92+
}
93+
}
94+
95+
private boolean anyJacksonAnnotationEnabled(RecordBuilder.Options metaData) {
96+
return metaData.jackson().jsonPOJOBuilder();
97+
}
98+
99+
private void addJacksonAnnotations(RecordBuilder.Options metaData, TypeSpec.Builder builder, String packageName) {
100+
if (metaData.jackson().jsonPOJOBuilder()) {
101+
addJsonPOJOBuilderAnnotation(metaData, builder, packageName);
102+
}
103+
}
104+
105+
private void addJsonPOJOBuilderAnnotation(RecordBuilder.Options metaData, TypeSpec.Builder builder,
106+
String packageName) {
107+
final var annotationSpec = AnnotationSpec.builder(ClassName.get(packageName, JSON_POJO_BUILDER))
108+
.addMember("withPrefix", "$S", metaData.setterPrefix()).build();
109+
110+
builder.addAnnotation(annotationSpec);
111+
}
112+
}

record-builder-test/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@
7676
<groupId>com.fasterxml.jackson.core</groupId>
7777
<artifactId>jackson-databind</artifactId>
7878
</dependency>
79+
<dependency>
80+
<groupId>tools.jackson.core</groupId>
81+
<artifactId>jackson-databind</artifactId>
82+
</dependency>
7983

8084
<dependency>
8185
<groupId>org.glassfish</groupId>

record-builder-test/src/main/java/io/soabase/recordbuilder/test/JacksonAnnotated.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,40 @@ public interface JacksonAnnotated {
2828
Map<String, Object> properties();
2929

3030
@RecordBuilder
31-
@RecordBuilder.Options(addJacksonAnnotations = true, useImmutableCollections = true, prefixEnclosingClassNames = false)
31+
@RecordBuilder.Options(jackson = @RecordBuilder.JacksonConfig(jsonPOJOBuilder = true), useImmutableCollections = true, prefixEnclosingClassNames = false)
3232
@JsonDeserialize(builder = JacksonAnnotatedRecordBuilder.class)
3333
record JacksonAnnotatedRecord(String name, @RecordBuilder.Initializer("DEFAULT_TYPE") String type,
3434
Map<String, Object> properties) implements JacksonAnnotated {
3535
public static final String DEFAULT_TYPE = "dummy";
3636
}
3737

3838
@RecordBuilder
39-
@RecordBuilder.Options(addJacksonAnnotations = true, useImmutableCollections = true, prefixEnclosingClassNames = false, setterPrefix = "set")
39+
@RecordBuilder.Options(jackson = @RecordBuilder.JacksonConfig(jsonPOJOBuilder = true), useImmutableCollections = true, prefixEnclosingClassNames = false, setterPrefix = "set")
4040
@JsonDeserialize(builder = JacksonAnnotatedRecordCustomSetterPrefixBuilder.class)
4141
record JacksonAnnotatedRecordCustomSetterPrefix(String name, @RecordBuilder.Initializer("DEFAULT_TYPE") String type,
4242
Map<String, Object> properties) implements JacksonAnnotated {
4343
public static final String DEFAULT_TYPE = "dummy";
4444
}
45+
46+
@RecordBuilder
47+
@RecordBuilder.Options(jackson = @RecordBuilder.JacksonConfig(jsonPOJOBuilder = true, version = RecordBuilder.JacksonVersion.JACKSON_2), useImmutableCollections = true, prefixEnclosingClassNames = false)
48+
@JsonDeserialize(builder = JacksonAnnotatedRecordJackson2Builder.class)
49+
record JacksonAnnotatedRecordJackson2(String name, @RecordBuilder.Initializer("DEFAULT_TYPE") String type,
50+
Map<String, Object> properties) implements JacksonAnnotated {
51+
public static final String DEFAULT_TYPE = "dummy";
52+
}
53+
54+
@RecordBuilder
55+
@RecordBuilder.Options(jackson = @RecordBuilder.JacksonConfig(jsonPOJOBuilder = true, version = RecordBuilder.JacksonVersion.JACKSON_3), useImmutableCollections = true, prefixEnclosingClassNames = false)
56+
@tools.jackson.databind.annotation.JsonDeserialize(builder = JacksonAnnotatedRecordJackson3Builder.class)
57+
record JacksonAnnotatedRecordJackson3(String name, @RecordBuilder.Initializer("DEFAULT_TYPE") String type,
58+
Map<String, Object> properties) implements JacksonAnnotated {
59+
public static final String DEFAULT_TYPE = "dummy";
60+
}
61+
62+
@RecordBuilder
63+
@RecordBuilder.Options(prefixEnclosingClassNames = false)
64+
record JacksonAnnotatedRecordNoJackson(String name, String type, Map<String, Object> properties)
65+
implements JacksonAnnotated {
66+
}
4567
}

0 commit comments

Comments
 (0)