Skip to content

Deserialize Java 21 sequenced collection types #4089

@mjustin

Description

@mjustin

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:

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;
        }

Metadata

Metadata

Assignees

No one assigned

    Labels

    to-evaluateIssue that has been received but not yet evaluated

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions