ExternalTypeIdView5958Test.java

package tools.jackson.databind.jsontype.ext;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.*;

import tools.jackson.databind.MapperFeature;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.ObjectReader;
import tools.jackson.databind.testutil.DatabindTestUtil;

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

// [databind#5958]: When a POJO uses a property-based @JsonCreator together with
// an EXTERNAL_PROPERTY type id whose `visible=true` settable property is
// restricted by @JsonView, the deserializer must skip that type-id property in
// views where it is not visible (instead of forwarding the type id into the
// external type handler).
public class ExternalTypeIdView5958Test extends DatabindTestUtil
{
    static class Views {
        static class Public { }
        static class Internal { }
    }

    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
    @JsonSubTypes({
            @JsonSubTypes.Type(value = Dog.class, name = "dog"),
            @JsonSubTypes.Type(value = Cat.class, name = "cat")
    })
    static abstract class Animal {
        public String name;
    }

    @JsonTypeName("dog")
    static class Dog extends Animal { }

    @JsonTypeName("cat")
    static class Cat extends Animal { }

    // Configured default impl. Note: since [databind#6055] a view-filtered external
    // property is skipped entirely (left null) rather than falling back to this, so
    // it is not exercised by the restricted-view case; kept to mirror real configs.
    @JsonTypeName("unknown")
    static class UnknownAnimal extends Animal { }

    static class Container {
        // External type id, exposed (`visible=true`) as a settable property,
        // restricted to the Internal view.
        @JsonView(Views.Internal.class)
        public String petType;

        @JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
                include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
                property = "petType",
                visible = true,
                defaultImpl = UnknownAnimal.class)
        @JsonSubTypes({
                @JsonSubTypes.Type(value = Dog.class, name = "dog"),
                @JsonSubTypes.Type(value = Cat.class, name = "cat")
        })
        @JsonView(Views.Internal.class)
        public Animal pet;

        public String label;

        // Property-based creator forces the
        // deserializeUsingPropertyBasedWithExternalTypeId code path.
        @JsonCreator
        public Container(@JsonProperty("label") String label) {
            this.label = label;
        }
    }

    private final ObjectMapper MAPPER = jsonMapperBuilder()
            // Properties without a @JsonView are visible in every view; ones
            // with @JsonView are only visible in (a subtype of) that view.
            .enable(MapperFeature.DEFAULT_VIEW_INCLUSION)
            .build();

    private static final String JSON = a2q(
            "{'label':'box','petType':'dog','pet':{'name':'Rex'}}");

    @Test
    public void typeIdHonoredInVisibleView() throws Exception
    {
        ObjectReader reader = MAPPER.readerFor(Container.class)
                .withView(Views.Internal.class);
        Container c = reader.readValue(JSON);

        assertEquals("box", c.label);
        assertEquals("dog", c.petType);
        assertNotNull(c.pet);
        assertInstanceOf(Dog.class, c.pet);
        assertEquals("Rex", c.pet.name);
    }

    // The core [databind#5958] assertion: under a view where the type-id property is
    // not visible, the value in the JSON must NOT be forwarded into the
    // external type handler. Without the patch, `petType:"dog"` would be picked
    // up via `_propsByIndex`/`handleTypePropertyValue` even though the field is
    // restricted to a different view, and `pet` would be deserialized as `Dog`.
    //
    // Here the value property `pet` is itself `@JsonView(Internal)`-restricted, so
    // under the Public view it is a view-filtered property. Per [databind#6055]
    // (GHSA-mhm7-754m-9p8w) a view-filtered EXTERNAL_PROPERTY value is skipped
    // entirely -- both the value and its external type id -- and left null, the
    // same way any ordinary view-filtered property is; `defaultImpl` does not apply
    // to a property that is invisible in the active view.
    @Test
    public void typeIdSkippedInRestrictedView() throws Exception
    {
        ObjectReader reader = MAPPER.readerFor(Container.class)
                .withView(Views.Public.class);
        Container c = reader.readValue(JSON);

        assertEquals("box", c.label);
        assertNull(c.petType, "petType is @JsonView-restricted; must be skipped under Public view");
        assertNull(c.pet,
                "pet is @JsonView-restricted; view-filtered external property must be left null, "
                + "type id must not leak across views");
    }
}