-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Description
Is your feature request related to a problem? Please describe.
Java 21 is adding sequenced collection interfaces SequencedCollection
, SequencedSet
, and SequencedMap
. These represent collections with a well-defined iteration order, such as List
, SortedSet
, LinkedHashSet
, and LinkedHashMap
.
Currently, using the new sequenced collection types for deserialization (such as the type of a property of a value being deserialized) results in a failure:
Cannot find a deserializer for non-concrete Collection type [collection type; class java.util.SequencedCollection, contains [simple type, class java.lang.String]]
This contrasts with existing collection interfaces (such as List
, Queue
, SortedMap
), which are currently automatically deserialized to a reasonable type (ArrayList
, LinkedList
, and TreeMap
for these specific examples).
Describe the solution you'd like
It would be ideal if properties of the new sequenced interface types could be deserialized from JSON in the same way as the existing collection interface types.
The ideal mappings most in line with the existing ones seem to be:
SequencedCollection
:ArrayList
SequencedSet
:LinkedHashSet
SequencedMap
:LinkedHashMap
Usage example
Here is a JUnit Jupiter test that shows the existing behavior for the pre-Java 21 interface types, and the proposed behavior for the sequenced types.
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.json.JsonMapper;
import org.junit.jupiter.api.Test;
import java.util.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
class SequencedCollectionsTest {
private JsonMapper objectMapper = JsonMapper.builder().build();
/**
* Existing behavior.
*/
@Test
void nonSequencedCollectionTypes() throws JsonProcessingException {
record NonSequencedCollections(
Collection<String> collection,
Set<String> set,
SortedSet<String> sortedSet,
NavigableSet<String> navigableSet,
LinkedHashSet<String> linkedHashSet,
List<String> list,
Queue<String> queue,
Deque<String> deque,
Map<String, Integer> map,
SortedMap<String, Integer> sortedMap,
NavigableMap<String, Integer> navigableMap,
LinkedHashMap<String, Integer> linkedHashMap) {
}
String json = """
{
"collection": ["A", "B"],
"set": ["A", "B"],
"sortedSet": ["A", "B"],
"navigableSet": ["A", "B"],
"linkedHashSet": ["A", "B"],
"list": ["A", "B"],
"queue": ["A", "B"],
"deque": ["A", "B"],
"sortedMap": {"A": 1, "B": 2},
"navigableMap": {"A": 1, "B": 2},
"linkedHashMap": {"A": 1, "B": 2}
}
""";
NonSequencedCollections value = objectMapper.readValue(json, NonSequencedCollections.class);
assertEquals(ArrayList.class, value.collection.getClass());
assertEquals(HashSet.class, value.set.getClass());
assertEquals(TreeSet.class, value.sortedSet.getClass());
assertEquals(TreeSet.class, value.navigableSet.getClass());
assertEquals(LinkedHashSet.class, value.linkedHashSet.getClass());
assertEquals(ArrayList.class, value.list.getClass());
assertEquals(LinkedList.class, value.queue.getClass());
assertEquals(LinkedList.class, value.deque.getClass());
assertEquals(TreeMap.class, value.sortedMap.getClass());
assertEquals(TreeMap.class, value.navigableMap.getClass());
assertEquals(LinkedHashMap.class, value.linkedHashMap.getClass());
}
/**
* Desired new behavior.
*/
@Test
void sequencedCollectionTypes() throws JsonProcessingException {
record SequencedCollections(
SequencedCollection<String> sequencedCollection,
SequencedSet<String> sequencedSet,
SequencedMap<String, Integer> sequencedMap) {
}
String json = """
{
"sequencedCollection": ["A", "B"],
"sequencedSet": ["A", "B"],
"sequencedMap": {"A": 1, "B": 2}
}
""";
SequencedCollections value = objectMapper.readValue(json, SequencedCollections.class);
assertEquals(ArrayList.class, value.sequencedCollection.getClass());
assertEquals(LinkedHashSet.class, value.sequencedSet.getClass());
assertEquals(LinkedHashMap.class, value.sequencedMap.getClass());
}
}
Additional context
This is obviously a Java 21 feature. One option is to mirror what is done with Java 8 with jackson-modules-java8 and have a separate module.
Alternatively, I see that deserialization of standard collection interfaces appears to be handled by BasicDeserializerFactory.ContainerDefaultMappings
:
jackson-databind/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java
Lines 2540 to 2595 in d7e77c3
protected static class ContainerDefaultMappings { | |
// We do some defaulting for abstract Collection classes and | |
// interfaces, to avoid having to use exact types or annotations in | |
// cases where the most common concrete Collection will do. | |
final static HashMap<String, Class<? extends Collection>> _collectionFallbacks; | |
static { | |
HashMap<String, Class<? extends Collection>> fallbacks = new HashMap<>(); | |
final Class<? extends Collection> DEFAULT_LIST = ArrayList.class; | |
final Class<? extends Collection> DEFAULT_SET = HashSet.class; | |
fallbacks.put(Collection.class.getName(), DEFAULT_LIST); | |
fallbacks.put(List.class.getName(), DEFAULT_LIST); | |
fallbacks.put(Set.class.getName(), DEFAULT_SET); | |
fallbacks.put(SortedSet.class.getName(), TreeSet.class); | |
fallbacks.put(Queue.class.getName(), LinkedList.class); | |
// 09-Feb-2019, tatu: How did we miss these? Related in [databind#2251] problem | |
fallbacks.put(AbstractList.class.getName(), DEFAULT_LIST); | |
fallbacks.put(AbstractSet.class.getName(), DEFAULT_SET); | |
// 09-Feb-2019, tatu: And more esoteric types added in JDK6 | |
fallbacks.put(Deque.class.getName(), LinkedList.class); | |
fallbacks.put(NavigableSet.class.getName(), TreeSet.class); | |
_collectionFallbacks = fallbacks; | |
} | |
// We do some defaulting for abstract Map classes and | |
// interfaces, to avoid having to use exact types or annotations in | |
// cases where the most common concrete Maps will do. | |
final static HashMap<String, Class<? extends Map>> _mapFallbacks; | |
static { | |
HashMap<String, Class<? extends Map>> fallbacks = new HashMap<>(); | |
final Class<? extends Map> DEFAULT_MAP = LinkedHashMap.class; | |
fallbacks.put(Map.class.getName(), DEFAULT_MAP); | |
fallbacks.put(AbstractMap.class.getName(), DEFAULT_MAP); | |
fallbacks.put(ConcurrentMap.class.getName(), ConcurrentHashMap.class); | |
fallbacks.put(SortedMap.class.getName(), TreeMap.class); | |
fallbacks.put(java.util.NavigableMap.class.getName(), TreeMap.class); | |
fallbacks.put(java.util.concurrent.ConcurrentNavigableMap.class.getName(), | |
java.util.concurrent.ConcurrentSkipListMap.class); | |
_mapFallbacks = fallbacks; | |
} | |
public static Class<?> findCollectionFallback(JavaType type) { | |
return _collectionFallbacks.get(type.getRawClass().getName()); | |
} | |
public static Class<?> findMapFallback(JavaType type) { | |
return _mapFallbacks.get(type.getRawClass().getName()); | |
} | |
} |
Since it looks like BasicDeserializerFactory.ContainerDefaultMappings
doesn't require the classes to exist on the classpath, it looks like you could just hardcode the Java 21 types in there:
// 09-Feb-2019, tatu: And more esoteric types added in JDK6
fallbacks.put(Deque.class.getName(), LinkedList.class);
fallbacks.put(NavigableSet.class.getName(), TreeSet.class);
// Sequenced types added in JDK21
fallbacks.put("java.util.SequencedCollection", DEFAULT_LIST);
fallbacks.put("java.util.SequencedSet", LinkedHashSet.class);
_collectionFallbacks = fallbacks;
}