diff --git a/src/main/java/tools/jackson/databind/AnnotationIntrospector.java b/src/main/java/tools/jackson/databind/AnnotationIntrospector.java index 5ddeb565d1..1e41c20679 100644 --- a/src/main/java/tools/jackson/databind/AnnotationIntrospector.java +++ b/src/main/java/tools/jackson/databind/AnnotationIntrospector.java @@ -1134,6 +1134,24 @@ public JsonPOJOBuilder.Value findPOJOBuilderConfig(MapperConfig config, Annot return null; } + /** + * Method for finding the builder prefix specified on the value class + * via {@code @JsonDeserialize(builderPrefix=...)}. This provides an + * alternative way to configure the prefix used by Builder "with-methods" + * without having to annotate the Builder class itself with {@code @JsonPOJOBuilder}. + * + * @param config Effective mapper configuration in use + * @param valueClass The value class (not Builder class!) to check for the annotation + * + * @return Builder prefix if explicitly specified; {@code null} to use default behavior + * (check {@code @JsonPOJOBuilder} on builder class, or use global default) + * + * @since 3.1 + */ + public String findBuilderPrefix(MapperConfig config, AnnotatedClass valueClass) { + return null; + } + /** * Method called to check whether potential Creator (constructor or static factory * method) has explicit annotation to indicate it as actual Creator; and if so, diff --git a/src/main/java/tools/jackson/databind/annotation/JsonDeserialize.java b/src/main/java/tools/jackson/databind/annotation/JsonDeserialize.java index b6ce56cdde..30a0165281 100644 --- a/src/main/java/tools/jackson/databind/annotation/JsonDeserialize.java +++ b/src/main/java/tools/jackson/databind/annotation/JsonDeserialize.java @@ -42,6 +42,15 @@ @com.fasterxml.jackson.annotation.JacksonAnnotation public @interface JsonDeserialize { + /** + * Marker value used as default for {@link #builderPrefix} to indicate + * that default handling should be used (check for {@link JsonPOJOBuilder} + * annotation on builder class, or use default prefix of "with"). + * + * @since 3.1 + */ + public static final String USE_DEFAULT_PREFIX = "\u0000"; + // // // Annotations for explicitly specifying deserialize/builder /** @@ -88,6 +97,22 @@ public Class keyUsing() */ public Class builder() default Void.class; + /** + * Optional property for specifying the prefix used for builder + * "with" methods when using {@link #builder()}. When set to a non-default value + * (something other than {@link #USE_DEFAULT_PREFIX}), this overrides any + * {@link JsonPOJOBuilder#withPrefix()} annotation on the builder class itself. + * Can be set to empty string ({@code ""}) for no prefix (common with Lombok-generated + * builders). + *

