JsonInclude1757Test.java

package tools.jackson.databind.ser.filter;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.JsonInclude;

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

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

// [databind#1757]: Global NON_DEFAULT should use real bean defaults
//  for round-trip consistency when USE_REAL_NON_DEFAULT is enabled
public class JsonInclude1757Test extends DatabindTestUtil
{
    static class Entity {
        private String someFieldWithDefault = "a default";

        public void setSomeFieldWithDefault(String someFieldWithDefault) {
            this.someFieldWithDefault = someFieldWithDefault;
        }

        public String getSomeFieldWithDefault() {
            return someFieldWithDefault;
        }
    }

    static class IntDefaultEntity {
        public int value = 42;
    }

    // [databind#1757]: default values should round-trip with global NON_DEFAULT
    @Test
    public void testGlobalNonDefaultRoundTrip() throws Exception {
        ObjectMapper mapper = jsonMapperBuilder()
                .enable(MapperFeature.USE_REAL_INCLUDE_NON_DEFAULT)
                .changeDefaultPropertyInclusion(incl ->
                        incl.withValueInclusion(JsonInclude.Include.NON_DEFAULT))
                .build();

        // Serializing a default instance should produce {}
        Entity entity = new Entity();
        String json = mapper.writeValueAsString(entity);
        assertEquals("{}", json);

        // Deserializing {} should restore the default value
        Entity deserialized = mapper.readValue(json, Entity.class);
        assertEquals("a default", deserialized.getSomeFieldWithDefault());
    }

    // [databind#1757]: non-default values should be included
    @Test
    public void testGlobalNonDefaultIncludesNonDefault() throws Exception {
        ObjectMapper mapper = jsonMapperBuilder()
                .enable(MapperFeature.USE_REAL_INCLUDE_NON_DEFAULT)
                .changeDefaultPropertyInclusion(incl ->
                        incl.withValueInclusion(JsonInclude.Include.NON_DEFAULT))
                .build();

        Entity entity = new Entity();
        entity.setSomeFieldWithDefault("custom value");
        String json = mapper.writeValueAsString(entity);
        assertEquals(a2q("{'someFieldWithDefault':'custom value'}"), json);
    }

    // [databind#1757]: empty string should not be suppressed if it's not the default
    @Test
    public void testGlobalNonDefaultEmptyStringNonDefault() throws Exception {
        ObjectMapper mapper = jsonMapperBuilder()
                .enable(MapperFeature.USE_REAL_INCLUDE_NON_DEFAULT)
                .changeDefaultPropertyInclusion(incl ->
                        incl.withValueInclusion(JsonInclude.Include.NON_DEFAULT))
                .build();

        Entity entity = new Entity();
        entity.setSomeFieldWithDefault("");
        String json = mapper.writeValueAsString(entity);
        // Empty string is NOT the default ("a default"), so it should be included
        assertEquals(a2q("{'someFieldWithDefault':''}"), json);
    }

    // [databind#1757]: null should be suppressed with global NON_DEFAULT
    @Test
    public void testGlobalNonDefaultSuppressesNull() throws Exception {
        ObjectMapper mapper = jsonMapperBuilder()
                .enable(MapperFeature.USE_REAL_INCLUDE_NON_DEFAULT)
                .changeDefaultPropertyInclusion(incl ->
                        incl.withValueInclusion(JsonInclude.Include.NON_DEFAULT))
                .build();

        Entity entity = new Entity();
        entity.setSomeFieldWithDefault(null);
        String json = mapper.writeValueAsString(entity);
        assertEquals("{}", json);
    }

    // [databind#1757]: global NON_DEFAULT with primitive field having non-zero default
    @Test
    public void testGlobalNonDefaultPrimitiveField() throws Exception {
        ObjectMapper mapper = jsonMapperBuilder()
                .enable(MapperFeature.USE_REAL_INCLUDE_NON_DEFAULT)
                .changeDefaultPropertyInclusion(incl ->
                        incl.withValueInclusion(JsonInclude.Include.NON_DEFAULT))
                .build();

        // Default value (42) should be suppressed
        IntDefaultEntity entity = new IntDefaultEntity();
        assertEquals("{}", mapper.writeValueAsString(entity));

        // Non-default value should be included
        entity.value = 0;
        assertEquals(a2q("{'value':0}"), mapper.writeValueAsString(entity));

        // Another non-default value
        entity.value = 99;
        assertEquals(a2q("{'value':99}"), mapper.writeValueAsString(entity));
    }

    // [databind#1757]: global NON_DEFAULT should match per-class NON_DEFAULT behavior
    @Test
    public void testGlobalMatchesPerClassBehavior() throws Exception {
        ObjectMapper globalMapper = jsonMapperBuilder()
                .enable(MapperFeature.USE_REAL_INCLUDE_NON_DEFAULT)
                .changeDefaultPropertyInclusion(incl ->
                        incl.withValueInclusion(JsonInclude.Include.NON_DEFAULT))
                .build();

        ObjectMapper defaultMapper = newJsonMapper();

        // Per-class annotated bean
        AnnotatedEntity annotated = new AnnotatedEntity();
        String perClassJson = defaultMapper.writeValueAsString(annotated);

        // Global NON_DEFAULT bean (same structure, no annotation)
        Entity global = new Entity();
        String globalJson = globalMapper.writeValueAsString(global);

        // Both should produce {}
        assertEquals("{}", perClassJson);
        assertEquals("{}", globalJson);
    }

    // [databind#1757]: Without USE_REAL_NON_DEFAULT, old behavior is preserved
    @Test
    public void testOldBehaviorWithoutFeature() throws Exception {
        ObjectMapper mapper = jsonMapperBuilder()
                // USE_REAL_NON_DEFAULT is disabled by default
                .changeDefaultPropertyInclusion(incl ->
                        incl.withValueInclusion(JsonInclude.Include.NON_DEFAULT))
                .build();

        // With old behavior (Mode 2), String type default is "".
        // "a default" != "" so the field IS included.
        Entity entity = new Entity();
        String json = mapper.writeValueAsString(entity);
        assertEquals(a2q("{'someFieldWithDefault':'a default'}"), json);
    }

    @JsonInclude(JsonInclude.Include.NON_DEFAULT)
    static class AnnotatedEntity {
        private String someFieldWithDefault = "a default";

        public void setSomeFieldWithDefault(String v) {
            this.someFieldWithDefault = v;
        }

        public String getSomeFieldWithDefault() {
            return someFieldWithDefault;
        }
    }
}