package com.fasterxml.jackson.databind.ser; import java.io.*; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentSkipListMap; import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping; import com.fasterxml.jackson.databind.annotation.JsonSerialize; @SuppressWarnings("serial") public class TestMapSerialization extends BaseMapTest { @JsonSerialize(using=PseudoMapSerializer.class) static class PseudoMap extends LinkedHashMap<String,String> { public PseudoMap(String... values) { for (int i = 0, len = values.length; i < len; i += 2) { put(values[i], values[i+1]); } } } static class PseudoMapSerializer extends JsonSerializer<Map<String,String>> { @Override public void serialize(Map<String,String> value, JsonGenerator gen, SerializerProvider provider) throws IOException { // just use standard Map.toString(), output as JSON String gen.writeString(value.toString()); } } // [databind#335] static class MapOrderingBean { @JsonPropertyOrder(alphabetic=true) public LinkedHashMap<String,Integer> map; public MapOrderingBean(String... keys) { map = new LinkedHashMap<String,Integer>(); int ix = 1; for (String key : keys) { map.put(key, ix++); } } } // [databind#565]: Support ser/deser of Map.Entry static class StringIntMapEntry implements Map.Entry<String,Integer> { public final String k; public final Integer v; public StringIntMapEntry(String k, Integer v) { this.k = k; this.v = v; } @Override public String getKey() { return k; } @Override public Integer getValue() { return v; } @Override public Integer setValue(Integer value) { throw new UnsupportedOperationException(); } } static class StringIntMapEntryWrapper { public StringIntMapEntry value; public StringIntMapEntryWrapper(String k, Integer v) { value = new StringIntMapEntry(k, v); } } // for [databind#691] @JsonTypeInfo(use=JsonTypeInfo.Id.NAME) @JsonTypeName("mymap") static class MapWithTypedValues extends LinkedHashMap<String,String> { } @JsonTypeInfo(use = Id.CLASS) public static class Mixin691 { } /* /********************************************************** /* Test methods /********************************************************** */ final private ObjectMapper MAPPER = objectMapper(); public void testUsingObjectWriter() throws IOException { ObjectWriter w = MAPPER.writerFor(Object.class); Map<String,Object> map = new LinkedHashMap<String,Object>(); map.put("a", 1); String json = w.writeValueAsString(map); assertEquals(aposToQuotes("{'a':1}"), json); } public void testMapSerializer() throws IOException { assertEquals("\"{a=b, c=d}\"", MAPPER.writeValueAsString(new PseudoMap("a", "b", "c", "d"))); } // problems with map entries, values public void testMapKeySetValuesSerialization() throws IOException { Map<String,String> map = new HashMap<String,String>(); map.put("a", "b"); assertEquals("[\"a\"]", MAPPER.writeValueAsString(map.keySet())); assertEquals("[\"b\"]", MAPPER.writeValueAsString(map.values())); // TreeMap has similar inner class(es): map = new TreeMap<String,String>(); map.put("c", "d"); assertEquals("[\"c\"]", MAPPER.writeValueAsString(map.keySet())); assertEquals("[\"d\"]", MAPPER.writeValueAsString(map.values())); // and for [JACKSON-533], same for concurrent maps map = new ConcurrentHashMap<String,String>(); map.put("e", "f"); assertEquals("[\"e\"]", MAPPER.writeValueAsString(map.keySet())); assertEquals("[\"f\"]", MAPPER.writeValueAsString(map.values())); } // sort Map entries by key public void testOrderByKey() throws IOException { ObjectMapper m = new ObjectMapper(); assertFalse(m.isEnabled(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS)); LinkedHashMap<String,Integer> map = new LinkedHashMap<String,Integer>(); map.put("b", 3); map.put("a", 6); // by default, no (re)ordering: assertEquals("{\"b\":3,\"a\":6}", m.writeValueAsString(map)); // but can be changed ObjectWriter sortingW = m.writer(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS); assertEquals("{\"a\":6,\"b\":3}", sortingW.writeValueAsString(map)); } // related to [databind#1411] public void testOrderByWithNulls() throws IOException { ObjectWriter sortingW = MAPPER.writer(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS); // 16-Oct-2016, tatu: but mind the null key, if any Map<String,Integer> mapWithNullKey = new LinkedHashMap<String,Integer>(); mapWithNullKey.put(null, 1); mapWithNullKey.put("b", 2); // 16-Oct-2016, tatu: By default, null keys are not accepted... try { /*String json =*/ sortingW.writeValueAsString(mapWithNullKey); //assertEquals(aposToQuotes("{'':1,'b':2}"), json); } catch (JsonMappingException e) { verifyException(e, "Null key for a Map not allowed"); } } // [Databind#335] public void testOrderByKeyViaProperty() throws IOException { MapOrderingBean input = new MapOrderingBean("c", "b", "a"); String json = MAPPER.writeValueAsString(input); assertEquals(aposToQuotes("{'map':{'a':3,'b':2,'c':1}}"), json); } // [Databind#565] public void testMapEntry() throws IOException { StringIntMapEntry input = new StringIntMapEntry("answer", 42); String json = MAPPER.writeValueAsString(input); assertEquals(aposToQuotes("{'answer':42}"), json); StringIntMapEntry[] array = new StringIntMapEntry[] { input }; json = MAPPER.writeValueAsString(array); assertEquals(aposToQuotes("[{'answer':42}]"), json); // and maybe with bit of extra typing? ObjectMapper mapper = new ObjectMapper().enableDefaultTyping(DefaultTyping.NON_FINAL); json = mapper.writeValueAsString(input); assertEquals(aposToQuotes("['"+StringIntMapEntry.class.getName()+"',{'answer':42}]"), json); } public void testMapEntryWrapper() throws IOException { StringIntMapEntryWrapper input = new StringIntMapEntryWrapper("answer", 42); String json = MAPPER.writeValueAsString(input); assertEquals(aposToQuotes("{'value':{'answer':42}}"), json); } // [databind#691] public void testNullJsonMapping691() throws Exception { MapWithTypedValues input = new MapWithTypedValues(); input.put("id", "Test"); input.put("NULL", null); String json = MAPPER.writeValueAsString(input); assertEquals(aposToQuotes("{'@type':'mymap','id':'Test','NULL':null}"), json); } // [databind#691] public void testNullJsonInTypedMap691() throws Exception { Map<String, String> map = new HashMap<String, String>(); map.put("NULL", null); ObjectMapper mapper = new ObjectMapper(); mapper.addMixIn(Object.class, Mixin691.class); String json = mapper.writeValueAsString(map); assertEquals("{\"@class\":\"java.util.HashMap\",\"NULL\":null}", json); } // [databind#1513] public void testConcurrentMaps() throws Exception { final ObjectWriter w = MAPPER.writer(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS); Map<String,String> input = new ConcurrentSkipListMap<String,String>(); input.put("x", "y"); input.put("a", "b"); String json = w.writeValueAsString(input); assertEquals(aposToQuotes("{'a':'b','x':'y'}"), json); input = new ConcurrentHashMap<String,String>(); input.put("x", "y"); input.put("a", "b"); json = w.writeValueAsString(input); assertEquals(aposToQuotes("{'a':'b','x':'y'}"), json); // One more: while not technically concurrent map at all, exhibits same issue input = new Hashtable<String,String>(); input.put("x", "y"); input.put("a", "b"); json = w.writeValueAsString(input); assertEquals(aposToQuotes("{'a':'b','x':'y'}"), json); } }