ApplyViewSerializationTest.java

package tools.jackson.databind.views;

import java.io.StringWriter;
import java.util.Map;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.JsonApplyView;
import com.fasterxml.jackson.annotation.JsonView;

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

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

//[databind#5745] : allow overriding JsonView

/**
 * Unit tests for verifying JSON apply view functionality.
 */
public class ApplyViewSerializationTest extends DatabindTestUtil
{
    // Classes that represent views
    static class ViewA { }
    static class ViewAA extends ViewA { }
    static class ViewB { }

    static class Bean
    {
        @JsonView(ViewA.class)
        public String a = "1";

        @JsonView({ViewAA.class, ViewB.class})
        public String aa = "2";

        @JsonView(ViewB.class)
        public String getB() { return "3"; }
    }

    static class Bean2 {

        @JsonView(ViewA.class)
        @JsonApplyView(ViewB.class)
        public Bean beanWithApplyViewB = new Bean();

        @JsonView(ViewA.class)
        @JsonApplyView(JsonApplyView.NONE.class)
        public Bean beanWithApplyNoneView = new Bean();
    }

    static class Bean3 {

        @JsonView(ViewA.class)
        public Bean bean = new Bean();

        @JsonView(ViewA.class)
        public Bean2 bean2 = new Bean2();

        @JsonView(ViewA.class)
        @JsonApplyView(ViewB.class)
        public Bean2 bean2WithApplyViewB = new Bean2();
    }

    // Used to verify that an inner @JsonApplyView overrides an outer one:
    // outer writer activates ViewB; this field's @JsonView(ViewB) lets it through,
    // and its @JsonApplyView(ViewA) switches the active view to ViewA for the
    // nested Bean2 ��� without which both Bean2 fields (each @JsonView(ViewA))
    // would be filtered out by ViewB.
    static class Bean4 {

        @JsonView(ViewB.class)
        @JsonApplyView(ViewA.class)
        public Bean2 wrapped = new Bean2();
    }

    /*
    /**********************************************************
    /* Unit tests
    /**********************************************************
     */

    // Ensure `MapperFeature.DEFAULT_VIEW_INCLUSION` is enabled
    // (its default differs b/w Jackson 2.x and 3.x)
    private final ObjectMapper MAPPER = jsonMapperBuilder()
            .enable(MapperFeature.DEFAULT_VIEW_INCLUSION)
            .build();

    // Mapper using the 3.x default (DEFAULT_VIEW_INCLUSION disabled), used to
    // confirm @JsonApplyView semantics do not depend on this feature flag.
    private final ObjectMapper MAPPER_NO_DEFAULT_INCLUSION = jsonMapperBuilder()
            .disable(MapperFeature.DEFAULT_VIEW_INCLUSION)
            .build();

    @SuppressWarnings("unchecked")
    @Test
    public void testJsonApplyView() {
        // With "ViewA" active: both Bean2 properties are @JsonView(ViewA), so both
        // serialize, but each applies its own override view to the nested Bean.
        Bean2 bean2 = new Bean2();

        StringWriter sw = new StringWriter();
        MAPPER.writerWithView(ViewA.class).writeValue(sw, bean2);

        Map<?,?> bean2Map = MAPPER.readValue(sw.toString(), Map.class);
        assertEquals(2, bean2Map.size());

        Map<String,Object> beanWithApplyViewBMap = (Map<String, Object>) bean2Map.get("beanWithApplyViewB");
        assertEquals(2, beanWithApplyViewBMap.size());
        assertFalse(beanWithApplyViewBMap.containsKey("a"));
        assertEquals("2", beanWithApplyViewBMap.get("aa"));
        assertEquals("3", beanWithApplyViewBMap.get("b"));

        Map<String,Object> beanWithApplyNoneViewMap = (Map<String, Object>) bean2Map.get("beanWithApplyNoneView");
        assertEquals(3, beanWithApplyNoneViewMap.size());
        assertEquals("1", beanWithApplyNoneViewMap.get("a"));
        assertEquals("2", beanWithApplyNoneViewMap.get("aa"));
        assertEquals("3", beanWithApplyNoneViewMap.get("b"));
    }

