EnumAsMapKeySerializationTest.java

package tools.jackson.databind.ser.enums;

import java.util.*;

import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonValue;

import tools.jackson.core.JsonGenerator;
import tools.jackson.databind.*;
import tools.jackson.databind.annotation.JsonSerialize;
import tools.jackson.databind.cfg.EnumFeature;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.testutil.DatabindTestUtil;

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

@TestMethodOrder(MethodOrderer.MethodName.class)
public class EnumAsMapKeySerializationTest extends DatabindTestUtil
{
    static class MapBean {
        public Map<ABCEnum,Integer> map = new HashMap<>();

        public void add(ABCEnum key, int value) {
            map.put(key, Integer.valueOf(value));
        }
    }

    protected enum ABCEnum {
        A, B, C;
        private ABCEnum() { }

        @Override public String toString() { return name().toLowerCase(); }
    }

    // [databind#594]
    static enum MyEnum594 {
        VALUE_WITH_A_REALLY_LONG_NAME_HERE("longValue");

        private final String key;
        private MyEnum594(String k) { key = k; }

        @JsonValue
        public String getKey() { return key; }
    }

    static class MyStuff594 {
        public Map<MyEnum594,String> stuff = new EnumMap<MyEnum594,String>(MyEnum594.class);

        protected MyStuff594() { }
        public MyStuff594(String value) {
            stuff.put(MyEnum594.VALUE_WITH_A_REALLY_LONG_NAME_HERE, value);
        }
    }

    // [databind#661]
    static class MyBean661 {
        private Map<Foo661, String> foo = new EnumMap<Foo661, String>(Foo661.class);

        public MyBean661(String value) {
            foo.put(Foo661.FOO, value);
        }

        @JsonAnyGetter
        @JsonSerialize(keyUsing = Foo661.Serializer.class)
        public Map<Foo661, String> getFoo() {
            return foo;
        }
    }

    public enum Foo661 {
        FOO;
        public static class Serializer extends ValueSerializer<Foo661> {
            @Override
            public void serialize(Foo661 value, JsonGenerator g, SerializationContext provider)
            {
                g.writeName("X-"+value.name());
            }
        }
    }

    // [databind#2129]
    public enum Type {
        FIRST,
        SECOND;
    }

    static class TypeContainer {
        public Map<Type, Integer> values;

        public TypeContainer(Type type, int value) {
            values = Collections.singletonMap(type, value);
        }
    }

    // [databind#2457]
    enum MyEnum2457 {
        A,
        B() {
            // just to ensure subclass construction
            @Override
            public void foo() { }
        };

        // needed to force subclassing
        public void foo() { }

        @Override
        public String toString() { return name() + " as string"; }
    }

    // [databind#2457]
    enum MyEnum2457Base {
        @JsonProperty("a_base")
        A,
        @JsonProperty("b_base")
        B() {
            // just to ensure subclass construction
            @Override
            public void foo() { }
        },
        C;
        
        // needed to force sub-classing
        public void foo() { }
        
        @Override
        public String toString() { return name() + " as string"; }
    }

    // [databind#2457]
    enum MyEnum2457Mixin {
        @JsonProperty("a_mixin")
        A,
        @JsonProperty("b_mixin")
        B() {
            // just to ensure subclass construction
            @Override
            public void foo() { }
        };

        // needed to force sub=classing
        public void foo() { }

        @Override
        public String toString() { return name() + " as string"; }
    }

    // [databind#5432]
    enum Color5432 {
        @JsonProperty("red")
        RED
    }

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

    private final ObjectMapper MAPPER = jsonMapperBuilder()
            .disable(EnumFeature.WRITE_ENUMS_USING_TO_STRING)
            .build();

    @Test
    public void testMapWithEnumKeys() throws Exception
    {
        MapBean bean = new MapBean();
        bean.add(ABCEnum.B, 3);

        // By default Enums serialized using `name()`
        String json = MAPPER.writeValueAsString(bean);
        assertEquals("{\"map\":{\"B\":3}}", json);

        // but can change
        json = MAPPER.writer()
                .with(EnumFeature.WRITE_ENUMS_USING_TO_STRING)
                .writeValueAsString(bean);
        assertEquals("{\"map\":{\"b\":3}}", json);

        // [databind#1570]

        // 14-Sep-2019, tatu: as per [databind#2129], must NOT use this feature but
        //    instead new `WRITE_ENUM_KEYS_USING_INDEX` added in 2.10
        json = MAPPER.writer()
                .with(EnumFeature.WRITE_ENUMS_USING_INDEX)
                .writeValueAsString(bean);
//        assertEquals(a2q("{'map':{'"+TestEnum.B.ordinal()+"':3}}"), json);
        assertEquals(a2q("{'map':{'B':3}}"), json);
    }

    @Test
    public void testCustomEnumMapKeySerializer() throws Exception {
        String json = MAPPER.writeValueAsString(new MyBean661("abc"));
        assertEquals(a2q("{'X-FOO':'abc'}"), json);
    }

    // [databind#594]
    @Test
    public void testJsonValueForEnumMapKeySer() throws Exception {
        assertEquals(a2q("{'stuff':{'longValue':'foo'}}"),
                MAPPER.writeValueAsString(new MyStuff594("foo")));
    }

