Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/main/java/tools/jackson/databind/AnnotationIntrospector.java
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand Down Expand Up @@ -88,6 +97,22 @@ public Class<? extends KeyDeserializer> 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).
*<p>
* 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+)

/**
Expand All @@ -97,17 +122,13 @@ public Class<? extends KeyDeserializer> 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<? extends Converter> converter() default Converter.None.class;

/**
* 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<? extends Converter> contentConverter() default Converter.None.class;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
}