diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index d42e8723f6..a59fb2cc6a 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -20,6 +20,10 @@ Project: jackson-databind #4676: Support other enum naming strategies than camelCase (requested by @hajdamak) (contributed by Lars B) +#4773: `SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS` should not apply to Maps + with uncomparable keys + (requested by @nathanukey) + (fix by Joo-Hyuk K) 2.18.1 (28-Oct-2024) diff --git a/src/main/java/com/fasterxml/jackson/databind/SerializationFeature.java b/src/main/java/com/fasterxml/jackson/databind/SerializationFeature.java index 98ca736c78..a16e5cbeff 100644 --- a/src/main/java/com/fasterxml/jackson/databind/SerializationFeature.java +++ b/src/main/java/com/fasterxml/jackson/databind/SerializationFeature.java @@ -434,6 +434,23 @@ public enum SerializationFeature implements ConfigFeature */ ORDER_MAP_ENTRIES_BY_KEYS(false), + /** + * Feature that determines whether to intentionally fail when the mapper attempts to + * order map entries with incomparable keys by accessing the first key of the map. + * So depending on the Map implementation, this may not be the same key every time. + *

+ * If enabled, will simply fail by throwing an exception. + * If disabled, will not throw an exception and instead simply return the original map. + *

+ * Note that this feature will apply only when configured to order map entries by keys, either + * through annotation or enabling {@link #ORDER_MAP_ENTRIES_BY_KEYS}. + *

+ * Feature is enabled by default and will default false in Jackson 3 and later. + * + * @since 2.19 + */ + FAIL_ON_ORDER_MAP_BY_INCOMPARABLE_KEY(true), + /* /****************************************************** /* Other diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/MapSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/MapSerializer.java index e5147cb829..c5c5019924 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/std/MapSerializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/MapSerializer.java @@ -1166,6 +1166,26 @@ protected Map _orderEntries(Map input, JsonGenerator gen, if (input instanceof SortedMap) { return input; } + // or is it empty? then no need to sort either + if (input.isEmpty()) { + return input; + } + // [databind#4773] Since 2.19: We should not try sorting Maps with uncomparable keys + // And first key is a good enough sample for now. + Object firstKey = input.keySet().iterator().next(); + if (!Comparable.class.isInstance(firstKey)) { + // We cannot sort incomparable keys, should we fail or just skip sorting? + if (!provider.isEnabled(SerializationFeature.FAIL_ON_ORDER_MAP_BY_INCOMPARABLE_KEY)) { + return input; + } else { + Class clazz = firstKey == null ? Object.class : firstKey.getClass(); + provider.reportBadDefinition(clazz, + String.format("Cannot order Map entries by key of incomparable type %s, consider disabling " + + "`SerializationFeature.FAIL_ON_ORDER_MAP_BY_INCOMPARABLE_KEY` to simply skip sorting", + ClassUtil.classNameOf(firstKey))); + } + } + // [databind#1411]: TreeMap does not like null key... (although note that // check above should prevent this code from being called in that case) // [databind#153]: but, apparently, some custom Maps do manage hit this diff --git a/src/test/java/com/fasterxml/jackson/databind/tofix/OrderMapEntriesByKeysSerializationFeature4773Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/OrderMapEntriesByKeysSerializationFeature4773Test.java similarity index 62% rename from src/test/java/com/fasterxml/jackson/databind/tofix/OrderMapEntriesByKeysSerializationFeature4773Test.java rename to src/test/java/com/fasterxml/jackson/databind/deser/OrderMapEntriesByKeysSerializationFeature4773Test.java index fc114f0b6e..9976a4e6a3 100644 --- a/src/test/java/com/fasterxml/jackson/databind/tofix/OrderMapEntriesByKeysSerializationFeature4773Test.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/OrderMapEntriesByKeysSerializationFeature4773Test.java @@ -1,4 +1,4 @@ -package com.fasterxml.jackson.databind.tofix; +package com.fasterxml.jackson.databind.deser; import org.junit.jupiter.api.Test; @@ -8,11 +8,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; import com.fasterxml.jackson.databind.testutil.DatabindTestUtil; -import com.fasterxml.jackson.databind.testutil.failure.JacksonTestFailureExpected; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; // [databind#4773] Test to verify `SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS` behavior // when serializing `Map` instances with un-comparable keys. @@ -32,9 +31,8 @@ public static class ObjectContainer4773 { .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true) .build(); - @JacksonTestFailureExpected @Test - void testSerializationWithIncomparableKeys() + void testSerializationFailureWhenEnabledWithIncomparableKeys() throws Exception { // Given @@ -44,11 +42,15 @@ void testSerializationWithIncomparableKeys() // When : Throws exception // com.fasterxml.jackson.databind.JsonMappingException: class java.util.Currency cannot be cast to class java.lang.Comparable - String jsonResult = objectMapper.writeValueAsString(entity); - - // Then : Order should not matter, just plain old serialize - assertTrue(jsonResult.contains("GBP")); - assertTrue(jsonResult.contains("AUD")); + try { + objectMapper.writer() + .with(SerializationFeature.FAIL_ON_ORDER_MAP_BY_INCOMPARABLE_KEY) + .writeValueAsString(entity); + fail("Should not pass"); + } catch (InvalidDefinitionException e) { + // Then + verifyException(e, "Cannot order Map entries by key of incomparable type"); + } } @Test @@ -75,4 +77,24 @@ void testSerializationWithGenericObjectKeys() "'5':'N_TEXT'}}"), jsonResult); } + @Test + void testSerWithNullType() + throws Exception + { + // Given : Mixed keys with incomparable `Currency` and comparable `Integer` + ObjectContainer4773 entity = new ObjectContainer4773(); + entity.exampleMap.put(null, "AUD_TEXT"); + + // When : Throws exception + try { + objectMapper.writer() + .with(SerializationFeature.FAIL_ON_ORDER_MAP_BY_INCOMPARABLE_KEY) + .writeValueAsString(entity); + fail("Should not pass"); + } catch (InvalidDefinitionException e) { + // Then + verifyException(e, "Cannot order Map entries by key of incomparable type [null]"); + } + } + } diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/jdk/MapSerializationTest.java b/src/test/java/com/fasterxml/jackson/databind/ser/jdk/MapSerializationTest.java index d52198eb9f..6ceef81128 100644 --- a/src/test/java/com/fasterxml/jackson/databind/ser/jdk/MapSerializationTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/ser/jdk/MapSerializationTest.java @@ -164,7 +164,8 @@ public void testOrderByKey() throws IOException @Test public void testOrderByWithNulls() throws IOException { - ObjectWriter sortingW = MAPPER.writer(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS); + ObjectWriter sortingW = MAPPER.writer(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS) + .without(SerializationFeature.FAIL_ON_ORDER_MAP_BY_INCOMPARABLE_KEY); // 16-Oct-2016, tatu: but mind the null key, if any Map mapWithNullKey = new LinkedHashMap(); mapWithNullKey.put(null, 1);