    @Test
    public void testJsonValueForEnumMapKeyDeser() throws Exception {
        final String json = a2q("{'stuff':{'longValue':'foo'}}");
        ObjectReader r = MAPPER.readerFor(MyStuff594.class);
        MyStuff594 result = r.with(EnumFeature.READ_ENUMS_USING_TO_STRING).readValue(json);
        assertEquals("foo", result.stuff.get(MyEnum594.VALUE_WITH_A_REALLY_LONG_NAME_HERE));

        result = r.without(EnumFeature.READ_ENUMS_USING_TO_STRING).readValue(json);
        assertEquals("foo", result.stuff.get(MyEnum594.VALUE_WITH_A_REALLY_LONG_NAME_HERE));
    }

    // [databind#2129]
    @Test
    public void testEnumAsIndexForRootMap() throws Exception
    {
        final Map<Type, Integer> input = Collections.singletonMap(Type.FIRST, 3);

        // by default, write using name()
        assertEquals(a2q("{'FIRST':3}"),
                MAPPER.writeValueAsString(input));

        // but change with setting
        assertEquals(a2q("{'0':3}"),
                MAPPER.writer()
                .with(EnumFeature.WRITE_ENUM_KEYS_USING_INDEX)
                .writeValueAsString(input));

        // but NOT with value settings
        assertEquals(a2q("{'FIRST':3}"),
                MAPPER.writer()
                    .with(EnumFeature.WRITE_ENUMS_USING_INDEX)
                    .writeValueAsString(input));
    }

    // [databind#2129]
    @Test
    public void testEnumAsIndexForValueMap() throws Exception
    {
        final TypeContainer input = new TypeContainer(Type.SECOND, 72);

        // by default, write using name()
        assertEquals(a2q("{'values':{'SECOND':72}}"),
                MAPPER.writeValueAsString(input));

        // but change with setting
        assertEquals(a2q("{'values':{'1':72}}"),
                MAPPER.writer()
                .with(EnumFeature.WRITE_ENUM_KEYS_USING_INDEX)
                .writeValueAsString(input));

        // but NOT with value settings
        assertEquals(a2q("{'values':{'SECOND':72}}"),
                MAPPER.writer()
                    .with(EnumFeature.WRITE_ENUMS_USING_INDEX)
                    .writeValueAsString(input));
    }

    // [databind#2457]
    @Test
    public void testCustomEnumAsRootMapKey() throws Exception
    {
        final Map<MyEnum2457, String> map = new LinkedHashMap<>();
        map.put(MyEnum2457.A, "1");
        map.put(MyEnum2457.B, "2");
        assertEquals(a2q("{'A':'1','B':'2'}"),
                MAPPER.writer()
                        .without(EnumFeature.WRITE_ENUMS_USING_TO_STRING)
                        .writeValueAsString(map));

        // But should be able to override
        assertEquals(a2q("{'"+MyEnum2457.A.toString()+"':'1','"+MyEnum2457.B.toString()+"':'2'}"),
                MAPPER.writer()
                    .with(EnumFeature.WRITE_ENUMS_USING_TO_STRING)
                    .writeValueAsString(map));
    }

    /**
     * @see #testCustomEnumAsRootMapKey
     */
    // [databind#2457]
    @Test
    public void testCustomEnumAsRootMapKeyMixin() throws Exception
    {
        ObjectMapper mixinMapper = JsonMapper.builder()
                .addMixIn(MyEnum2457Base.class, MyEnum2457Mixin.class)
                .build();
        final Map<MyEnum2457Base, String> map = new LinkedHashMap<>();
        map.put(MyEnum2457Base.A, "1");
        map.put(MyEnum2457Base.B, "2");
        map.put(MyEnum2457Base.C, "3");
        assertEquals(a2q("{'a_mixin':'1','b_mixin':'2','C':'3'}"),
                mixinMapper.writer()
                        .without(EnumFeature.WRITE_ENUMS_USING_TO_STRING)
                        .writeValueAsString(map));

        // But should be able to override
        assertEquals(a2q("{'a_mixin':'1','b_mixin':'2','"
                +MyEnum2457Base.C.toString()+"':'3'}"
                ),
                mixinMapper.writer()
                        .with(EnumFeature.WRITE_ENUMS_USING_TO_STRING)
                        .writeValueAsString(map));
    }

    // [databind#5432]
    @Test
    void enumKeyShouldSerializeUsingJsonPropertyAndToString() throws Exception
    {
        final ObjectMapper mapper = jsonMapperBuilder()
                .enable(EnumFeature.WRITE_ENUMS_USING_TO_STRING)
                .build();

        // Sanity check first
        // assertEquals(q("red"), mapper.writeValueAsString(Color5432.RED));

        // Then actual test
        Map<Color5432, String> map = Collections.singletonMap(Color5432.RED, "#ff0000");
        String json = mapper.writeValueAsString(map);
        assertEquals("{\"red\":\"#ff0000\"}", json);
    }

    // [databind#5432]
    @Test
    void enumKeyShouldSerializeUsingJsonPropertyAndName() throws Exception
    {
        final ObjectMapper mapper = jsonMapperBuilder()
                .disable(EnumFeature.WRITE_ENUMS_USING_TO_STRING)
                .build();

        // Sanity check first
        assertEquals(q("red"), mapper.writeValueAsString(Color5432.RED));

        // Then actual test
        Map<Color5432, String> map = Collections.singletonMap(Color5432.RED, "#ff0000");
        String json = mapper.writeValueAsString(map);
        assertEquals("{\"red\":\"#ff0000\"}", json);
    }
}