PropertyAliasTest.java

package tools.jackson.databind.deser;

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.json.JsonMapper;

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

import static tools.jackson.databind.testutil.DatabindTestUtil.a2q;
import static tools.jackson.databind.testutil.DatabindTestUtil.newJsonMapper;

public class PropertyAliasTest
{
    static class AliasBean {
        @JsonAlias({ "nm", "Name" })
        public String name;

        int _xyz;

        int _a;

        @JsonCreator
        public AliasBean(@JsonProperty("a")
            @JsonAlias("A") int a) {
            _a = a;
        }

        @JsonAlias({ "Xyz" })
        public void setXyz(int x) {
            _xyz = x;
        }
    }

    static class AliasBean2378 {
        String partitionId;
        String _id;

        private AliasBean2378(boolean bogus, String partId, String userId) {
            partitionId = partId;
            _id = userId;
        }

        @JsonCreator
        public static AliasBean2378 create(@JsonProperty("partitionId") String partId,
                @JsonProperty("id") @JsonAlias("userId") String userId) {
            return new AliasBean2378(false, partId, userId);
        }
    }

    static class PolyWrapperForAlias {
        @JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
                include = JsonTypeInfo.As.WRAPPER_ARRAY)
        @JsonSubTypes({
            @JsonSubTypes.Type(value = AliasBean.class,name = "ab"),
        })
        public Object value;

