JsonViewExternalTypeId6055Test.java

package tools.jackson.databind.views;

import com.fasterxml.jackson.annotation.*;

import org.junit.jupiter.api.Test;

import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.testutil.DatabindTestUtil;

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

// [databind#6055] A property with an EXTERNAL_PROPERTY type id that is filtered
// out by an active JsonView should be skipped gracefully (left null), the same
// way ordinary view-filtered properties are -- the dangling external type id
// ("kind") must not trigger a "Missing property" MismatchedInputException.
public class JsonViewExternalTypeId6055Test extends DatabindTestUtil {
    public static class PublicView {}
    public static class AdminView extends PublicView {}

    public static abstract class Asset {
        @JsonView(AdminView.class)
        public String name;
    }
    public static class PublicAsset extends Asset {}
    public static class AdminAsset extends Asset {
        @JsonView(AdminView.class)
        public String secret;
    }

    public static class Container {
        @JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
                include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
                property = "kind")
        @JsonSubTypes({
                @JsonSubTypes.Type(value = PublicAsset.class, name = "pub"),
                @JsonSubTypes.Type(value = AdminAsset.class,  name = "admin")
        })
        @JsonView(AdminView.class)
        public Asset asset;

        @JsonView(PublicView.class)
        public String label;

        @JsonCreator
        public Container(
                @JsonProperty("label") @JsonView(PublicView.class) String label,
                @JsonProperty("asset") @JsonView(AdminView.class) Asset asset) {
            this.label = label;
            this.asset = asset;
        }
    }

    public static class Wrapper {
        @JsonView(PublicView.class)
        public Container data;
    }

    // Default-constructor variant, to exercise the bean-based (non-creator)
    // external-type-id path in addition to the property-based-creator one above.
    public static class BeanContainer {
        @JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
                include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
                property = "kind")
        @JsonSubTypes({
                @JsonSubTypes.Type(value = PublicAsset.class, name = "pub"),
                @JsonSubTypes.Type(value = AdminAsset.class,  name = "admin")
        })
        @JsonView(AdminView.class)
        public Asset asset;

        @JsonView(PublicView.class)
        public String label;
    }

    public static class BeanWrapper {
        @JsonView(PublicView.class)
        public BeanContainer data;
    }

    private final ObjectMapper MAPPER = sharedMapper();

    // Type id ("kind") appears BEFORE the value ("asset"): exercises the eager-bind
    // branch (both type id and value available when value token arrives).
    private static final String JSON_TYPEID_FIRST = a2q(
            "{'data':{'label':'hello','kind':'admin',"
            + "'asset':{'name':'foo','secret':'LEAKED'}}}");

    // Type id ("kind") appears AFTER the value ("asset"): value is buffered first,
    // then the type id arrives -- exercises the other ordering of the same paths.
    private static final String JSON_VALUE_FIRST = a2q(
            "{'data':{'label':'hello',"
            + "'asset':{'name':'foo','secret':'LEAKED'},'kind':'admin'}}");

    // Admin-only "asset" is annotated @JsonView(AdminView.class); AdminView extends
    // PublicView, so the property is NOT visible under PublicView and must be
    // skipped (left null) -- including its external type id -- rather than read.
    @Test
    void externalTypeIdFilteredByPublicView() throws Exception {
        Wrapper r = MAPPER.readerWithView(PublicView.class)
                .forType(Wrapper.class)
                .readValue(JSON_TYPEID_FIRST);

        assertEquals("hello", r.data.label);
        // Admin-only "asset" must not be populated when reading with PublicView
        assertNull(r.data.asset);
    }

    // As above, but with the type id appearing after the value in the input.
    @Test
    void externalTypeIdFilteredByPublicView_valueBeforeTypeId() throws Exception {
        Wrapper r = MAPPER.readerWithView(PublicView.class)
                .forType(Wrapper.class)
                .readValue(JSON_VALUE_FIRST);

        assertEquals("hello", r.data.label);
        assertNull(r.data.asset);
    }

    // Sanity check: with AdminView the property IS visible and read normally.
    @Test
    void externalTypeIdVisibleForAdminView() throws Exception {
        Wrapper r = MAPPER.readerWithView(AdminView.class)
                .forType(Wrapper.class)
                .readValue(JSON_TYPEID_FIRST);

        assertEquals("hello", r.data.label);
        AdminAsset asset = (AdminAsset) r.data.asset;
        assertEquals("foo", asset.name);
        assertEquals("LEAKED", asset.secret);
    }

    // Same as above but for the bean-based (default-constructor) path.
    @Test
    void externalTypeIdFilteredByPublicView_beanBased() throws Exception {
        BeanWrapper r = MAPPER.readerWithView(PublicView.class)
                .forType(BeanWrapper.class)
                .readValue(JSON_TYPEID_FIRST);

        assertEquals("hello", r.data.label);
        assertNull(r.data.asset);
    }

    // Bean-based path with the type id appearing after the value.
    @Test
    void externalTypeIdFilteredByPublicView_beanBased_valueBeforeTypeId() throws Exception {
        BeanWrapper r = MAPPER.readerWithView(PublicView.class)
                .forType(BeanWrapper.class)
                .readValue(JSON_VALUE_FIRST);

        assertEquals("hello", r.data.label);
        assertNull(r.data.asset);
    }

    @Test
    void externalTypeIdVisibleForAdminView_beanBased() throws Exception {
        BeanWrapper r = MAPPER.readerWithView(AdminView.class)
                .forType(BeanWrapper.class)
                .readValue(JSON_TYPEID_FIRST);

        assertEquals("hello", r.data.label);
        AdminAsset asset = (AdminAsset) r.data.asset;
        assertEquals("foo", asset.name);
        assertEquals("LEAKED", asset.secret);
    }

    // Bean-based, AdminView, value-before-type-id: confirms the eager-bind path
    // still binds the value correctly when the property IS visible.
    @Test
    void externalTypeIdVisibleForAdminView_beanBased_valueBeforeTypeId() throws Exception {
        BeanWrapper r = MAPPER.readerWithView(AdminView.class)
                .forType(BeanWrapper.class)
                .readValue(JSON_VALUE_FIRST);

        assertEquals("hello", r.data.label);
        AdminAsset asset = (AdminAsset) r.data.asset;
        assertEquals("foo", asset.name);
        assertEquals("LEAKED", asset.secret);
    }

    // When a view-filtered external property is skipped, its value and its type id
    // must be fully consumed -- not left dangling as "unknown properties". Verify by
    // reading with FAIL_ON_UNKNOWN_PROPERTIES enabled: a leak would throw here.
    @Test
    void filteredExternalTypeIdIsFullyConsumed() throws Exception {
        ObjectMapper strict = jsonMapperBuilder()
                .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
                .build();

        for (String json : new String[] { JSON_TYPEID_FIRST, JSON_VALUE_FIRST }) {
            // creator-based
            Wrapper w = strict.readerWithView(PublicView.class)
                    .forType(Wrapper.class)
                    .readValue(json);
            assertEquals("hello", w.data.label);
            assertNull(w.data.asset);

            // bean-based
            BeanWrapper bw = strict.readerWithView(PublicView.class)
                    .forType(BeanWrapper.class)
                    .readValue(json);
            assertEquals("hello", bw.data.label);
            assertNull(bw.data.asset);
        }
    }
}