    @SuppressWarnings("unchecked")
    @Test
    public void testJsonApplyViewNested() {
        // With "ViewAA" active (extends ViewA): all three Bean3 properties are
        // @JsonView(ViewA), so all serialize; verify nested @JsonApplyView still applies.
        Bean3 bean3 = new Bean3();

        StringWriter sw = new StringWriter();
        MAPPER.writerWithView(ViewAA.class).writeValue(sw, bean3);

        Map<?,?> bean3Map = MAPPER.readValue(sw.toString(), Map.class);
        assertEquals(3, bean3Map.size());

        Map<String,Object> beanMap = (Map<String, Object>) bean3Map.get("bean");
        assertEquals(2, beanMap.size());
        assertEquals("1", beanMap.get("a"));
        assertEquals("2", beanMap.get("aa"));
        assertFalse(beanMap.containsKey("b"));

        Map<String,Object> bean2Map = (Map<String, Object>) bean3Map.get("bean2");
        assertEquals(2, bean2Map.size());
        Map<String,Object> nestedBeanWithApplyViewBMap = (Map<String, Object>) bean2Map.get("beanWithApplyViewB");
        assertEquals(2, nestedBeanWithApplyViewBMap.size());
        assertFalse(nestedBeanWithApplyViewBMap.containsKey("a"));
        assertEquals("2", nestedBeanWithApplyViewBMap.get("aa"));
        assertEquals("3", nestedBeanWithApplyViewBMap.get("b"));

        // NONE applyView reached via nested Bean2: all three Bean properties present
        Map<String,Object> nestedBeanWithApplyNoneViewMap = (Map<String, Object>) bean2Map.get("beanWithApplyNoneView");
        assertEquals(3, nestedBeanWithApplyNoneViewMap.size());
        assertEquals("1", nestedBeanWithApplyNoneViewMap.get("a"));
        assertEquals("2", nestedBeanWithApplyNoneViewMap.get("aa"));
        assertEquals("3", nestedBeanWithApplyNoneViewMap.get("b"));

        Map<?,?> bean2WithApplyViewBMap = (Map<?,?>) bean3Map.get("bean2WithApplyViewB");
        assertEquals(0, bean2WithApplyViewBMap.size());
    }

    // Verifies an inner @JsonApplyView overrides an outer one: writer is ViewB,
    // Bean4.wrapped switches the active view to ViewA, then Bean2's own
    // @JsonApplyView re-fires for the deepest Bean.
    @SuppressWarnings("unchecked")
    @Test
    public void testJsonApplyViewOverridesOuter() {
        Bean4 bean4 = new Bean4();

        StringWriter sw = new StringWriter();
        MAPPER.writerWithView(ViewB.class).writeValue(sw, bean4);

        Map<?,?> bean4Map = MAPPER.readValue(sw.toString(), Map.class);
        assertEquals(1, bean4Map.size());

        Map<String,Object> wrappedMap = (Map<String, Object>) bean4Map.get("wrapped");
        // Both Bean2 fields are @JsonView(ViewA): they only survive because Bean4
        // applied ViewA, not the writer's ViewB.
        assertEquals(2, wrappedMap.size());

        // Inner @JsonApplyView(ViewB) wins over outer ViewA for the deepest Bean
        Map<String,Object> deepWithApplyB = (Map<String, Object>) wrappedMap.get("beanWithApplyViewB");
        assertEquals(2, deepWithApplyB.size());
        assertFalse(deepWithApplyB.containsKey("a"));
        assertEquals("2", deepWithApplyB.get("aa"));
        assertEquals("3", deepWithApplyB.get("b"));

        // Inner @JsonApplyView(NONE) wins over outer ViewA: all three properties
        Map<String,Object> deepWithApplyNone = (Map<String, Object>) wrappedMap.get("beanWithApplyNoneView");
        assertEquals(3, deepWithApplyNone.size());
        assertEquals("1", deepWithApplyNone.get("a"));
        assertEquals("2", deepWithApplyNone.get("aa"));
        assertEquals("3", deepWithApplyNone.get("b"));
    }

    // Same scenario as testJsonApplyView but with DEFAULT_VIEW_INCLUSION disabled
    // (3.x default). All test properties are explicitly @JsonView-annotated, so
    // the feature must not affect outcomes; in particular NONE still yields all 3.
    @SuppressWarnings("unchecked")
    @Test
    public void testJsonApplyViewWithoutDefaultInclusion() {
        Bean2 bean2 = new Bean2();

        StringWriter sw = new StringWriter();
        MAPPER_NO_DEFAULT_INCLUSION.writerWithView(ViewA.class).writeValue(sw, bean2);

        Map<?,?> bean2Map = MAPPER_NO_DEFAULT_INCLUSION.readValue(sw.toString(), Map.class);
        assertEquals(2, bean2Map.size());

        Map<String,Object> beanWithApplyViewBMap = (Map<String, Object>) bean2Map.get("beanWithApplyViewB");
        assertEquals(2, beanWithApplyViewBMap.size());
        assertFalse(beanWithApplyViewBMap.containsKey("a"));
        assertEquals("2", beanWithApplyViewBMap.get("aa"));
        assertEquals("3", beanWithApplyViewBMap.get("b"));

        Map<String,Object> beanWithApplyNoneViewMap = (Map<String, Object>) bean2Map.get("beanWithApplyNoneView");
        assertEquals(3, beanWithApplyNoneViewMap.size());
        assertEquals("1", beanWithApplyNoneViewMap.get("a"));
        assertEquals("2", beanWithApplyNoneViewMap.get("aa"));
        assertEquals("3", beanWithApplyNoneViewMap.get("b"));
    }
}