UnwrappedWithIgnore1075Test.java

package tools.jackson.databind.struct;

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.annotation.JsonPOJOBuilder;
import tools.jackson.databind.testutil.DatabindTestUtil;

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

public class UnwrappedWithIgnore1075Test extends DatabindTestUtil
{
    private final ObjectMapper MAPPER = newJsonMapper();

    // [databind#1075]
    static class Outer {
        @JsonUnwrapped
        private Inner inner;

        @JsonIgnore
        public Long getId() {
            return inner.id;
        }
    }

    static class Inner {
        @JsonProperty
        Long id;

        @JsonProperty
        String name;
    }

    // [databind#1075]: same as `Outer`, but with a property-based `@JsonCreator`,
    // which is handled by a separate code path
    static class OuterWithCreator {
        @JsonUnwrapped
        private Inner inner;

        private final int tag;

        @JsonCreator
        public OuterWithCreator(@JsonProperty("tag") int tag) {
            this.tag = tag;
        }

        @JsonIgnore
        public Long getId() {
            return inner.id;
        }

        public int getTag() {
            return tag;
        }
    }

    // [databind#1075]: builder-based equivalent, with a property-based Creator on the
    // builder, so that the outer type declares "id" ignorable while the unwrapped
    // `Inner` is the one that actually provides it
    @JsonDeserialize(builder = OuterBuilder.class)
    static class OuterFromBuilder {
        final Inner inner;
        final int tag;

        OuterFromBuilder(Inner inner, int tag) {
            this.inner = inner;
            this.tag = tag;
        }

        public Long getId() {
            return inner.id;
        }
    }

    @JsonPOJOBuilder(withPrefix = "with")
    @JsonIgnoreProperties({"id"})
    static class OuterBuilder {
        @JsonUnwrapped
        Inner inner;

        final int tag;

        @JsonCreator
        OuterBuilder(@JsonProperty("tag") int tag) {
            this.tag = tag;
        }

        public OuterFromBuilder build() {
            return new OuterFromBuilder(inner, tag);
        }
    }

    // [databind#1075]
    @Test
    public void jsonUnwrappedShouldDeserializeFieldsWithGetterInOuterClass() throws Exception
    {
        final String JSON = "{\"id\": 1, \"name\": \"John\"}";
        Outer outer = MAPPER.readValue(JSON, Outer.class);
        assertEquals(Long.valueOf(1), outer.getId());
    }

    // [databind#1075]: as above but via property-based Creator -- goes through
    // `BeanDeserializer.deserializeUsingPropertyBasedWithUnwrapped()` instead of
    // `deserializeWithUnwrapped()`, so needs its own coverage
    @Test
    public void jsonUnwrappedShouldDeserializeFieldsWithGetterInOuterClassViaCreator() throws Exception
    {
        OuterWithCreator outer = MAPPER.readValue("""
                {"tag": 7, "id": 1, "name": "John"}
                """, OuterWithCreator.class);
        assertEquals(Long.valueOf(1), outer.getId());
        assertEquals("John", outer.inner.name);
        assertEquals(7, outer.getTag());
    }

    // [databind#1075]: as above but builder-based -- goes through
    // `BuilderBasedDeserializer.deserializeUsingPropertyBasedWithUnwrapped()`.
    //
    // NOTE: property ORDER is significant here. That method delegates to
    // `deserializeWithUnwrapped()` as soon as the last Creator property is seen, so
    // the Creator property ("tag") must come LAST for the unwrapped/ignorable names
    // to be handled by the method under test. Both orders are asserted, since the
    // result must not depend on JSON property order.
    @Test
    public void jsonUnwrappedShouldDeserializeIgnoredNameViaBuilderCreator() throws Exception
    {
        // Creator property last: handled by deserializeUsingPropertyBasedWithUnwrapped()
        OuterFromBuilder outer = MAPPER.readValue("""
                {"id": 1, "name": "John", "tag": 7}
                """, OuterFromBuilder.class);
        assertEquals(Long.valueOf(1), outer.getId());
        assertEquals("John", outer.inner.name);
        assertEquals(7, outer.tag);

        // Creator property first: delegated to deserializeWithUnwrapped()
        OuterFromBuilder outer2 = MAPPER.readValue("""
                {"tag": 7, "id": 1, "name": "John"}
                """, OuterFromBuilder.class);
        assertEquals(Long.valueOf(1), outer2.getId());
        assertEquals("John", outer2.inner.name);
        assertEquals(7, outer2.tag);
    }
}