        protected PolyWrapperForAlias() { }
        public PolyWrapperForAlias(Object v) { value = v; }
    }

    // [databind#2669]
    static class Pojo2669 {
        @JsonAlias({"nick", "name"})
        private String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }

    // [databind#6031]: a @JsonIgnore getter whose implicit name collides with a
    // creator parameter's @JsonAlias must not suppress that alias
    static class Bean6031 {
        @JsonProperty("newName")
        private final String value;

        @JsonCreator
        public Bean6031(@JsonProperty("newName") @JsonAlias("oldName") String value) {
            this.value = value;
        }

        public String getValue() { return value; }

        @JsonIgnore
        public String getOldName() { return value; }
    }

    // [databind#6031]: same as above, but exercising the record-update path
    // (`_deserializeRecordForUpdate`) where the creator-property ignore check lives
    // separately from the regular property-based path
    public record Record6031(@JsonProperty("newName") @JsonAlias("oldName") String value) {
        @JsonIgnore
        public String getOldName() { return value; }
    }

    // [databind#6031]: class-level @JsonIgnoreProperties is absolute and must keep
    // suppressing the name even when it coincides with a creator parameter's
    // @JsonAlias (unlike per-property @JsonIgnore, which yields to the alias)
    @JsonIgnoreProperties("oldName")
    static class ClassIgnoreBean6031 {
        @JsonProperty("newName")
        private final String value;

        @JsonCreator
        public ClassIgnoreBean6031(@JsonProperty("newName") @JsonAlias("oldName") String value) {
            this.value = value;
        }

        public String getValue() { return value; }
    }

    // [databind#6031]: multiple aliases on one creator param, and multiple creator
    // params each with an alias colliding with its own @JsonIgnore getter
    static class MultiAliasBean6031 {
        @JsonProperty("aName")
        private final String a;
        @JsonProperty("bName")
        private final String b;

        @JsonCreator
        public MultiAliasBean6031(
                @JsonProperty("aName") @JsonAlias({ "a1", "a2" }) String a,
                @JsonProperty("bName") @JsonAlias("b1") String b) {
            this.a = a;
            this.b = b;
        }

        public String getA() { return a; }
        public String getB() { return b; }

        @JsonIgnore
        public String getA1() { return a; }
        @JsonIgnore
        public String getA2() { return a; }
        @JsonIgnore
        public String getB1() { return b; }
    }

    /*
    /**********************************************************************
    /* Test methods
    /**********************************************************************
     */

    private final ObjectMapper MAPPER = newJsonMapper();

    // [databind#1029]
    @Test
    public void testSimpleAliases() throws Exception
    {
        AliasBean bean;

        // first, one indicated by field annotation, set via field
        bean = MAPPER.readValue(a2q("{'Name':'Foobar','a':3,'xyz':37}"),
                AliasBean.class);
        assertEquals("Foobar", bean.name);
        assertEquals(3, bean._a);
        assertEquals(37, bean._xyz);

        // then method-bound one
        bean = MAPPER.readValue(a2q("{'name':'Foobar','a':3,'Xyz':37}"),
                AliasBean.class);
        assertEquals("Foobar", bean.name);
        assertEquals(3, bean._a);
        assertEquals(37, bean._xyz);

        // and finally, constructor-backed one
        bean = MAPPER.readValue(a2q("{'name':'Foobar','A':3,'xyz':37}"),
                AliasBean.class);
        assertEquals("Foobar", bean.name);
        assertEquals(3, bean._a);
        assertEquals(37, bean._xyz);
    }

    @Test
    public void testAliasWithPolymorphic() throws Exception
    {
        PolyWrapperForAlias value = MAPPER.readValue(a2q(
                "{'value': ['ab', {'nm' : 'Bob', 'A' : 17} ] }"
                ), PolyWrapperForAlias.class);
        assertNotNull(value.value);
        AliasBean bean = (AliasBean) value.value;
        assertEquals("Bob", bean.name);
        assertEquals(17, bean._a);
    }

    // [databind#2378]
    @Test
    public void testAliasInFactoryMethod() throws Exception
    {
        AliasBean2378 bean = MAPPER.readValue(a2q(
                "{'partitionId' : 'a', 'userId' : '123'}"
                ), AliasBean2378.class);
        assertEquals("a", bean.partitionId);
        assertEquals("123", bean._id);
    }

    // [databind#2669]
    @Test
    public void testCaseInsensitiveAliases() throws Exception {

        ObjectMapper mapper = JsonMapper.builder()
                .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES)
                .build();

        String text = "{\"name\":\"test\"}";
        Pojo2669 pojo = mapper.readValue(text, Pojo2669.class);
        assertNotNull(pojo);
        assertEquals("test", pojo.getName());
    }

    static class FixedOrderAliasBean {
        @JsonAlias({"a", "b", "c"})
        public String value;
    }

    @Test
    public void testAliasDeserializedToLastMatchingKey_ascendingKeys() throws Exception {
        String ascendingOrderInput = a2q(
            "{\"a\": \"a-value\", " +
                "\"b\": \"b-value\", " +
                "\"c\": \"c-value\"}");

        FixedOrderAliasBean ascObj = MAPPER.readValue(ascendingOrderInput, FixedOrderAliasBean.class);
        assertEquals("c-value", ascObj.value);
    }

    @Test
    public void testAliasDeserializedToLastMatchingKey_descendingKeys() throws Exception {
        String descendingOrderInput = a2q(
            "{\"c\": \"c-value\", " +
                "\"b\": \"b-value\", " +
                "\"a\": \"a-value\"}");

        FixedOrderAliasBean descObj = MAPPER.readValue(descendingOrderInput, FixedOrderAliasBean.class);
        assertEquals("a-value", descObj.value);
    }

    static class AscendingOrderAliasBean {
        @JsonAlias({"a", "b", "c"})
        public String value;
    }

    @Test
    public void testAliasDeserializedToLastMatchingKey_ascendingAliases() throws Exception {
        String input = a2q(
                "{\"a\": \"a-value\", " +
                "\"b\": \"b-value\", " +
                "\"c\": \"c-value\"}");

        AscendingOrderAliasBean ascObj = MAPPER.readValue(input, AscendingOrderAliasBean.class);
        assertEquals("c-value", ascObj.value);
    }

    static class DescendingOrderAliasBean {
        @JsonAlias({"c", "b", "a"})
        public String value;
    }

    @Test
    public void testAliasDeserializedToLastMatchingKey_descendingAliases() throws Exception {
        String input = a2q(
            "{\"a\": \"a-value\", " +
                "\"b\": \"b-value\", " +
                "\"c\": \"c-value\"}");

        DescendingOrderAliasBean descObj = MAPPER.readValue(input, DescendingOrderAliasBean.class);
        assertEquals("c-value", descObj.value);
    }

    static class AliasTestBeanA {
        @JsonAlias({"fullName"})
        public String name;

        @JsonAlias({"fullName"})
        public String fullName;
    }

    @Test
    public void testAliasFallBackToField() throws Exception {
        AliasTestBeanA obj = MAPPER.readValue(a2q(
            "{\"fullName\": \"Faster Jackson\", \"name\":\"Jackson\"}"
        ), AliasTestBeanA.class);

        assertEquals("Jackson", obj.name);
        assertEquals("Faster Jackson", obj.fullName);
    }

    // [databind#6031]
    @Test
    public void testAliasOnCreatorWithIgnoredGetter() throws Exception {
        Bean6031 result = MAPPER.readValue(a2q("{'oldName':'hello'}"), Bean6031.class);
        assertEquals("hello", result.getValue());

        // and the primary name must still work
        result = MAPPER.readValue(a2q("{'newName':'hello'}"), Bean6031.class);
        assertEquals("hello", result.getValue());
    }

    // [databind#6031]: same defect on the record-update path (`_deserializeRecordForUpdate`)
    @Test
    public void testAliasOnRecordUpdateWithIgnoredGetter() throws Exception {
        Record6031 orig = new Record6031("orig");
        Record6031 result = MAPPER.readerForUpdating(orig)
                .readValue(a2q("{'oldName':'hello'}"));
        assertEquals("hello", result.value());

        // and the primary name must still work
        result = MAPPER.readerForUpdating(orig)
                .readValue(a2q("{'newName':'hello'}"));
        assertEquals("hello", result.value());
    }

    // [databind#6031]: the @JsonIgnore getter whose implicit name is the alias
    // ("oldName") must not leak into serialization output
    @Test
    public void testNoAliasNameInSerialization() throws Exception {
        assertEquals(a2q("{'newName':'hello'}"),
                MAPPER.writeValueAsString(new Bean6031("hello")));
        assertEquals(a2q("{'newName':'hello'}"),
                MAPPER.writeValueAsString(new Record6031("hello")));
    }

    // [databind#6031]: class-level @JsonIgnoreProperties stays absolute; unlike a
    // per-property @JsonIgnore it must keep suppressing the name even when it is a
    // creator parameter's alias. So "oldName" is dropped (value stays null) while
    // the primary name still binds.
    @Test
    public void testClassLevelIgnoreNotRescuedByAlias() throws Exception {
        ClassIgnoreBean6031 result = MAPPER.readValue(a2q("{'oldName':'hello'}"),
                ClassIgnoreBean6031.class);
        assertNull(result.getValue());

        result = MAPPER.readValue(a2q("{'newName':'hello'}"), ClassIgnoreBean6031.class);
        assertEquals("hello", result.getValue());
    }

    // [databind#6031]: every alias must be rescued ��� across multiple aliases on a
    // single creator param and across multiple creator params
    @Test
    public void testMultipleAliasesWithIgnoredGetters() throws Exception {
        // first alias of the multi-alias param
        MultiAliasBean6031 result = MAPPER.readValue(a2q("{'a1':'AA','b1':'BB'}"),
                MultiAliasBean6031.class);
        assertEquals("AA", result.getA());
        assertEquals("BB", result.getB());

        // second alias of the same param
        result = MAPPER.readValue(a2q("{'a2':'AA','b1':'BB'}"), MultiAliasBean6031.class);
        assertEquals("AA", result.getA());
        assertEquals("BB", result.getB());

        // and primary names still work
        result = MAPPER.readValue(a2q("{'aName':'AA','bName':'BB'}"), MultiAliasBean6031.class);
        assertEquals("AA", result.getA());
        assertEquals("BB", result.getB());
    }
}