RecordIgnoreGettersTest.java

package tools.jackson.databind.records;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.annotation.PropertyAccessor;

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

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

/**
 * Tests for {@link MapperFeature#INFER_RECORD_GETTERS_FROM_COMPONENTS_ONLY}
 * which controls whether only Record component getters are auto-detected
 * or if all JavaBean-style getters are detected (backward compatible behavior).
 * Also covers non-component getter filtering from interface implementations.
 */
public class RecordIgnoreGettersTest extends DatabindTestUtil
{
    // Test Case 1: Basic record with helper getter
    record PersonRecord(String name, int age) {
        // Helper method that is NOT a record component
        public String getDisplayName() {
            return name.toUpperCase();
        }
    }

    // Test Case 2: Record implementing interface with getter
    interface Identifiable {
        String getId();
    }

    @JsonPropertyOrder({"name", "id"})
    record UserRecord(String name) implements Identifiable {
        @Override
        public String getId() {
            return "ID:" + name;
        }
    }

    // Test Case 3: Record with explicit annotation on helper method
    record AnnotatedHelperRecord(String name) {
        @JsonProperty("display")
        public String getDisplayName() {
            return name.toUpperCase();
        }
    }

    // Test Case 4: Record with is-getter helper
    record BooleanHelperRecord(String name, boolean active) {
        // Helper method - not a component
        public boolean isSpecial() {
            return name.startsWith("Special");
        }
    }

    // Test Case 5: Record with both component getter and helper
    record MixedRecord(int value) {
        // Component accessor - should always work
        @Override
        public int value() {
            return value;
        }

        // Helper - behavior depends on feature
        public int getDoubleValue() {
            return value * 2;
        }
    }

    // Test Case 6: Empty record with static getter
    record EmptyWithStatic() {
        public static String getStaticValue() {
            return "static";
        }
    }

    // [databind#3628] Record implementing interface with multiple getters
    interface InterfaceWithGetter {
        String getId();
        String getName();
    }

    @JsonPropertyOrder({"id", "name", "count"}) // easier to assert when JSON field ordering is always the same
    record RecordWithInterfaceWithGetter(String name) implements InterfaceWithGetter {

        @Override
        public String getId() {
            return "ID:" + name;
        }

        @Override
        public String getName() {
            return name;
        }

        // [databind#3895]
        public int getCount() {
            return 999;
        }
    }

    private final ObjectMapper MAPPER_DEFAULT = newJsonMapper();

    private final ObjectMapper MAPPER_RESTRICTED = jsonMapperBuilder()
            .enable(MapperFeature.INFER_RECORD_GETTERS_FROM_COMPONENTS_ONLY)
            .build();

    /*
    /**********************************************************************
    /* Test methods, INFER_RECORD_GETTERS_FROM_COMPONENTS_ONLY feature [databind#4157]
    /**********************************************************************
     */

    @Test
    public void testFeatureDisabledByDefault() throws Exception {
        assertFalse(MAPPER_DEFAULT.isEnabled(MapperFeature.INFER_RECORD_GETTERS_FROM_COMPONENTS_ONLY));
    }

    @Test
    public void testHelperGetterIncluded_FeatureDisabled() throws Exception {
        PersonRecord person = new PersonRecord("john", 30);
        String json = MAPPER_DEFAULT.writeValueAsString(person);

        assertTrue(json.contains("displayName"), "Should include displayName with feature disabled");
        assertTrue(json.contains("JOHN"), "Should have uppercase name");
        assertTrue(json.contains("name"), "Should include actual component");
        assertTrue(json.contains("age"), "Should include actual component");
    }

    @Test
    public void testHelperGetterExcluded_FeatureEnabled() throws Exception {
        PersonRecord person = new PersonRecord("john", 30);
        String json = MAPPER_RESTRICTED.writeValueAsString(person);

        assertFalse(json.contains("displayName"), "Should NOT include displayName with feature enabled");
        assertFalse(json.contains("JOHN"), "Should NOT have uppercase name");
        assertTrue(json.contains("name"), "Should include actual component");
        assertTrue(json.contains("john"), "Should have original name");
        assertTrue(json.contains("age"), "Should include actual component");
    }

    @Test
    public void testInterfaceGetterExcluded_FeatureEnabled() throws Exception {
        UserRecord user = new UserRecord("alice");
        String json = MAPPER_RESTRICTED.writeValueAsString(user);

        assertEquals(a2q("{'name':'alice'}"), json);
        assertFalse(json.contains("id"), "Should NOT include interface getter");
    }

    @Test
    public void testInterfaceGetterIncluded_FeatureDisabled() throws Exception {
        UserRecord user = new UserRecord("alice");
        String json = MAPPER_DEFAULT.writeValueAsString(user);

        assertTrue(json.contains("id"), "Should include interface getter");
        assertTrue(json.contains("ID:alice"), "Should have computed id");
    }

    @Test
    public void testExplicitAnnotation_AlwaysWorks_FeatureEnabled() throws Exception {
        AnnotatedHelperRecord record = new AnnotatedHelperRecord("test");
        String json = MAPPER_RESTRICTED.writeValueAsString(record);

        assertTrue(json.contains("display"), "Explicit @JsonProperty should always work");
        assertTrue(json.contains("TEST"), "Should have uppercase value");
    }

    @Test
    public void testExplicitAnnotation_AlwaysWorks_FeatureDisabled() throws Exception {
        AnnotatedHelperRecord record = new AnnotatedHelperRecord("test");
        String json = MAPPER_DEFAULT.writeValueAsString(record);

        assertTrue(json.contains("display"), "Explicit @JsonProperty should always work");
        assertTrue(json.contains("TEST"), "Should have uppercase value");
    }

