ObjectIdWithTypeInfo4014Test.java

package tools.jackson.databind.objectid;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;

import tools.jackson.databind.*;
import tools.jackson.databind.testutil.DatabindTestUtil;

import static org.junit.jupiter.api.Assertions.*;

/**
 * Test for [databind#4014]: combining {@code @JsonTypeInfo(include = As.PROPERTY)}
 * with {@code @JsonIdentityInfo(generator = PropertyGenerator.class)} on an interface
 * should work for deserialization.
 */
public class ObjectIdWithTypeInfo4014Test extends DatabindTestUtil
{
    @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@c")
    @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "@id")
    public interface BaseEntity {
        @JsonProperty("@id")
        Integer getId();
    }

    // Immutable classes: constructor-only @id, no setter
    static class Foo implements BaseEntity {
        private final Integer id;
        private Bar bar;

        @JsonCreator
        public Foo(@JsonProperty("@id") Integer id) {
            this.id = id;
        }

        @Override @JsonProperty("@id") public Integer getId() { return id; }
        public Bar getBar() { return bar; }
        public void setBar(Bar bar) { this.bar = bar; }
    }

    static class Bar implements BaseEntity {
        private final Integer id;
        private Foo foo;

        @JsonCreator
        public Bar(@JsonProperty("@id") Integer id) {
            this.id = id;
        }

        @Override @JsonProperty("@id") public Integer getId() { return id; }
        public Foo getFoo() { return foo; }
        public void setFoo(Foo foo) { this.foo = foo; }
    }

    // Mutable classes: default constructor + setters
    static class FooWithSetter implements BaseEntity {
        private Integer id;
        private BarWithSetter bar;

        public FooWithSetter() { }

        @Override @JsonProperty("@id") public Integer getId() { return id; }
        @JsonProperty("@id") public void setId(Integer id) { this.id = id; }
        public BarWithSetter getBar() { return bar; }
        public void setBar(BarWithSetter bar) { this.bar = bar; }
    }

    static class BarWithSetter implements BaseEntity {
        private Integer id;
        private FooWithSetter foo;

        public BarWithSetter() { }

        @Override @JsonProperty("@id") public Integer getId() { return id; }
        @JsonProperty("@id") public void setId(Integer id) { this.id = id; }
        public FooWithSetter getFoo() { return foo; }
        public void setFoo(FooWithSetter foo) { this.foo = foo; }
    }

    // Wrapper-array variants: exercises AsArrayTypeDeserializer fix
    @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.WRAPPER_ARRAY, property = "@c")
    @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "@id")
    public interface WrapperArrayEntity {
        @JsonProperty("@id")
        Integer getId();
    }

    static class WrapFoo implements WrapperArrayEntity {
        private final Integer id;
        private WrapBar bar;

        @JsonCreator
        public WrapFoo(@JsonProperty("@id") Integer id) { this.id = id; }

        @Override @JsonProperty("@id") public Integer getId() { return id; }
        public WrapBar getBar() { return bar; }
        public void setBar(WrapBar bar) { this.bar = bar; }
    }

    static class WrapBar implements WrapperArrayEntity {
        private final Integer id;
        private WrapFoo foo;

        @JsonCreator
        public WrapBar(@JsonProperty("@id") Integer id) { this.id = id; }

        @Override @JsonProperty("@id") public Integer getId() { return id; }
        public WrapFoo getFoo() { return foo; }
        public void setFoo(WrapFoo foo) { this.foo = foo; }
    }

    private final ObjectMapper MAPPER = newJsonMapper();

    // [databind#4014]: simple round-trip via interface (setter-based)
    @Test
    public void testSimpleDeserializationViaInterface() throws Exception
    {
        FooWithSetter foo = new FooWithSetter();
        foo.setId(1);
        String json = MAPPER.writeValueAsString(foo);
        BaseEntity result = MAPPER.readValue(json, BaseEntity.class);
        assertNotNull(result);
        assertInstanceOf(FooWithSetter.class, result);
        assertEquals(1, ((FooWithSetter) result).getId());
    }

    // [databind#4014]: simple round-trip via interface (constructor-based)
    @Test
    public void testSimpleDeserializationWithCreator() throws Exception
    {
        Foo foo = new Foo(1);
        String json = MAPPER.writeValueAsString(foo);
        BaseEntity result = MAPPER.readValue(json, BaseEntity.class);
        assertNotNull(result);
        assertInstanceOf(Foo.class, result);
        assertEquals(1, ((Foo) result).getId());
    }

    // [databind#4014]: circular references with setter-based classes
    @Test
    public void testCircularReferenceWithSetters() throws Exception
    {
        FooWithSetter foo = new FooWithSetter();
        foo.setId(1);
        BarWithSetter bar = new BarWithSetter();
        bar.setId(2);
        foo.setBar(bar);
        bar.setFoo(foo);

        String json = MAPPER.writeValueAsString(foo);

        BaseEntity result = MAPPER.readValue(json, BaseEntity.class);
        assertNotNull(result);
        assertInstanceOf(FooWithSetter.class, result);

        FooWithSetter resultFoo = (FooWithSetter) result;
        assertEquals(1, resultFoo.getId());
        assertNotNull(resultFoo.getBar());
        assertEquals(2, resultFoo.getBar().getId());
        assertSame(resultFoo, resultFoo.getBar().getFoo());
    }

    // [databind#4014]: circular references with constructor-based classes
    @Test
    public void testCircularReferenceWithCreator() throws Exception
    {
        Foo foo = new Foo(1);
        Bar bar = new Bar(2);
        foo.setBar(bar);
        bar.setFoo(foo);

        String json = MAPPER.writeValueAsString(foo);

        BaseEntity result = MAPPER.readValue(json, BaseEntity.class);
        assertNotNull(result);
        assertInstanceOf(Foo.class, result);

        Foo resultFoo = (Foo) result;
        assertEquals(1, resultFoo.getId());
        assertNotNull(resultFoo.getBar());
        assertEquals(2, resultFoo.getBar().getId());
        assertSame(resultFoo, resultFoo.getBar().getFoo());
    }

    // [databind#4014]: simple round-trip with WRAPPER_ARRAY
    @Test
    public void testSimpleDeserializationWrapperArray() throws Exception
    {
        WrapFoo foo = new WrapFoo(1);
        String json = MAPPER.writeValueAsString(foo);
        WrapperArrayEntity result = MAPPER.readValue(json, WrapperArrayEntity.class);
        assertNotNull(result);
        assertInstanceOf(WrapFoo.class, result);
        assertEquals(1, ((WrapFoo) result).getId());
    }

    // [databind#4014]: circular references with WRAPPER_ARRAY
    @Test
    public void testCircularReferenceWrapperArray() throws Exception
    {
        WrapFoo foo = new WrapFoo(1);
        WrapBar bar = new WrapBar(2);
        foo.setBar(bar);
        bar.setFoo(foo);

        String json = MAPPER.writeValueAsString(foo);

        WrapperArrayEntity result = MAPPER.readValue(json, WrapperArrayEntity.class);
        assertNotNull(result);
        assertInstanceOf(WrapFoo.class, result);

        WrapFoo resultFoo = (WrapFoo) result;
        assertEquals(1, resultFoo.getId());
        assertNotNull(resultFoo.getBar());
        assertEquals(2, resultFoo.getBar().getId());
        assertSame(resultFoo, resultFoo.getBar().getFoo());
    }
}