NativeObjectIdAndTypeIdTest.java
package tools.jackson.databind.objectid;
import java.util.*;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.annotation.*;
import tools.jackson.core.*;
import tools.jackson.databind.*;
import tools.jackson.databind.testutil.DatabindTestUtil;
import tools.jackson.databind.util.TokenBuffer;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests for handling of "native" Object Ids and Type Ids -- that is, ids
* provided by format (like YAML anchors for Object Ids, or tagged types)
* rather than as JSON properties. Tests use {@link TokenBuffer} to simulate
* a parser that supports native ids.
*<p>
* Native ids are written via {@code writeObjectId()} / {@code writeTypeId()}
* before the token they should be associated with. The TokenBuffer stores
* the pending id with the next appended token.
*/
public class NativeObjectIdAndTypeIdTest extends DatabindTestUtil
{
/*
/**********************************************************
/* Helper types for Object Id tests
/**********************************************************
*/
// Simple bean with Object Identity using IntSequenceGenerator
@JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class, property = "@id")
static class IdBean {
public int value;
public IdBean next;
public IdBean() { }
public IdBean(int v) { value = v; }
}
// Bean using PropertyGenerator for Object Id (native id replaces property)
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
static class PropIdBean {
public String id;
public String name;
public PropIdBean() { }
}
// Bean with @JsonCreator and property-based Object Id
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
static class CreatorIdBean {
public String id;
public String name;
@JsonCreator
public CreatorIdBean(@JsonProperty("id") String id, @JsonProperty("name") String name) {
this.id = id;
this.name = name;
}
}
// Wrapper that references an IdBean
@JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class, property = "@id")
static class IdBeanWrapper {
public IdBean first;
public IdBean second;
}
/*
/**********************************************************
/* Helper types for Type Id tests
/**********************************************************
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "@type")
@JsonSubTypes({
@JsonSubTypes.Type(value = Dog.class, name = "dog"),
@JsonSubTypes.Type(value = Cat.class, name = "cat")
})
static class Animal {
public String name;
}
static class Dog extends Animal {
public String breed;
}
static class Cat extends Animal {
public int lives;
}
// Container for polymorphic value
static class AnimalWrapper {
public Animal animal;
}
/*
/**********************************************************
/* Helper types combining Object Id + Type Id
/**********************************************************
*/
@JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class, property = "@id")
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "@type")
@JsonSubTypes({
@JsonSubTypes.Type(value = TypedIdNodeSub.class, name = "sub")
})
static class TypedIdNode {
public String name;
}
static class TypedIdNodeSub extends TypedIdNode {
public int extra;
}
/*
/**********************************************************
/* Test methods: Native Object Id
/**********************************************************
*/
private final ObjectMapper MAPPER = newJsonMapper();
// Basic: native Object Id with default constructor bean
@Test
public void testNativeObjectIdBasic() throws Exception
{
// Native id is set before first property name, so it's on the FIELD_NAME
// token where BeanDeserializer checks for it (after consuming START_OBJECT)
TokenBuffer buf = new TokenBuffer(null, true);
buf.writeStartObject();
buf.writeObjectId(1); // will be associated with next token (FIELD_NAME)
buf.writeName("value");
buf.writeNumber(42);
buf.writeEndObject();
JsonParser p = buf.asParser(ObjectReadContext.empty());
IdBean result = MAPPER.readValue(p, IdBean.class);
p.close();
buf.close();
assertNotNull(result);
assertEquals(42, result.value);
}
// Native Object Id with forward reference: second reference uses id
@Test
public void testNativeObjectIdWithReference() throws Exception
{
TokenBuffer buf = new TokenBuffer(null, true);
buf.writeStartObject();
// "first": full object with native id
buf.writeName("first");
buf.writeStartObject();
buf.writeObjectId(1); // associated with the next FIELD_NAME
buf.writeName("value");
buf.writeNumber(99);
buf.writeEndObject();
// "second": reference by id value
buf.writeName("second");
buf.writeNumber(1);
buf.writeEndObject();
JsonParser p = buf.asParser(ObjectReadContext.empty());
IdBeanWrapper result = MAPPER.readValue(p, IdBeanWrapper.class);
p.close();
buf.close();
assertNotNull(result);
assertNotNull(result.first);
assertEquals(99, result.first.value);
// second should resolve to the same instance
assertSame(result.first, result.second);
}
// Native Object Id with PropertyGenerator
@Test
public void testNativeObjectIdPropertyGenerator() throws Exception
{
TokenBuffer buf = new TokenBuffer(null, true);
buf.writeStartObject();
buf.writeObjectId("myId123"); // native id
buf.writeName("id");
buf.writeString("myId123");
buf.writeName("name");
buf.writeString("test");
buf.writeEndObject();
JsonParser p = buf.asParser(ObjectReadContext.empty());
PropIdBean result = MAPPER.readValue(p, PropIdBean.class);
p.close();
buf.close();
assertNotNull(result);
assertEquals("myId123", result.id);
assertEquals("test", result.name);
}
// Native Object Id with @JsonCreator (property-based creator path)
@Test
public void testNativeObjectIdWithCreator() throws Exception
{
TokenBuffer buf = new TokenBuffer(null, true);
buf.writeStartObject();
buf.writeObjectId("creatorId"); // native id
buf.writeName("id");
buf.writeString("creatorId");
buf.writeName("name");
buf.writeString("created");
buf.writeEndObject();
JsonParser p = buf.asParser(ObjectReadContext.empty());
CreatorIdBean result = MAPPER.readValue(p, CreatorIdBean.class);
p.close();
buf.close();
assertNotNull(result);
assertEquals("creatorId", result.id);
assertEquals("created", result.name);
}
/*
/**********************************************************
/* Test methods: Native Type Id
/**********************************************************
*/
// Native Type Id with AS_PROPERTY -- type id written before START_OBJECT
// so the type deserializer sees it on the START_OBJECT token
@Test
public void testNativeTypeIdAsProperty() throws Exception
{
TokenBuffer buf = new TokenBuffer(null, true);
buf.writeTypeId("dog"); // pending, will attach to next token
buf.writeStartObject(); // type id now on START_OBJECT
buf.writeName("name");
buf.writeString("Rex");
buf.writeName("breed");
buf.writeString("Labrador");
buf.writeEndObject();
JsonParser p = buf.asParser(ObjectReadContext.empty());
Animal result = MAPPER.readValue(p, Animal.class);
p.close();
buf.close();
assertNotNull(result);
assertInstanceOf(Dog.class, result);
Dog dog = (Dog) result;
assertEquals("Rex", dog.name);
assertEquals("Labrador", dog.breed);
}
// Native Type Id resolving to a different subtype
@Test
public void testNativeTypeIdCat() throws Exception
{
TokenBuffer buf = new TokenBuffer(null, true);
buf.writeTypeId("cat");
buf.writeStartObject();
buf.writeName("name");
buf.writeString("Whiskers");
buf.writeName("lives");
buf.writeNumber(9);
buf.writeEndObject();
JsonParser p = buf.asParser(ObjectReadContext.empty());
Animal result = MAPPER.readValue(p, Animal.class);
p.close();
buf.close();
assertNotNull(result);
assertInstanceOf(Cat.class, result);
Cat cat = (Cat) result;
assertEquals("Whiskers", cat.name);
assertEquals(9, cat.lives);
}
// Native Type Id in a nested property context
@Test
public void testNativeTypeIdInProperty() throws Exception
{
TokenBuffer buf = new TokenBuffer(null, true);
buf.writeStartObject();
buf.writeName("animal");
buf.writeTypeId("cat"); // type id for the nested object
buf.writeStartObject();
buf.writeName("name");
buf.writeString("Felix");
buf.writeName("lives");
buf.writeNumber(7);
buf.writeEndObject();
buf.writeEndObject();
JsonParser p = buf.asParser(ObjectReadContext.empty());
AnimalWrapper result = MAPPER.readValue(p, AnimalWrapper.class);
p.close();
buf.close();
assertNotNull(result);
assertNotNull(result.animal);
assertInstanceOf(Cat.class, result.animal);
assertEquals("Felix", result.animal.name);
assertEquals(7, ((Cat) result.animal).lives);
}
/*
/**********************************************************
/* Test methods: Combined Object Id + Type Id
/**********************************************************
*/
// Both native Object Id and native Type Id on same object
@Test
public void testNativeObjectIdAndTypeIdCombined() throws Exception
{
TokenBuffer buf = new TokenBuffer(null, true);
// Type id before START_OBJECT so type deserializer sees it
buf.writeTypeId("sub");
buf.writeStartObject();
// Object id before first property so bean deserializer sees it
buf.writeObjectId(1);
buf.writeName("name");
buf.writeString("combined");
buf.writeName("extra");
buf.writeNumber(7);
buf.writeEndObject();
JsonParser p = buf.asParser(ObjectReadContext.empty());
TypedIdNode result = MAPPER.readValue(p, TypedIdNode.class);
p.close();
buf.close();
assertNotNull(result);
assertInstanceOf(TypedIdNodeSub.class, result);
assertEquals("combined", result.name);
assertEquals(7, ((TypedIdNodeSub) result).extra);
}
/*
/**********************************************************
/* Test methods: TokenBuffer parser native id API
/**********************************************************
*/
// Verify that TokenBuffer parser correctly reports native id capabilities
@Test
public void testTokenBufferParserNativeIdFlags() throws Exception
{
// With native ids enabled
try (TokenBuffer buf = new TokenBuffer(null, true)) {
buf.writeStartObject();
buf.writeEndObject();
try (JsonParser p = buf.asParser(ObjectReadContext.empty())) {
assertTrue(p.canReadObjectId());
assertTrue(p.canReadTypeId());
}
}
// Without native ids
try (TokenBuffer buf = TokenBuffer.forGeneration()) {
buf.writeStartObject();
buf.writeEndObject();
try (JsonParser p = buf.asParser(ObjectReadContext.empty())) {
assertFalse(p.canReadObjectId());
assertFalse(p.canReadTypeId());
}
}
}
// Verify native ids are null when not set on a token
@Test
public void testTokenBufferParserNativeIdNullWhenNotSet() throws Exception
{
try (TokenBuffer buf = new TokenBuffer(null, true)) {
buf.writeStartObject();
buf.writeName("field");
buf.writeString("value");
buf.writeEndObject();
try (JsonParser p = buf.asParser(ObjectReadContext.empty())) {
p.nextToken(); // START_OBJECT
assertNull(p.getObjectId());
assertNull(p.getTypeId());
}
}
}
// Verify native ids survive round-trip through TokenBuffer serialization
@Test
public void testNativeIdRoundTripThroughTokenBuffer() throws Exception
{
TokenBuffer source = new TokenBuffer(null, true);
source.writeTypeId("dog");
source.writeStartObject();
source.writeName("name");
source.writeString("Fido");
source.writeName("breed");
source.writeString("Mutt");
source.writeEndObject();
// Serialize into another TokenBuffer
TokenBuffer target = new TokenBuffer(null, true);
source.serialize(target);
// Read from target
JsonParser p = target.asParser(ObjectReadContext.empty());
Animal result = MAPPER.readValue(p, Animal.class);
p.close();
target.close();
source.close();
assertNotNull(result);
assertInstanceOf(Dog.class, result);
assertEquals("Fido", result.name);
assertEquals("Mutt", ((Dog) result).breed);
}
}