Skip to content

Deserialize Java 21 sequenced collection types #4089

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
mjustin opened this issue Aug 20, 2023 · 3 comments
Closed

Deserialize Java 21 sequenced collection types #4089

mjustin opened this issue Aug 20, 2023 · 3 comments
Labels
to-evaluate Issue that has been received but not yet evaluated

Comments

@mjustin
Copy link

mjustin commented Aug 20, 2023

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;
        }
@mjustin mjustin added the to-evaluate Issue that has been received but not yet evaluated label Aug 20, 2023
@pjfanning
Copy link
Member

Thanks for the report. We will have to have a solution in place for some future version of Jackson. If you want to use Sequence types today, you could try writing a custom deserializer. Jackson is very extensible this way.

@pjfanning
Copy link
Member

@mjustin Thanks for the detailed summary and pointer to a solution. I have created #4090

@pjfanning
Copy link
Member

fix will be in 2.16.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
to-evaluate Issue that has been received but not yet evaluated
Projects
None yet
Development

No branches or pull requests

2 participants