    @Test
    public void testIsGetterHelper_FeatureEnabled() throws Exception {
        BooleanHelperRecord record = new BooleanHelperRecord("Special Case", true);
        String json = MAPPER_RESTRICTED.writeValueAsString(record);

        assertTrue(json.contains("active"), "Should include actual boolean component");
        assertFalse(json.contains("special"), "Should NOT include is-getter helper");
    }

    @Test
    public void testIsGetterHelper_FeatureDisabled() throws Exception {
        BooleanHelperRecord record = new BooleanHelperRecord("Special Case", true);
        String json = MAPPER_DEFAULT.writeValueAsString(record);

        assertTrue(json.contains("active"), "Should include actual boolean component");
        assertTrue(json.contains("special"), "Should include is-getter helper with feature disabled");
    }

    @Test
    public void testComponentAccessor_AlwaysWorks() throws Exception {
        MixedRecord record = new MixedRecord(42);

        String jsonRestricted = MAPPER_RESTRICTED.writeValueAsString(record);
        assertTrue(jsonRestricted.contains("value"), "Component should be included");
        assertTrue(jsonRestricted.contains("42"), "Should have value 42");
        assertFalse(jsonRestricted.contains("doubleValue"), "Helper should be excluded");

        String jsonDefault = MAPPER_DEFAULT.writeValueAsString(record);
        assertTrue(jsonDefault.contains("value"), "Component should be included");
        assertTrue(jsonDefault.contains("doubleValue"), "Helper should be included");
        assertTrue(jsonDefault.contains("84"), "Should have doubled value");
    }

    @Test
    public void testRoundTrip_FeatureEnabled() throws Exception {
        PersonRecord original = new PersonRecord("alice", 25);

        String json = MAPPER_RESTRICTED.writeValueAsString(original);
        PersonRecord deserialized = MAPPER_RESTRICTED.readValue(json, PersonRecord.class);

        assertEquals(original, deserialized, "Round-trip should preserve data");
        assertEquals("alice", deserialized.name());
        assertEquals(25, deserialized.age());
    }

    @Test
    public void testDeserialization_IgnoresNonComponentProperties() throws Exception {
        String json = a2q("{'name':'bob','age':30,'displayName':'BOB'}");

        PersonRecord deserialized = MAPPER_RESTRICTED.readValue(json, PersonRecord.class);

        assertEquals("bob", deserialized.name());
        assertEquals(30, deserialized.age());
    }

    @Test
    public void testStaticGetter_NeverIncluded() throws Exception {
        EmptyWithStatic record = new EmptyWithStatic();

        assertEquals("{}", MAPPER_RESTRICTED.writeValueAsString(record));
        assertEquals("{}", MAPPER_DEFAULT.writeValueAsString(record));
    }

    @Test
    public void testFeatureConfiguration_ViaBuilder() throws Exception {
        ObjectMapper mapper = jsonMapperBuilder()
                .enable(MapperFeature.INFER_RECORD_GETTERS_FROM_COMPONENTS_ONLY)
                .build();

        assertTrue(mapper.isEnabled(MapperFeature.INFER_RECORD_GETTERS_FROM_COMPONENTS_ONLY));

        PersonRecord person = new PersonRecord("test", 1);
        String json = mapper.writeValueAsString(person);
        assertFalse(json.contains("displayName"));
    }

    @Test
    public void testFeatureConfiguration_ExplicitDisable() throws Exception {
        ObjectMapper mapper = jsonMapperBuilder()
                .disable(MapperFeature.INFER_RECORD_GETTERS_FROM_COMPONENTS_ONLY)
                .build();

        assertFalse(mapper.isEnabled(MapperFeature.INFER_RECORD_GETTERS_FROM_COMPONENTS_ONLY));

        PersonRecord person = new PersonRecord("test", 1);
        String json = mapper.writeValueAsString(person);
        assertTrue(json.contains("displayName"));
    }

    /*
    /**********************************************************************
    /* Test methods, non-component interface getters [databind#3628]
    /**********************************************************************
     */

    // [databind#3628]
    @Test
    public void testSerializeIgnoreInterfaceGetter_WithoutUsingVisibilityConfig()
    {
        String json = MAPPER_DEFAULT.writeValueAsString(new RecordWithInterfaceWithGetter("Bob"));
        assertEquals("{\"id\":\"ID:Bob\",\"name\":\"Bob\",\"count\":999}", json);
    }

    @Test
    public void testSerializeIgnoreInterfaceGetter_UsingVisibilityConfig()
    {
        final ObjectMapper mapper = jsonMapperBuilder()
                .changeDefaultVisibility(vc ->
                    vc.withVisibility(PropertyAccessor.GETTER, Visibility.NONE)
                        .withVisibility(PropertyAccessor.FIELD, Visibility.ANY)
                )
                .build();

        String json = mapper.writeValueAsString(new RecordWithInterfaceWithGetter("Bob"));

        assertEquals("{\"name\":\"Bob\"}", json);
    }

    // [databind#4157]
    @Test
    public void testWithInferGettersFromComponentsOnlyFeature()
    {
        String json = MAPPER_RESTRICTED.writeValueAsString(new RecordWithInterfaceWithGetter("Bob"));

        // With feature enabled, only the actual record component should be serialized
        assertEquals("{\"name\":\"Bob\"}", json);
    }
}