ObjectIdDeserializationTest.java
package tools.jackson.databind.objectid;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.annotation.ObjectIdGenerator.IdKey;
import tools.jackson.core.JsonParser;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.*;
import tools.jackson.databind.cfg.ContextAttributes;
import tools.jackson.databind.deser.UnresolvedForwardReference;
import tools.jackson.databind.deser.UnresolvedId;
import tools.jackson.databind.testutil.DatabindTestUtil;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests to verify handling of Object Id deserialization.
*/
public class ObjectIdDeserializationTest extends DatabindTestUtil
{
private static final String POOL_KEY = "POOL";
// // // Classes for external id use
@JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class, property="id")
static class Identifiable
{
public int value;
public Identifiable next;
public Identifiable() { this(0); }
public Identifiable(int v) { value = v; }
}
@JsonIdentityInfo(generator=ObjectIdGenerators.UUIDGenerator.class, property="#")
static class UUIDNode
{
public int value;
public UUIDNode parent;
public UUIDNode first;
public UUIDNode second;
public UUIDNode() { this(0); }
public UUIDNode(int v) { value = v; }
}
// // // Classes for external id from property annotations:
static class IdWrapper
{
@JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class, property="@id")
public ValueNode node;
public IdWrapper() { }
public IdWrapper(int v) { node = new ValueNode(v); }
}
static class ValueNode {
public int value;
public IdWrapper next;
public ValueNode() { this(0); }
public ValueNode(int v) { value = v; }
}
// // // Classes for custom property id use
@JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="customId")
static class IdentifiableCustom
{
public int value;
public int customId;
public IdentifiableCustom next;
public IdentifiableCustom() { this(-1, 0); }
protected IdentifiableCustom(int i, int v) {
customId = i;
value = v;
}
}
static class IdWrapperExt
{
@JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="customId")
public ValueNodeExt node;
public IdWrapperExt() { }
public IdWrapperExt(int v) { node = new ValueNodeExt(v); }
}
static class ValueNodeExt
{
public int value;
protected int customId;
public IdWrapperExt next;
public ValueNodeExt() { this(0); }
public ValueNodeExt(int v) { value = v; }
public void setCustomId(int i) { customId = i; }
}
static class MappedCompany {
public Map<Integer, Employee> employees;
}
@JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class)
static class AnySetterObjectId {
protected Map<String, AnySetterObjectId> values = new HashMap<>();
@JsonAnySetter
public void anySet(String field, AnySetterObjectId value) {
// Ensure that it is never called with null because of unresolved reference.
assertNotNull(value);
values.put(field, value);
}
}
// // // Classes for custom id resolver
static class CustomResolutionWrapper {
public List<WithCustomResolution> data;
}
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id", resolver = PoolResolver.class)
@JsonIdentityReference(alwaysAsId = true) // #524
static class WithCustomResolution {
public int id;
public int data;
public WithCustomResolution(int id, int data) {
this.id = id;
this.data = data;
}
}
public static class PoolResolver implements ObjectIdResolver {
private Map<Object,WithCustomResolution> _pool;
public PoolResolver() {}
public PoolResolver(Map<Object,WithCustomResolution> pool) { _pool = pool; }
@Override
public void bindItem(IdKey id, Object pojo) { }
@Override
public Object resolveId(IdKey id) { return _pool.get(id.key); }
@Override
public boolean canUseFor(ObjectIdResolver resolverType) {
return resolverType.getClass() == getClass() && _pool != null && !_pool.isEmpty();
}
@Override
public ObjectIdResolver newForDeserialization(Object c) {
DeserializationContext context = (DeserializationContext) c;
@SuppressWarnings("unchecked")
Map<Object,WithCustomResolution> pool =
(Map<Object,WithCustomResolution>) context.getAttribute(POOL_KEY);
return new PoolResolver(pool);
}
}
// // // Classes for FAIL_ON_UNRESOLVED_OBJECT_IDS tests
static class SomeWrapper {
@JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class, property = "@id")
public SomeNode node;
public SomeWrapper() {}
public SomeWrapper(int v) { node = new SomeNode(v); }
}
static class SomeNode {
public int value;
public SomeWrapper next;
public SomeNode() { this(0); }
public SomeNode(int v) { value = v; }
}
// // // Company/Employee for forward-reference tests
static class Company {
public List<Employee> employees;
public void add(Employee e) {
if (employees == null) {
employees = new ArrayList<>();
}
employees.add(e);
}
}
// // // For testNullStringPropertyId [databind#1150]
@JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="id")
static class IdentifiableStringId
{
public String id;
public int value;
public Identifiable next;
public IdentifiableStringId() { this(0); }
public IdentifiableStringId(int v) { value = v; }
}
// // // For testAtomicReferenceWithObjectId [from ReferentialWithObjectIdTest]
public static class LinkedEmployeeList {
public AtomicReference<LinkedEmployee> first;
}
@JsonIdentityInfo(property="id", generator=ObjectIdGenerators.PropertyGenerator.class)
public static class LinkedEmployee {
public int id;
public String name;
public AtomicReference<LinkedEmployee> next;
public LinkedEmployee next(LinkedEmployee n) {
next = new AtomicReference<>(n);
return this;
}
}
// // // For testObjectIdWithInjectables [from ObjectIdWithInjectables538Test]
@JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class)
public static class InjectableA {
public InjectableB b;
public InjectableA(@JacksonInject("i1") String injected) { }
}
@JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class)
public static class InjectableB {
public InjectableA a;
@JsonCreator
public InjectableB(@JacksonInject("i2") String injected) { }
}
// // // For testForwardReferenceInEnumMap
static class EnumMapCompany {
public EnumMap<FooEnum, Employee> employees;
}
static enum FooEnum {
A, B, C
}
// // // For testOrdering1388 [databind#1388]
@JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class)
public static class NamedThing {
private final UUID id;
private final String name;
@JsonCreator
public NamedThing(@JsonProperty("id") UUID id, @JsonProperty("name") String name) {
this.id = id;
this.name = name;
}
public UUID getId() { return id; }
public String getName() { return name; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
NamedThing that = (NamedThing) o;
return that.id.equals(id) && that.name.equals(name);
}
@Override
public int hashCode() { return name.hashCode(); }
}
// // // For [databind#2955]: unresolved scalar Object Id with FAIL_ON_UNRESOLVED_OBJECT_IDS disabled
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
static class Node2955 {
public int id;
public String name;
public Node2955 ref;
}
// // // For [databind#2955] / jackson-jaxrs-providers#189: mixed forward, invalid,
// missing and inline references with FAIL_ON_UNRESOLVED_OBJECT_IDS disabled
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,
property = "name", scope = Value2955.class)
static class Value2955 {
public String name;
public Integer value;
}
static class Owned2955 {
public String name;
public Value2955 optionalValue;
}
static class Owner2955 {
public List<Owned2955> owned = new ArrayList<>();
public List<Value2955> values = new ArrayList<>();
}
// // // For ObjectReader + FAIL_ON_UNRESOLVED_OBJECT_IDS [databind#5542]
public static class ReaderWrapper5542 {
public ReaderValueNode5542 node;
}
@JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class, property="@id")
public static class ReaderValueNode5542 {
public int value;
public ReaderWrapper5542 next;
}
// // // For testWithFieldsInBaseClass [databind#1083]
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
property = "type",
defaultImpl = JsonMapSchema.class)
@JsonSubTypes({
@JsonSubTypes.Type(value = JsonMapSchema.class, name = "map"),
@JsonSubTypes.Type(value = JsonJdbcSchema.class, name = "jdbc") })
public static abstract class JsonSchema {
public String name;
}
static class JsonMapSchema extends JsonSchema { }
static class JsonJdbcSchema extends JsonSchema { }
static class JsonRoot1083 {
public List<JsonSchema> schemas = new ArrayList<>();
}
/*
/**********************************************************
/* Unit tests, external id deserialization
/**********************************************************
*/
private final ObjectMapper MAPPER = newJsonMapper();
private final static String EXP_SIMPLE_INT_CLASS = "{\"id\":1,\"value\":13,\"next\":1}";
@Test
public void testSimpleDeserializationClass() throws Exception
{
Identifiable result = MAPPER.readValue(EXP_SIMPLE_INT_CLASS, Identifiable.class);
assertEquals(13, result.value);
assertSame(result, result.next);
}
// Should be ok NOT to have Object id, as well
@Test
public void testMissingObjectId() throws Exception
{
Identifiable result = MAPPER.readValue(a2q("{'value':28, 'next':{'value':29}}"),
Identifiable.class);
assertNotNull(result);
assertEquals(28, result.value);
assertNotNull(result.next);
assertEquals(29, result.next.value);
}
@Test
public void testSimpleUUIDForClassRoundTrip() throws Exception
{
UUIDNode root = new UUIDNode(1);
UUIDNode child1 = new UUIDNode(2);
UUIDNode child2 = new UUIDNode(3);
root.first = child1;
root.second = child2;
child1.parent = root;
child2.parent = root;
child1.first = child2;
String json = MAPPER.writeValueAsString(root);
UUIDNode result = MAPPER.readValue(json, UUIDNode.class);
assertEquals(1, result.value);
UUIDNode result2 = result.first;
UUIDNode result3 = result.second;
assertNotNull(result2);
assertNotNull(result3);
assertEquals(2, result2.value);
assertEquals(3, result3.value);
assertSame(result, result2.parent);
assertSame(result, result3.parent);
assertSame(result3, result2.first);
}
// Bit more complex, due to extra wrapping etc:
private final static String EXP_SIMPLE_INT_PROP = "{\"node\":{\"@id\":1,\"value\":7,\"next\":{\"node\":1}}}";
@Test
public void testSimpleDeserializationProperty() throws Exception
{
IdWrapper result = MAPPER.readValue(EXP_SIMPLE_INT_PROP, IdWrapper.class);
assertEquals(7, result.node.value);
assertSame(result.node, result.node.next.node);
}
// Another test to ensure ordering is not required (i.e. can do front references)
@Test
public void testSimpleDeserWithForwardRefs() throws Exception
{
IdWrapper result = MAPPER.readValue("{\"node\":{\"value\":7,\"next\":{\"node\":1}, \"@id\":1}}"
,IdWrapper.class);
assertEquals(7, result.node.value);
assertSame(result.node, result.node.next.node);
}
@Test
public void testForwardReference() throws Exception
{
String json = "{\"employees\":["
+ "{\"id\":1,\"name\":\"First\",\"manager\":2,\"reports\":[]},"
+ "{\"id\":2,\"name\":\"Second\",\"manager\":null,\"reports\":[1]}"
+ "]}";
Company company = MAPPER.readValue(json, Company.class);
assertEquals(2, company.employees.size());
Employee firstEmployee = company.employees.get(0);
Employee secondEmployee = company.employees.get(1);
assertEquals(1, firstEmployee.id);
assertEquals(2, secondEmployee.id);
assertEquals(secondEmployee, firstEmployee.manager); // Ensure forward reference was properly resolved.
assertEquals(firstEmployee, secondEmployee.reports.get(0)); // And back reference.
}
@Test
public void testForwardReferenceInCollection() throws Exception
{
String json = "{\"employees\":["
+ "{\"id\":1,\"name\":\"First\",\"manager\":null,\"reports\":[2]},"
+ "{\"id\":2,\"name\":\"Second\",\"manager\":1,\"reports\":[]}"
+ "]}";
Company company = MAPPER.readValue(json, Company.class);
assertEquals(2, company.employees.size());
Employee firstEmployee = company.employees.get(0);
Employee secondEmployee = company.employees.get(1);
assertEmployees(firstEmployee, secondEmployee);
}
@Test
public void testForwardReferenceInMap() throws Exception
{
String json = "{\"employees\":{"
+ "\"1\":{\"id\":1,\"name\":\"First\",\"manager\":null,\"reports\":[2]},"
+ "\"2\": 2,"
+ "\"3\":{\"id\":2,\"name\":\"Second\",\"manager\":1,\"reports\":[]}"
+ "}}";
MappedCompany company = MAPPER.readValue(json, MappedCompany.class);
assertEquals(3, company.employees.size());
Employee firstEmployee = company.employees.get(1);
Employee secondEmployee = company.employees.get(3);
assertEmployees(firstEmployee, secondEmployee);
}
private void assertEmployees(Employee firstEmployee, Employee secondEmployee) {
assertEquals(1, firstEmployee.id);
assertEquals(2, secondEmployee.id);
assertEquals(1, firstEmployee.reports.size());
assertSame(secondEmployee, firstEmployee.reports.get(0));
assertSame(firstEmployee, secondEmployee.manager);
}
@Test
public void testForwardReferenceAnySetterCombo() throws Exception {
String json = "{\"@id\":1, \"foo\":2, \"bar\":{\"@id\":2, \"foo\":1}}";
AnySetterObjectId value = MAPPER.readValue(json, AnySetterObjectId.class);
assertSame(value.values.get("bar"), value.values.get("foo"));
}
@Test
public void testUnresolvedForwardReference() throws Exception
{
String json = "{\"employees\":["
+ "{\"id\":1,\"name\":\"First\",\"manager\":null,\"reports\":[3]},"
+ "{\"id\":2,\"name\":\"Second\",\"manager\":3,\"reports\":[]}"
+ "]}";
try {
MAPPER.readValue(json, Company.class);
fail("Should have thrown.");
} catch (UnresolvedForwardReference exception) {
List<UnresolvedId> unresolvedIds = exception.getUnresolvedIds();
assertEquals(2, unresolvedIds.size());
UnresolvedId firstUnresolvedId = unresolvedIds.get(0);
assertEquals(3, firstUnresolvedId.getId());
assertEquals(Employee.class, firstUnresolvedId.getType());
UnresolvedId secondUnresolvedId = unresolvedIds.get(1);
assertEquals(firstUnresolvedId.getId(), secondUnresolvedId.getId());
assertEquals(Employee.class, secondUnresolvedId.getType());
}
}
// [databind#299]: Allow unresolved ids to become nulls
@Test
public void testUnresolvableAsNull() throws Exception
{
IdWrapper w = MAPPER.readerFor(IdWrapper.class)
.without(DeserializationFeature.FAIL_ON_UNRESOLVED_OBJECT_IDS)
.readValue(a2q("{'node':123}"));
assertNotNull(w);
assertNull(w.node);
}
@Test
public void testKeepCollectionOrdering() throws Exception
{
String json = "{\"employees\":[2,1,"
+ "{\"id\":1,\"name\":\"First\",\"manager\":null,\"reports\":[2]},"
+ "{\"id\":2,\"name\":\"Second\",\"manager\":1,\"reports\":[]}"
+ "]}";
Company company = MAPPER.readValue(json, Company.class);
assertEquals(4, company.employees.size());
Employee firstEmployee = company.employees.get(1);
Employee secondEmployee = company.employees.get(0);
assertSame(firstEmployee, company.employees.get(2));
assertSame(secondEmployee, company.employees.get(3));
assertEmployees(firstEmployee, secondEmployee);
}
@Test
public void testKeepMapOrdering() throws Exception
{
String json = "{\"employees\":{"
+ "\"1\":2, \"2\":1,"
+ "\"3\":{\"id\":1,\"name\":\"First\",\"manager\":null,\"reports\":[2]},"
+ "\"4\":{\"id\":2,\"name\":\"Second\",\"manager\":1,\"reports\":[]}"
+ "}}";
MappedCompany company = MAPPER.readValue(json, MappedCompany.class);
assertEquals(4, company.employees.size());
Employee firstEmployee = company.employees.get(2);
Employee secondEmployee = company.employees.get(1);
assertEmployees(firstEmployee, secondEmployee);
// Deser must keep object ordering.
Iterator<Entry<Integer,Employee>> iterator = company.employees.entrySet().iterator();
assertSame(secondEmployee, iterator.next().getValue());
assertSame(firstEmployee, iterator.next().getValue());
assertSame(firstEmployee, iterator.next().getValue());
assertSame(secondEmployee, iterator.next().getValue());
}
/*
/**********************************************************
/* Unit tests, custom (property-based) id deserialization
/**********************************************************
*/
private final static String EXP_CUSTOM_VIA_CLASS = "{\"customId\":123,\"value\":-900,\"next\":123}";
@Test
public void testCustomDeserializationClass() throws Exception
{
IdentifiableCustom result = MAPPER.readValue(EXP_CUSTOM_VIA_CLASS, IdentifiableCustom.class);
assertEquals(-900, result.value);
assertSame(result, result.next);
}
private final static String EXP_CUSTOM_VIA_PROP = "{\"node\":{\"customId\":3,\"value\":99,\"next\":{\"node\":3}}}";
@Test
public void testCustomDeserializationProperty() throws Exception
{
IdWrapperExt result = MAPPER.readValue(EXP_CUSTOM_VIA_PROP, IdWrapperExt.class);
assertEquals(99, result.node.value);
assertSame(result.node, result.node.next.node);
assertEquals(3, result.node.customId);
}
/*
/**********************************************************
/* Unit tests, custom id resolver
/**********************************************************
*/
@Test
public void testCustomPoolResolver() throws Exception
{
Map<Object,WithCustomResolution> pool = new HashMap<>();
pool.put(1, new WithCustomResolution(1, 1));
pool.put(2, new WithCustomResolution(2, 2));
pool.put(3, new WithCustomResolution(3, 3));
pool.put(4, new WithCustomResolution(4, 4));
pool.put(5, new WithCustomResolution(5, 5));
ContextAttributes attrs = MAPPER.deserializationConfig().getAttributes()
.withSharedAttribute(POOL_KEY, pool);
String content = "{\"data\":[1,2,3,4,5]}";
CustomResolutionWrapper wrapper = MAPPER.readerFor(CustomResolutionWrapper.class)
.with(attrs).readValue(content);
assertFalse(wrapper.data.isEmpty());
for (WithCustomResolution ob : wrapper.data) {
assertSame(pool.get(ob.id), ob);
}
}
/*
/**********************************************************
/* Unit tests, null/missing Object id [databind#742]
/**********************************************************
*/
@Test
public void testNullObjectId() throws Exception
{
Identifiable value = MAPPER.readValue(
a2q("{'value':3, 'next':null, 'id':null}"), Identifiable.class);
assertNotNull(value);
assertEquals(3, value.value);
}
// [databind#1150]
@Test
public void testNullStringPropertyId() throws Exception
{
IdentifiableStringId value = MAPPER.readValue(
a2q("{'value':3, 'next':null, 'id':null}"), IdentifiableStringId.class);
assertNotNull(value);
assertEquals(3, value.value);
}
/*
/**********************************************************
/* Unit tests, DeserializationFeature.FAIL_ON_UNRESOLVED_OBJECT_IDS
/**********************************************************
*/
private final ObjectMapper DEFAULT_MAPPER = newJsonMapper();
private final ObjectMapper DISABLED_MAPPER = jsonMapperBuilder()
.disable(DeserializationFeature.FAIL_ON_UNRESOLVED_OBJECT_IDS)
.build();
private final ObjectMapper ENABLED_MAPPER = jsonMapperBuilder()
.enable(DeserializationFeature.FAIL_ON_UNRESOLVED_OBJECT_IDS)
.build();
@Test
public void testDefaultSetting() {
assertTrue(DEFAULT_MAPPER.isEnabled(DeserializationFeature.FAIL_ON_UNRESOLVED_OBJECT_IDS));
assertTrue(ENABLED_MAPPER.isEnabled(DeserializationFeature.FAIL_ON_UNRESOLVED_OBJECT_IDS));
assertFalse(DISABLED_MAPPER.isEnabled(DeserializationFeature.FAIL_ON_UNRESOLVED_OBJECT_IDS));
}
@Test
public void testSuccessResolvedObjectIds() throws Exception {
String json = a2q("{'node':{'@id':1,'value':7,'next':{'node':1}}}");
SomeWrapper wrapper = DEFAULT_MAPPER.readValue(json, SomeWrapper.class);
assertSame(wrapper.node, wrapper.node.next.node);
assertSame(wrapper.node.next.node, wrapper.node.next.node.next.node);
}
@Test
public void testUnresolvedObjectIdsFailure() throws Exception {
String json = a2q("{'node':{'@id':1,'value':7,'next':{'node':2}}}");
// 1. Does not fail with feature disabled
SomeWrapper wrapper = DISABLED_MAPPER.readValue(json, SomeWrapper.class);
assertNull(wrapper.node.next.node);
try {
// 2. Will fail by default
DEFAULT_MAPPER.readValue(json, SomeWrapper.class);
fail("should not pass");
} catch (UnresolvedForwardReference e) {
verifyException(e, "Unresolved forward reference", "Object id [2]");
}
try {
// 3. Will also fail with feature explicitly enabled
ENABLED_MAPPER.readValue(json, SomeWrapper.class);
fail("should not pass");
} catch (UnresolvedForwardReference e) {
verifyException(e, "Unresolved forward reference", "Object id [2]");
}
}
/*
/**********************************************************
/* Unit tests, type info with JsonIdentityInfo [databind#1083]
/**********************************************************
*/
@Test
public void testWithFieldsInBaseClass1083() throws Exception {
final String json = """
{"schemas": [{
"name": "FoodMart"
}]}
""";
MAPPER.readValue(json, JsonRoot1083.class);
}
/*
/**********************************************************
/* Unit tests, AtomicReference with ObjectId
/**********************************************************
*/
@Test
public void testAtomicReferenceWithObjectId() throws Exception
{
LinkedEmployee first = new LinkedEmployee();
first.id = 1;
first.name = "Alice";
LinkedEmployee second = new LinkedEmployee();
second.id = 2;
second.name = "Bob";
first.next(second);
second.next(first);
LinkedEmployeeList input = new LinkedEmployeeList();
input.first = new AtomicReference<>(first);
String json = MAPPER.writeValueAsString(input);
LinkedEmployeeList result = MAPPER.readValue(json, LinkedEmployeeList.class);
LinkedEmployee firstB = result.first.get();
assertNotNull(firstB);
assertEquals("Alice", firstB.name);
LinkedEmployee secondB = firstB.next.get();
assertNotNull(secondB);
assertEquals("Bob", secondB.name);
assertNotNull(secondB.next.get());
assertSame(firstB, secondB.next.get());
}
/*
/**********************************************************
/* Unit tests, ObjectId with injectable values [databind#538]
/**********************************************************
*/
@Test
public void testObjectIdWithInjectables() throws Exception
{
InjectableA a = new InjectableA("a");
InjectableB b = new InjectableB("b");
a.b = b;
b.a = a;
String json = MAPPER.writeValueAsString(a);
InjectableValues.Std inject = new InjectableValues.Std();
inject.addValue("i1", "e1");
inject.addValue("i2", "e2");
InjectableA output;
try {
output = MAPPER.reader(inject).forType(InjectableA.class).readValue(json);
} catch (Exception e) {
throw new IllegalStateException("Failed to deserialize from JSON '"+json+"'", e);
}
assertNotNull(output);
assertNotNull(output.b);
assertSame(output, output.b.a);
}
/*
/**********************************************************
/* Unit tests, forward reference in EnumMap
/**********************************************************
*/
@Test
public void testForwardReferenceInEnumMap() throws Exception {
String json = "{\"employees\":{"
+ "\"A\":{\"id\":1,\"name\":\"First\",\"manager\":null,\"reports\":[2]},"
+ "\"B\": 2,"
+ "\"C\":{\"id\":2,\"name\":\"Second\",\"manager\":1,\"reports\":[]}"
+ "}}";
EnumMapCompany company = MAPPER.readValue(json, EnumMapCompany.class);
assertEquals(3, company.employees.size());
Employee firstEmployee = company.employees.get(FooEnum.A);
Employee secondEmployee = company.employees.get(FooEnum.B);
assertEquals(1, firstEmployee.id);
assertEquals(2, secondEmployee.id);
assertEquals(1, firstEmployee.reports.size());
assertSame(secondEmployee, firstEmployee.reports.get(0));
assertSame(firstEmployee, secondEmployee.manager);
}
/*
/**********************************************************
/* Unit tests, ObjectId reordering [databind#1388]
/**********************************************************
*/
private final TypeReference<List<NamedThing>> NAMED_THING_LIST_TYPE =
new TypeReference<List<NamedThing>>() { };
// [databind#1388]
@Test
public void testOrdering1388() throws Exception
{
final UUID id = UUID.fromString("a59aa02c-fe3c-43f8-9b5a-5fe01878a818");
final NamedThing thing = new NamedThing(id, "Hello");
{
final String json = MAPPER.writeValueAsString(Arrays.asList(thing, thing, thing));
final List<NamedThing> list = MAPPER.readValue(json, NAMED_THING_LIST_TYPE);
_assertAllSame(list);
assertTrue(json.equals("[{\"@id\":1,\"id\":\"a59aa02c-fe3c-43f8-9b5a-5fe01878a818\",\"name\":\"Hello\"},1,1]"));
}
// now move it around to have forward references
{
final String json = "[1,1,{\"@id\":1,\"id\":\"a59aa02c-fe3c-43f8-9b5a-5fe01878a818\",\"name\":\"Hello\"}]";
final List<NamedThing> forward = MAPPER.readValue(json, NAMED_THING_LIST_TYPE);
_assertAllSame(forward);
}
// next, move @id to between properties
{
final String json = a2q("[{'id':'a59aa02c-fe3c-43f8-9b5a-5fe01878a818','@id':1,'name':'Hello'}, 1, 1]");
final List<NamedThing> forward = MAPPER.readValue(json, NAMED_THING_LIST_TYPE);
_assertAllSame(forward);
}
// and last, move @id to be not the first key in the object
{
final String json = a2q("[{'id':'a59aa02c-fe3c-43f8-9b5a-5fe01878a818','name':'Hello','@id':1}, 1, 1]");
final List<NamedThing> forward = MAPPER.readValue(json, NAMED_THING_LIST_TYPE);
_assertAllSame(forward);
}
}
@Test
public void testNullsNoObjectId() throws Exception
{
final List<NamedThing> l = MAPPER.readValue("[null]", NAMED_THING_LIST_TYPE);
assertEquals(1, l.size());
assertNull(l.get(0));
}
@Test
public void testUnresolvedObjectIdReordering() throws Exception
{
try {
MAPPER.readValue("[123]", NAMED_THING_LIST_TYPE);
fail("Should not pass");
} catch (UnresolvedForwardReference e) {
verifyException(e, "Unresolved forward references: [{Object id: 123}]");
}
}
private void _assertAllSame(List<?> entries) {
Object first = entries.get(0);
for (int i = 0, end = entries.size(); i < end; ++i) {
if (first != entries.get(i)) {
fail("Mismatch: entry #"+i+" not same as #0");
}
}
}
/*
/**********************************************************
/* Unit tests, ObjectReader + FAIL_ON_UNRESOLVED_OBJECT_IDS [databind#5542]
/**********************************************************
*/
@Test
public void testObjectMapperFailsOnUnresolvedObjectIds5542() throws Exception {
String json = a2q("{'node':{'@id':1,'value':7,'next':{'node':2}}}");
try {
MAPPER.readValue(json, ReaderWrapper5542.class);
fail("Should have thrown UnresolvedForwardReference");
} catch (UnresolvedForwardReference e) {
verifyException(e, "Unresolved forward reference");
}
}
@Test
public void testObjectReaderFailsOnUnresolvedObjectIds5542() throws Exception {
String json = a2q("{'node':{'@id':1,'value':7,'next':{'node':2}}}");
ObjectReader reader = MAPPER.readerFor(ReaderWrapper5542.class);
try (JsonParser p = MAPPER.createParser(json)) {
reader.readValue(p);
fail("Should have thrown UnresolvedForwardReference");
} catch (UnresolvedForwardReference e) {
verifyException(e, "Unresolved forward reference");
}
}
@Test
public void testObjectReaderWithDisabledFeature5542() throws Exception {
String json = a2q("{'node':{'@id':1,'value':7,'next':{'node':2}}}");
ObjectReader reader = MAPPER.readerFor(ReaderWrapper5542.class)
.without(DeserializationFeature.FAIL_ON_UNRESOLVED_OBJECT_IDS);
try (JsonParser p = MAPPER.createParser(json)) {
ReaderWrapper5542 wrapper = reader.readValue(p);
assertNotNull(wrapper);
assertNotNull(wrapper.node);
assertNull(wrapper.node.next.node);
}
}
@Test
public void testObjectReaderWithResolvedObjectIds5542() throws Exception {
String json = a2q("{'node':{'@id':1,'value':7,'next':{'node':1}}}");
ObjectReader reader = MAPPER.readerFor(ReaderWrapper5542.class);
try (JsonParser p = MAPPER.createParser(json)) {
ReaderWrapper5542 wrapper = reader.readValue(p);
assertNotNull(wrapper);
assertNotNull(wrapper.node);
assertSame(wrapper.node, wrapper.node.next.node);
}
}
/*
/**********************************************************
/* Unit tests, [databind#2955]: unresolved scalar Object Ids
/**********************************************************
*/
// [databind#2955]: default behavior should throw on unresolved scalar Object Id
@Test
public void testUnresolvedScalarObjectIdFails2955() throws Exception {
// Node with id=1 references node with id=999 which doesn't exist
String json = a2q("{'id':1,'name':'a','ref':{'id':2,'name':'b','ref':999}}");
try {
MAPPER.readValue(json, Node2955.class);
fail("Should have thrown UnresolvedForwardReference");
} catch (UnresolvedForwardReference e) {
verifyException(e, "Object id");
}
}
// [databind#2955]: with feature disabled, unresolved scalar Object Id should become null
@Test
public void testUnresolvedScalarObjectIdAsNull2955() throws Exception {
String json = a2q("{'id':1,'name':'a','ref':{'id':2,'name':'b','ref':999}}");
Node2955 result = DISABLED_MAPPER.readValue(json, Node2955.class);
assertNotNull(result);
assertEquals("a", result.name);
assertNotNull(result.ref);
assertEquals("b", result.ref.name);
// unresolved id=999 should become null instead of throwing
assertNull(result.ref.ref);
}
// [databind#2955] / jackson-jaxrs-providers#189: with feature disabled, valid forward
// references must still resolve (not get prematurely turned into null), while genuinely
// unresolvable scalar references become null.
@Test
public void testForwardAndUnresolvedScalarObjectIds2955() throws Exception {
String json = a2q("{"
+ "'owned':["
+ " {'name':'foo','optionalValue':'vFoo'},"
+ " {'name':'bar','optionalValue':'notAValidRef'},"
+ " {'name':'baz'},"
+ " {'name':'qux','optionalValue':{'name':'vQux','value':3}}"
+ "],"
+ "'values':["
+ " {'name':'vFoo','value':1},"
+ " {'name':'vBar','value':2}"
+ "]}");
Owner2955 owner = DISABLED_MAPPER.readValue(json, Owner2955.class);
assertEquals(4, owner.owned.size());
// Forward reference "vFoo" appears later in "values"; must resolve, not be null
assertNotNull(owner.owned.get(0).optionalValue);
assertEquals(Integer.valueOf(1), owner.owned.get(0).optionalValue.value);
// Reference that never appears anywhere -> null (feature disabled)
assertNull(owner.owned.get(1).optionalValue);
// No reference at all -> null
assertNull(owner.owned.get(2).optionalValue);
// Inline definition -> resolved directly
assertNotNull(owner.owned.get(3).optionalValue);
assertEquals(Integer.valueOf(3), owner.owned.get(3).optionalValue.value);
}
}