+ * Defaults to {@link #USE_DEFAULT_PREFIX} which means the prefix is determined + * by checking {@link JsonPOJOBuilder} annotation on the builder class, or using + * the global default ("with"). + * + * @since 3.1 + */ + public String builderPrefix() default USE_DEFAULT_PREFIX; + // // // Annotations for specifying intermediate Converters (2.2+) /** @@ -97,8 +122,6 @@ public Class keyUsing() * for two-step deserialization; Jackson binds data into suitable intermediate * type (like Tree representation), and converter then builds actual property * type. - * - * @since 2.2 */ @SuppressWarnings("rawtypes") // to work around JDK8 bug wrt Class-valued annotation properties public Class converter() default Converter.None.class; @@ -106,8 +129,6 @@ public Class keyUsing() /** * Similar to {@link #converter}, but used for values of structures types * (List, arrays, Maps). - * - * @since 2.2 */ @SuppressWarnings("rawtypes") // to work around JDK8 bug wrt Class-valued annotation properties public Class contentConverter() default Converter.None.class; diff --git a/src/main/java/tools/jackson/databind/introspect/AnnotationIntrospectorPair.java b/src/main/java/tools/jackson/databind/introspect/AnnotationIntrospectorPair.java index efbeceee25..b2cd89b8c4 100644 --- a/src/main/java/tools/jackson/databind/introspect/AnnotationIntrospectorPair.java +++ b/src/main/java/tools/jackson/databind/introspect/AnnotationIntrospectorPair.java @@ -635,6 +635,12 @@ public JsonPOJOBuilder.Value findPOJOBuilderConfig(MapperConfig config, Annot return (result == null) ? _secondary.findPOJOBuilderConfig(config, ac) : result; } + @Override + public String findBuilderPrefix(MapperConfig config, AnnotatedClass valueClass) { + String result = _primary.findBuilderPrefix(config, valueClass); + return (result == null) ? _secondary.findBuilderPrefix(config, valueClass) : result; + } + @Override public JsonCreator.Mode findCreatorAnnotation(MapperConfig config, Annotated a) { JsonCreator.Mode mode = _primary.findCreatorAnnotation(config, a); diff --git a/src/main/java/tools/jackson/databind/introspect/DefaultAccessorNamingStrategy.java b/src/main/java/tools/jackson/databind/introspect/DefaultAccessorNamingStrategy.java index 2f4d369e44..ecaa2a6a4b 100644 --- a/src/main/java/tools/jackson/databind/introspect/DefaultAccessorNamingStrategy.java +++ b/src/main/java/tools/jackson/databind/introspect/DefaultAccessorNamingStrategy.java @@ -422,8 +422,26 @@ public AccessorNamingStrategy forBuilder(MapperConfig config, { AnnotationIntrospector ai = config.isAnnotationProcessingEnabled() ? config.getAnnotationIntrospector() : null; - JsonPOJOBuilder.Value builderConfig = (ai == null) ? null : ai.findPOJOBuilderConfig(config, builderClass); - String mutatorPrefix = (builderConfig == null) ? _withPrefix : builderConfig.withPrefix; + String mutatorPrefix = _withPrefix; + + if (ai != null) { + // [databind#2624] First check @JsonDeserialize.builderPrefix on value class + if (valueTypeDesc != null) { + String prefix = ai.findBuilderPrefix(config, valueTypeDesc.getClassInfo()); + if (prefix != null) { + return new DefaultAccessorNamingStrategy(config, builderClass, + prefix, _getterPrefix, _isGetterPrefix, + _baseNameValidator); + } + } + + // Existing: check @JsonPOJOBuilder on builder class + JsonPOJOBuilder.Value builderConfig = ai.findPOJOBuilderConfig(config, builderClass); + if (builderConfig != null) { + mutatorPrefix = builderConfig.withPrefix; + } + } + return new DefaultAccessorNamingStrategy(config, builderClass, mutatorPrefix, _getterPrefix, _isGetterPrefix, _baseNameValidator); diff --git a/src/main/java/tools/jackson/databind/introspect/JacksonAnnotationIntrospector.java b/src/main/java/tools/jackson/databind/introspect/JacksonAnnotationIntrospector.java index e3e79c0586..38a5437441 100644 --- a/src/main/java/tools/jackson/databind/introspect/JacksonAnnotationIntrospector.java +++ b/src/main/java/tools/jackson/databind/introspect/JacksonAnnotationIntrospector.java @@ -1351,6 +1351,19 @@ public JsonPOJOBuilder.Value findPOJOBuilderConfig(MapperConfig config, Annot return (ann == null) ? null : new JsonPOJOBuilder.Value(ann); } + @Override + public String findBuilderPrefix(MapperConfig config, AnnotatedClass valueClass) + { + JsonDeserialize ann = _findAnnotation(valueClass, JsonDeserialize.class); + if (ann != null) { + String prefix = ann.builderPrefix(); + if (!JsonDeserialize.USE_DEFAULT_PREFIX.equals(prefix)) { + return prefix; + } + } + return null; + } + /* /********************************************************************** /* Deserialization: property annotations diff --git a/src/test/java/tools/jackson/databind/introspect/AccessorNamingForBuilderTest.java b/src/test/java/tools/jackson/databind/introspect/AccessorNamingForBuilderTest.java index a3c0577937..1f3fe7e440 100644 --- a/src/test/java/tools/jackson/databind/introspect/AccessorNamingForBuilderTest.java +++ b/src/test/java/tools/jackson/databind/introspect/AccessorNamingForBuilderTest.java @@ -4,6 +4,7 @@ import tools.jackson.databind.*; import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.annotation.JsonPOJOBuilder; import tools.jackson.databind.exc.UnrecognizedPropertyException; import tools.jackson.databind.testutil.DatabindTestUtil; @@ -43,6 +44,124 @@ public ValueClassXY build() { } } + // [databind#2624]: Test with builderPrefix="" on @JsonDeserialize (Lombok-style) + @JsonDeserialize(builder=NoPrefixBuilderViaAnnotation.NoPrefixBuilder.class, builderPrefix="") + static class NoPrefixBuilderViaAnnotation + { + final int a, b; + + protected NoPrefixBuilderViaAnnotation(int a, int b) { + this.a = a; + this.b = b; + } + + static class NoPrefixBuilder + { + protected int a, b; + + public NoPrefixBuilder a(int a0) { + this.a = a0; + return this; + } + + public NoPrefixBuilder b(int b0) { + this.b = b0; + return this; + } + + public NoPrefixBuilderViaAnnotation build() { + return new NoPrefixBuilderViaAnnotation(a, b); + } + } + } + + // [databind#2624]: Test with custom builderPrefix ("set") on @JsonDeserialize + @JsonDeserialize(builder=SetPrefixBuilderViaAnnotation.SetPrefixBuilder.class, builderPrefix="set") + static class SetPrefixBuilderViaAnnotation + { + final String name; + final int value; + + protected SetPrefixBuilderViaAnnotation(String name, int value) { + this.name = name; + this.value = value; + } + + static class SetPrefixBuilder + { + protected String name; + protected int value; + + public SetPrefixBuilder setName(String n) { + this.name = n; + return this; + } + + public SetPrefixBuilder setValue(int v) { + this.value = v; + return this; + } + + public SetPrefixBuilderViaAnnotation build() { + return new SetPrefixBuilderViaAnnotation(name, value); + } + } + } + + // [databind#2624]: Test that @JsonDeserialize.builderPrefix overrides @JsonPOJOBuilder.withPrefix + @JsonDeserialize(builder=AnnotationOverrideTest.OverriddenBuilder.class, builderPrefix="") + static class AnnotationOverrideTest + { + final int x; + + protected AnnotationOverrideTest(int x) { + this.x = x; + } + + // Builder has @JsonPOJOBuilder(withPrefix="with"), but @JsonDeserialize(builderPrefix="") should win + @JsonPOJOBuilder(withPrefix="with") + static class OverriddenBuilder + { + protected int x; + + // Using no-prefix method name, not "withX" + public OverriddenBuilder x(int x0) { + this.x = x0; + return this; + } + + public AnnotationOverrideTest build() { + return new AnnotationOverrideTest(x); + } + } + } + + // [databind#2624]: Test that @JsonPOJOBuilder.withPrefix still works when builderPrefix not specified + @JsonDeserialize(builder=FallbackToPojoBuilderTest.PojoBuilder.class) + static class FallbackToPojoBuilderTest + { + final int y; + + protected FallbackToPojoBuilderTest(int y) { + this.y = y; + } + + @JsonPOJOBuilder(withPrefix="set") + static class PojoBuilder + { + protected int y; + + public PojoBuilder setY(int y0) { + this.y = y0; + return this; + } + + public FallbackToPojoBuilderTest build() { + return new FallbackToPojoBuilderTest(y); + } + } + } + // For [databind#2624] @Test public void testAccessorCustomWithMethod() throws Exception @@ -70,4 +189,46 @@ public void testAccessorCustomWithMethod() throws Exception assertEquals(29, xy._x); assertEquals(73, xy._y); } + + // [databind#2624]: Test @JsonDeserialize.builderPrefix with empty string (Lombok-style) + @Test + public void testBuilderPrefixEmptyViaAnnotation() throws Exception + { + final ObjectMapper mapper = newJsonMapper(); + final String json = a2q("{'a':10,'b':20}"); + NoPrefixBuilderViaAnnotation result = mapper.readValue(json, NoPrefixBuilderViaAnnotation.class); + assertEquals(10, result.a); + assertEquals(20, result.b); + } + + // [databind#2624]: Test @JsonDeserialize.builderPrefix with custom prefix ("set") + @Test + public void testBuilderPrefixCustomViaAnnotation() throws Exception + { + final ObjectMapper mapper = newJsonMapper(); + final String json = a2q("{'name':'test','value':42}"); + SetPrefixBuilderViaAnnotation result = mapper.readValue(json, SetPrefixBuilderViaAnnotation.class); + assertEquals("test", result.name); + assertEquals(42, result.value); + } + + // [databind#2624]: Test that @JsonDeserialize.builderPrefix overrides @JsonPOJOBuilder.withPrefix + @Test + public void testBuilderPrefixOverridesJsonPOJOBuilder() throws Exception + { + final ObjectMapper mapper = newJsonMapper(); + final String json = a2q("{'x':99}"); + AnnotationOverrideTest result = mapper.readValue(json, AnnotationOverrideTest.class); + assertEquals(99, result.x); + } + + // [databind#2624]: Test fallback to @JsonPOJOBuilder when builderPrefix not specified + @Test + public void testFallbackToJsonPOJOBuilderPrefix() throws Exception + { + final ObjectMapper mapper = newJsonMapper(); + final String json = a2q("{'y':55}"); + FallbackToPojoBuilderTest result = mapper.readValue(json, FallbackToPojoBuilderTest.class); + assertEquals(55, result.y); + } }