POJOPropertiesCollectorTest.java
package tools.jackson.databind.introspect;
import java.beans.ConstructorProperties;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.math.BigDecimal;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.annotation.*;
import tools.jackson.databind.*;
import tools.jackson.databind.annotation.JsonDeserialize;
import tools.jackson.databind.cfg.MapperConfig;
import tools.jackson.databind.testutil.DatabindTestUtil;
import static org.junit.jupiter.api.Assertions.*;
public class POJOPropertiesCollectorTest
extends DatabindTestUtil
{
static class Simple {
public int value;
@JsonProperty("value")
public void valueSetter(int v) { value = v; }
@JsonProperty("value")
public int getFoobar() { return value; }
}
static class SimpleFieldDeser
{
@JsonDeserialize String[] values;
}
static class SimpleGetterVisibility {
public int getA() { return 0; }
protected int getB() { return 1; }
@SuppressWarnings("unused")
private int getC() { return 2; }
}
// Class for testing 'shared ignore'
static class Empty {
public int value;
public void setValue(int v) { value = v; }
@JsonIgnore
public int getValue() { return value; }
}
static class IgnoredSetter {
@JsonProperty
public int value;
@JsonIgnore
public void setValue(int v) { value = v; }
public int getValue() { return value; }
}
static class ImplicitIgnores {
@JsonIgnore public int a;
@JsonIgnore public void setB(int b) { }
public int c;
}
// Should find just one setter for "y", due to partial ignore
static class IgnoredRenamedSetter {
@JsonIgnore public void setY(int value) { }
@JsonProperty("y") void foobar(int value) { }
}
// should produce a single property, "x"
static class RenamedProperties {
@JsonProperty("x")
public int value;
public void setValue(int v) { value = v; }
public int getX() { return value; }
}
static class RenamedProperties2
{
@JsonProperty("renamed")
public int getValue() { return 1; }
public void setValue(int x) { }
}
// Testing that we can "merge" properties with renaming
static class MergedProperties {
public int x;
@JsonProperty("x")
public void setFoobar(int v) { x = v; }
}
// Testing that property order is obeyed, even for deserialization purposes
@JsonPropertyOrder({"a", "b", "c", "d"})
static class SortedProperties
{
public int b;
public int c;
public void setD(int value) { }
public void setA(int value) { }
}
// [JACKSON-700]: test property type detection, selection
static class TypeTestBean
{
protected Long value;
@JsonCreator
public TypeTestBean(@JsonProperty("value") String value) { }
// If you remove this method, the test will pass
public Integer getValue() { return 0; }
}
static class Jackson703
{
private List<FoodOrgLocation> location = new ArrayList<FoodOrgLocation>();
{
location.add(new FoodOrgLocation());
}
public List<FoodOrgLocation> getLocation() { return location; }
}
static class FoodOrgLocation
{
protected Long id;
public String name;
protected Location location;
public FoodOrgLocation() {
location = new Location();
}
public FoodOrgLocation(final Location foodOrg) { }
public FoodOrgLocation(final Long id, final String name, final Location location) { }
public Location getLocation() { return location; }
}
static class Location {
public BigDecimal lattitude;
public BigDecimal longitude;
public Location() { }
public Location(final BigDecimal lattitude, final BigDecimal longitude) { }
}
class Issue701Bean { // important: non-static!
private int i;
// annotation does not matter -- just need one on the last argument
public Issue701Bean(@JsonProperty int i) { this.i = i; }
public int getX() { return i; }
}
static class Issue744Bean
{
protected Map<String,Object> additionalProperties;
@JsonAnySetter
public void addAdditionalProperty(String key, Object value) {
if (additionalProperties == null) additionalProperties = new HashMap<String, Object>();
additionalProperties.put(key,value);
}
public void setAdditionalProperties(Map<String, Object> additionalProperties) {
this.additionalProperties = additionalProperties;
}
@JsonAnyGetter
public Map<String,Object> getAdditionalProperties() { return additionalProperties; }
@JsonIgnore
public String getName() {
return (String) additionalProperties.get("name");
}
}
static class PropDescBean
{
public final static String A_DESC = "That's A!";
public final static int B_INDEX = 3;
@JsonPropertyDescription(A_DESC)
public String a;
protected int b;
public String getA() { return a; }
public void setA(String a) { this.a = a; }
@JsonProperty(required=true, index=B_INDEX, defaultValue="13")
public int getB() { return b; }
}
@Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotation
@interface A {}
@Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotation
@interface B {}
static class DuplicateGetterBean
{
@A
public boolean isBloop() { return true; }
@B
public boolean getBloop() { return true; }
}
static class DuplicateGetterCreatorBean
{
public DuplicateGetterCreatorBean(@JsonProperty("bloop") @A boolean bloop) {}
public boolean isBloop() { return true; }
public boolean getBloop() { return true; }
}
// [databind#3591]: test that getIgnoredPropertyNames() includes
// both @JsonIgnore per-field and @JsonIgnoreProperties class-level
@JsonIgnoreProperties("second")
static class IgnoredMixed {
@JsonIgnore
public String first;
public String second;
public String third;
}
/*
/**********************************************************
/* Unit tests
/**********************************************************
*/
private final ObjectMapper MAPPER = newJsonMapper();
@Test
public void testSimple()
{
POJOPropertiesCollector coll = collector(MAPPER, Simple.class, false);
Map<String, POJOPropertyBuilder> props = coll.getPropertyMap();
assertEquals(1, props.size());
POJOPropertyBuilder prop = props.get("value");
assertNotNull(prop);
assertTrue(prop.hasSetter());
assertTrue(prop.hasGetter());
assertTrue(prop.hasField());
}
@Test
public void testSimpleFieldVisibility()
{
// false -> deserialization
POJOPropertiesCollector coll = collector(MAPPER, SimpleFieldDeser.class, false);
Map<String, POJOPropertyBuilder> props = coll.getPropertyMap();
assertEquals(1, props.size());
POJOPropertyBuilder prop = props.get("values");
assertNotNull(prop);
assertFalse(prop.hasSetter());
assertFalse(prop.hasGetter());
assertTrue(prop.hasField());
}
@Test
public void testSimpleGetterVisibility()
{
POJOPropertiesCollector coll = collector(MAPPER,
SimpleGetterVisibility.class, true);
Map<String, POJOPropertyBuilder> props = coll.getPropertyMap();
assertEquals(1, props.size());
POJOPropertyBuilder prop = props.get("a");
assertNotNull(prop);
assertFalse(prop.hasSetter());
assertTrue(prop.hasGetter());
assertFalse(prop.hasField());
}
// Unit test for verifying that a single @JsonIgnore can remove the
// whole property, unless explicit property marker exists
@Test
public void testEmpty()
{
POJOPropertiesCollector coll = collector(MAPPER,
Empty.class, true);
Map<String, POJOPropertyBuilder> props = coll.getPropertyMap();
assertEquals(0, props.size());
}
// Unit test for verifying handling of 'partial' @JsonIgnore; that is,
// if there is at least one explicit annotation to indicate property,
// only parts that are ignored are, well, ignored
@Test
public void testPartialIgnore()
{
POJOPropertiesCollector coll = collector(MAPPER,
IgnoredSetter.class, true);
Map<String, POJOPropertyBuilder> props = coll.getPropertyMap();
assertEquals(1, props.size());
POJOPropertyBuilder prop = props.get("value");
assertNotNull(prop);
assertFalse(prop.hasSetter());
assertTrue(prop.hasGetter());
assertTrue(prop.hasField());
}
@Test
public void testSimpleRenamed()
{
POJOPropertiesCollector coll = collector(MAPPER,
RenamedProperties.class, true);
Map<String, POJOPropertyBuilder> props = coll.getPropertyMap();
assertEquals(1, props.size());
POJOPropertyBuilder prop = props.get("x");
assertNotNull(prop);
assertTrue(prop.hasSetter());
assertTrue(prop.hasGetter());
assertTrue(prop.hasField());
}
@Test
public void testSimpleRenamed2()
{
POJOPropertiesCollector coll = collector(MAPPER,
RenamedProperties2.class, true);
Map<String, POJOPropertyBuilder> props = coll.getPropertyMap();
assertEquals(1, props.size());
POJOPropertyBuilder prop = props.get("renamed");
assertNotNull(prop);
assertTrue(prop.hasSetter());
assertTrue(prop.hasGetter());
assertFalse(prop.hasField());
}
@Test
public void testMergeWithRename()
{
POJOPropertiesCollector coll = collector(MAPPER,
MergedProperties.class, true);
Map<String, POJOPropertyBuilder> props = coll.getPropertyMap();
assertEquals(1, props.size());
POJOPropertyBuilder prop = props.get("x");
assertNotNull(prop);
assertTrue(prop.hasSetter());
assertFalse(prop.hasGetter());
assertTrue(prop.hasField());
}
@Test
public void testSimpleIgnoreAndRename()
{
POJOPropertiesCollector coll = collector(MAPPER,
IgnoredRenamedSetter.class, true);
Map<String, POJOPropertyBuilder> props = coll.getPropertyMap();
assertEquals(1, props.size());
POJOPropertyBuilder prop = props.get("y");
assertNotNull(prop);
assertTrue(prop.hasSetter());
assertFalse(prop.hasGetter());
assertFalse(prop.hasField());
}
@Test
public void testGlobalVisibilityForGetters()
{
ObjectMapper m = jsonMapperBuilder()
.changeDefaultVisibility(vc ->
vc.withVisibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE))
.build();
POJOPropertiesCollector coll = collector(m, SimpleGetterVisibility.class, true);
// should be 1, expect that we disabled getter auto-detection, so
Map<String, POJOPropertyBuilder> props = coll.getPropertyMap();
assertEquals(0, props.size());
}
@Test
public void testCollectionOfIgnored()
{
// should be 1, due to ignorals
Map<String, ?> props = beanPropMap(MAPPER, ImplicitIgnores.class, false);
assertEquals(1, props.size());
// but also have 2 ignored properties
POJOPropertiesCollector coll = collector(MAPPER, ImplicitIgnores.class, false);
Collection<String> ign = coll.getIgnoredPropertyNames();
assertEquals(2, ign.size());
assertTrue(ign.contains("a"));
assertTrue(ign.contains("b"));
}
@Test
public void testSimpleOrderingForDeserialization()
{
List<BeanPropertyDefinition> props = beanPropList(MAPPER, SortedProperties.class, false);
assertEquals(4, props.size());
assertEquals("a", props.get(0).getName());
assertEquals("b", props.get(1).getName());
assertEquals("c", props.get(2).getName());
assertEquals("d", props.get(3).getName());
}
@Test
public void testSimpleWithType()
{
// first for serialization; should base choice on getter
List<BeanPropertyDefinition> props = beanPropList(MAPPER, TypeTestBean.class, true);
assertEquals(1, props.size());
assertEquals("value", props.get(0).getName());
AnnotatedMember m = props.get(0).getAccessor();
assertInstanceOf(AnnotatedMethod.class, m);
assertEquals(Integer.class, m.getRawType());
// then for deserialization; prefer ctor param
props = beanPropList(MAPPER, TypeTestBean.class, false);
assertEquals(1, props.size());
assertEquals("value", props.get(0).getName());
m = props.get(0).getMutator();
assertEquals(AnnotatedParameter.class, m.getClass());
assertEquals(String.class, m.getRawType());
}
@Test
public void testDuplicateGetters() throws Exception
{
List<BeanPropertyDefinition> props = beanPropList(MAPPER, DuplicateGetterBean.class, true);
assertEquals(1, props.size());
BeanPropertyDefinition prop = props.get(0);
assertEquals("bloop", prop.getName());
assertTrue(prop.getGetter().hasAnnotation(A.class));
assertTrue(prop.getGetter().hasAnnotation(B.class));
}
// 27-Nov-2019, tatu: Not sure why, but changes for [databind#2555] started to
// fail this test, due to call to `prop.getMetadata()` (to check for `index`).
// Probably related to comment on "Can't call getGetter..."
@Test
public void testDuplicateGettersCreator() throws Exception
{
List<BeanPropertyDefinition> props = beanPropList(MAPPER, DuplicateGetterCreatorBean.class, true);
assertEquals(1, props.size());
POJOPropertyBuilder prop = (POJOPropertyBuilder) props.get(0);
assertEquals("bloop", prop.getName());
// Can't call getGetter or the duplicate will be removed
assertTrue(prop._getters.value.hasAnnotation(A.class));
assertNotNull(prop._getters.next);
assertTrue(prop._getters.next.value.hasAnnotation(A.class));
}
// [databind#3591]: getIgnoredPropertyNames() should include both
// @JsonIgnore per-field and @JsonIgnoreProperties class-level
@Test
public void testIgnoredPropertyNamesIncludesClassLevel()
{
// Deserialization
BeanDescription descDeser = beanDesc(MAPPER, IgnoredMixed.class, false);
Set<String> ignoredDeser = descDeser.getIgnoredPropertyNames();
assertTrue(ignoredDeser.contains("first"), "per-field @JsonIgnore should be in ignored (deser)");
assertTrue(ignoredDeser.contains("second"), "class-level @JsonIgnoreProperties should be in ignored (deser)");
assertEquals(2, ignoredDeser.size());
// Serialization: should also report ignored names
BeanDescription descSer = beanDesc(MAPPER, IgnoredMixed.class, true);
Set<String> ignoredSer = descSer.getIgnoredPropertyNames();
assertTrue(ignoredSer.contains("first"), "per-field @JsonIgnore should be in ignored (ser)");
assertTrue(ignoredSer.contains("second"), "class-level @JsonIgnoreProperties should be in ignored (ser)");
assertEquals(2, ignoredSer.size());
}
// [databind#5952]: with class-level @JsonIgnoreProperties on a creator-based POJO,
// getIgnoredPropertyNames() must report the class-level name even when it
// collides with a creator parameter (where the [databind#2001] rescue path
// would previously have stripped it).
@JsonIgnoreProperties("name")
static class ClassIgnoredCreator5952 {
final int id;
final String name;
@JsonCreator
public ClassIgnoredCreator5952(@JsonProperty("id") int id,
@JsonProperty("name") String name) {
this.id = id;
this.name = name;
}
}
@Test
public void testClassLevelIgnoredSurvivesCreatorRescue5952()
{
BeanDescription desc = beanDesc(MAPPER, ClassIgnoredCreator5952.class, false);
Set<String> ignored = desc.getIgnoredPropertyNames();
assertTrue(ignored.contains("name"),
"class-level @JsonIgnoreProperties name must survive the creator-rename rescue: " + ignored);
assertEquals(1, ignored.size());
}
// [databind#5952]: getNonRescuedIgnoredPropertyNames() exposes the un-rescued
// per-property view so tooling can still see names that were "rescued" by a
// creator parameter renamed to the same name.
static class PerPropertyIgnoredCreator5952 {
@JsonIgnore
public String query;
@JsonCreator
public PerPropertyIgnoredCreator5952(@JsonProperty("query") String rawQuery) {
this.query = rawQuery;
}
}
@Test
public void testNonRescuedIgnoredPropertyNames5952()
{
BeanDescription desc = beanDesc(MAPPER, PerPropertyIgnoredCreator5952.class, false);
// Rescued view: "query" is overridden by the creator and should not appear.
Set<String> rescued = desc.getIgnoredPropertyNames();
assertFalse(rescued.contains("query"),
"per-property @JsonIgnore should be rescued by creator-rename: " + rescued);
// Un-rescued view: original ignoral declaration is still visible.
Set<String> unrescued = desc.getNonRescuedIgnoredPropertyNames();
assertTrue(unrescued.contains("query"),
"getNonRescuedIgnoredPropertyNames() should report the original per-property ignoral: " + unrescued);
}
// [databind#5952]: when multiple per-property @JsonIgnore names are rescued by
// creator parameters in the same type, the snapshot taken on the FIRST rescue
// must capture both names ��� not just the one that triggered the snapshot. This
// exercises the once-only-take semantics in _renameProperties' strip-out site.
//
// Pattern follows [databind#2001]: the creator parameters' implicit names
// (from @ConstructorProperties) differ from the field names, and @JsonProperty
// renames each parameter to match the corresponding ignored field name ��� that
// is what causes _replaceCreatorProperty to fire and the strip-out to run.
static class MultiRescueCreator5952 {
@JsonIgnore
public String alpha;
@JsonIgnore
public String beta;
@JsonCreator
@ConstructorProperties({"rawAlpha", "rawBeta"})
public MultiRescueCreator5952(
@JsonProperty("alpha") String alpha,
@JsonProperty("beta") String beta) {
this.alpha = alpha;
this.beta = beta;
}
}
@Test
public void testMultiRescueSnapshotCapturesAllNames5952()
{
BeanDescription desc = beanDesc(MAPPER, MultiRescueCreator5952.class, false);
// Rescued view: both per-property @JsonIgnore names are overridden by creator
// parameters and should be absent.
Set<String> rescued = desc.getIgnoredPropertyNames();
assertFalse(rescued.contains("alpha"),
"alpha should be rescued by creator-rename: " + rescued);
assertFalse(rescued.contains("beta"),
"beta should be rescued by creator-rename: " + rescued);
// Un-rescued view: snapshot taken on first rescue must contain BOTH names,
// proving the snapshot captures pre-rescue state rather than just the
// currently-being-rescued name.
Set<String> unrescued = desc.getNonRescuedIgnoredPropertyNames();
assertTrue(unrescued.contains("alpha"),
"snapshot should contain alpha: " + unrescued);
assertTrue(unrescued.contains("beta"),
"snapshot should contain beta: " + unrescued);
}
// [databind#6031]: a creator parameter's @JsonAlias name that collides with an
// unrelated @JsonIgnore accessor must be rescued from the per-property ignore set
// (so it remains a usable deser input), while still being reported by the
// un-rescued view ��� exactly as for the creator-rename rescue above.
static class AliasIgnoredCreator6031 {
@JsonProperty("newName")
private final String value;
@JsonCreator
public AliasIgnoredCreator6031(@JsonProperty("newName") @JsonAlias("oldName") String value) {
this.value = value;
}
public String getValue() { return value; }
@JsonIgnore
public String getOldName() { return value; }
}
@Test
public void testNonRescuedAliasIgnoredPropertyNames6031()
{
BeanDescription desc = beanDesc(MAPPER, AliasIgnoredCreator6031.class, false);
// Rescued view: "oldName" is a live creator alias and must not appear.
Set<String> rescued = desc.getIgnoredPropertyNames();
assertFalse(rescued.contains("oldName"),
"creator @JsonAlias should be rescued from per-property ignorals: " + rescued);
// Un-rescued view: the original @JsonIgnore-getter ignoral is still visible.
Set<String> unrescued = desc.getNonRescuedIgnoredPropertyNames();
assertTrue(unrescued.contains("oldName"),
"getNonRescuedIgnoredPropertyNames() should report the pre-rescue alias ignoral: " + unrescued);
}
@SuppressWarnings("deprecation")
@Test
public void testFormatOverridesDeprecated()
{
POJOPropertiesCollector coll = collector(MAPPER, Simple.class, false);
JsonFormat.Value format = coll.getFormatOverrides();
assertNotNull(format);
assertEquals(JsonFormat.Shape.ANY, format.getShape());
}
/*
/**********************************************************************
/* Helper methods
/**********************************************************************
*/
protected Map<String, BeanPropertyDefinition> beanPropMap(ObjectMapper m0,
Class<?> cls, boolean forSerialization) {
return beanPropList(m0, cls, forSerialization).stream()
.collect(Collectors.toMap(BeanPropertyDefinition::getName,
Function.identity()));
}
protected List<BeanPropertyDefinition> beanPropList(ObjectMapper m0,
Class<?> cls, boolean forSerialization) {
return beanDesc(m0, cls, forSerialization).findProperties();
}
protected BeanDescription beanDesc(ObjectMapper m0,
Class<?> cls, boolean forSerialization)
{
return forSerialization
? ObjectMapperTestAccess.beanDescriptionForSer(m0, cls)
: ObjectMapperTestAccess.beanDescriptionForDeser(m0, cls)
;
}
protected POJOPropertiesCollector collector(ObjectMapper mapper,
Class<?> cls, boolean forSerialization)
{
MapperConfig<?> config = forSerialization
? mapper.serializationConfig()
: mapper.deserializationConfig();
AnnotatedClass classDef = forSerialization
? ObjectMapperTestAccess.annotatedClassForSer(mapper, cls)
: ObjectMapperTestAccess.annotatedClassForDeser(mapper, cls);
return new TestPOJOPropertiesCollector(config,
forSerialization, mapper.constructType(cls),
classDef,
config.getAccessorNaming());
}
// @since 3.2
static class TestPOJOPropertiesCollector extends POJOPropertiesCollector
{
public TestPOJOPropertiesCollector(MapperConfig<?> config, boolean forSerialization,
JavaType type, AnnotatedClass classDef,
AccessorNamingStrategy.Provider accessorNaming) {
super(config, forSerialization, type, classDef,
accessorNaming.forPOJO(config, classDef));
}
}
}