JsonValueCycleSer6206Test.java
package tools.jackson.databind.ser;
import java.util.*;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonValue;
import tools.jackson.core.JsonGenerator;
import tools.jackson.databind.*;
import tools.jackson.databind.annotation.JsonSerialize;
import tools.jackson.databind.exc.InvalidDefinitionException;
import tools.jackson.databind.testutil.DatabindTestUtil;
import static org.junit.jupiter.api.Assertions.*;
// [databind#6206]
/**
* Serializing through a {@code @JsonValue} accessor writes no structural tokens of its
* own, so a cycle in which every value is serialized that way never trips the nesting
* limit of {@code StreamWriteConstraints}. Verify it is reported as a
* {@link DatabindException}, the way cycles through bean properties are, rather than
* letting a {@link StackOverflowError} escape.
*/
public class JsonValueCycleSer6206Test extends DatabindTestUtil
{
static class SelfValue {
@JsonValue
public SelfValue value() { return this; }
}
static class ValueCycle {
public ValueCycle other;
@JsonValue
public ValueCycle value() { return other; }
}
static class ValueA {
public ValueB b;
@JsonValue
public ValueB value() { return b; }
}
static class ValueB {
public ValueA a;
@JsonValue
public ValueA value() { return a; }
}
static class TypedSelf {
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_ARRAY)
@JsonValue
public TypedSelf value() { return this; }
}
// Self-reference is fine if the value is NOT serialized the same way
static class SelfWithCustomSerializer {
@JsonValue
@JsonSerialize(using = FixedSerializer.class)
public SelfWithCustomSerializer value() { return this; }
}
static class FixedSerializer extends ValueSerializer<SelfWithCustomSerializer> {
@Override
public void serialize(SelfWithCustomSerializer value, JsonGenerator gen, SerializationContext ctxt)
{
gen.writeString("fixed");
}
}
static class Typed {
public Typed other;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_ARRAY)
@JsonValue
public Typed value() { return other; }
}
// Non-cyclic use of `@JsonValue` must keep working
static class Plain {
private final String _value;
Plain(String value) { _value = value; }
@JsonValue
public String value() { return _value; }
}
// Accessor returning `this` but typed as a base type whose own (different)
// `@JsonValue` accessor is used with static typing: not a cycle
static class NamedBase {
@JsonValue
public String name() { return "base"; }
}
static class SelfAsBase extends NamedBase {
@JsonValue(false)
@Override
public String name() { return "sub"; }
@JsonValue
public NamedBase self() { return this; }
}
private final ObjectMapper MAPPER = newJsonMapper();
// Immediate self-reference: detected before recursing, with a specific message
@Test
public void accessorReturningItself() throws Exception {
_verifySelfReference(new SelfValue());
}
@Test
public void accessorReturningItselfWithTypeInformation() throws Exception {
_verifySelfReference(new TypedSelf());
}
@Test
public void selfReferenceAllowedIfDisabled() throws Exception {
ObjectMapper mapper = jsonMapperBuilder()
.disable(SerializationFeature.FAIL_ON_SELF_REFERENCES)
.build();
try {
String json = mapper.writeValueAsString(new SelfValue());
fail("Should not pass, produced: "+json);
} catch (DatabindException e) {
verifyException(e, "Infinite recursion (StackOverflowError)");
}
}
@Test
public void selfReferenceAsNull() throws Exception {
ObjectMapper mapper = jsonMapperBuilder()
.disable(SerializationFeature.FAIL_ON_SELF_REFERENCES)
.enable(SerializationFeature.WRITE_SELF_REFERENCES_AS_NULL)
.build();
assertEquals("null", mapper.writeValueAsString(new SelfValue()));
assertEquals("null", mapper.writeValueAsString(new TypedSelf()));
assertEquals(a2q("{'self':null}"),
mapper.writeValueAsString(Collections.singletonMap("self", new SelfValue())));
// but custom serializer still used as-is
assertEquals(q("fixed"), mapper.writeValueAsString(new SelfWithCustomSerializer()));
}
@Test
public void selfReferenceWithCustomSerializer() throws Exception {
assertEquals(q("fixed"), MAPPER.writeValueAsString(new SelfWithCustomSerializer()));
}
@Test
public void cycleBetweenTwoValues() throws Exception {
ValueCycle v1 = new ValueCycle();
ValueCycle v2 = new ValueCycle();
v1.other = v2;
v2.other = v1;
_verifyInfiniteRecursion(v1);
}
@Test
public void cycleAcrossTwoTypes() throws Exception {
ValueA a = new ValueA();
ValueB b = new ValueB();
a.b = b;
b.a = a;
_verifyInfiniteRecursion(a);
}
@Test
public void cycleWithTypeInformation() throws Exception {
Typed t1 = new Typed();
Typed t2 = new Typed();
t1.other = t2;
t2.other = t1;
_verifyInfiniteRecursion(t1);
}
// Self-reference via a different `@JsonValue` accessor is not a cycle
@Test
public void selfReferenceThroughDifferentAccessor() throws Exception {
ObjectMapper mapper = jsonMapperBuilder()
.enable(MapperFeature.USE_STATIC_TYPING)
.build();
assertEquals(q("sub"), mapper.writeValueAsString(new SelfAsBase()));
}
// Ordinary `@JsonValue` handling must be unaffected
@Test
public void plainValueStillWorks() throws Exception {
assertEquals(q("abc"), MAPPER.writeValueAsString(new Plain("abc")));
assertEquals(a2q("['a','b']"),
MAPPER.writeValueAsString(Arrays.asList(new Plain("a"), new Plain("b"))));
}
private void _verifySelfReference(Object value) throws Exception
{
try {
String json = MAPPER.writeValueAsString(value);
fail("Should not pass, produced: "+json);
} catch (InvalidDefinitionException e) {
verifyException(e, "Direct self-reference leading to cycle");
verifyException(e, "value()");
}
}
private void _verifyInfiniteRecursion(Object value) throws Exception
{
try {
String json = MAPPER.writeValueAsString(value);
fail("Should not pass, produced: "+json);
} catch (DatabindException e) {
verifyException(e, "Infinite recursion (StackOverflowError)");
}
}
}