Skip to content

[Reflection-free Jackson deserialisers] ClassCastException with abstract/interface collection and map types (SortedSet, Deque, SortedMap, Guava ImmutableSet/ImmutableList) #53556

@lloydmeta

Description

@lloydmeta

Describe the bug

Follow-up to #53408. PR #53414 (shipped in 3.34.3) fixed concrete non-standard JDK collections (LinkedList, LinkedHashSet) and FAIL_ON_UNKNOWN_PROPERTIES, but abstract/interface collection and map types still produce ClassCastException at runtime.

This affects pure JDK types (SortedSet, SortedMap, Deque) and third-party ones like Guava's ImmutableSet/ImmutableList (via jackson-datatype-guava). It's still a regression vs the reflection-based deserialisers, and still relevant given enable-reflection-free-serializers becomes the default in 3.35 (#53161).

Reproducer (updated): https://github.com/lloydmeta/quarkus-jackson-reflection-free-bug

I think the issue is best demonstrated in concreteCollectionType() in JacksonDeserializerFactory. When the declared type is abstract or an interface, it falls back to HashSet/ArrayList:

private static Class<?> concreteCollectionType(String fieldTypeName, FieldKind fieldKind) {
    try {
        Class<?> declared = Class.forName(fieldTypeName);
        if (!declared.isInterface() && !Modifier.isAbstract(declared.getModifiers())) {
            return declared;
        }
    } catch (ClassNotFoundException ignored) {
    }
    return fieldKind == FieldKind.SET ? HashSet.class : ArrayList.class;
}

These fallback types then get passed to TypeFactory.constructCollectionType()/constructMapType(), which builds a CollectionType/MapType for the wrong concrete class. As far as I can tell, two things go wrong:

  1. The fallback type isn't assignable to the declared type (e.g. HashSet doesn't implement SortedSet), so the generated constructor call ClassCastExceptions
  2. Jackson module-provided deserialisers (e.g. jackson-datatype-guava's ImmutableSet deserialiser) are bypassed entirely, because Jackson sees HashSet and uses its default collection deserialiser

The MAP branch seems to have the same pattern, hardcoding HashMap.class.

Possible fix

One possible fix might be to use TypeFactory.constructParametricType(declaredType, elementTypes...) instead of constructCollectionType/constructMapType with a hardcoded concrete fallback. I think constructParametricType would preserve the declared type, letting Jackson's normal deserialiser resolution find module-provided deserialisers where registered. It should also work for standard types (List -> ArrayList, Set -> HashSet etc.) since Jackson already knows how to resolve those. But I haven't tested this in the Quarkus codebase, so there may be subtleties I'm missing.

Ideally this would also mean Guava collections (and other Jackson module-backed types) work correctly when the appropriate module is registered. Guava's ImmutableList/ImmutableSet/ImmutableMap are pretty widely used, and having them work with the reflection-free deserialisers before 3.35 makes the default switch much smoother.

Expected behavior

  1. A POST endpoint accepting e.g. record Foo(SortedSet<Token> tokens) or record Bar(Deque<Item> items) should deserialise elements as the correct types, just like the reflection-based deserialiser does.
  2. Jackson modules like jackson-datatype-guava should be respected.

Actual behavior

The generated deserialiser substitutes the wrong concrete collection/map type, causing ClassCastException:

java.lang.ClassCastException: class java.util.HashSet cannot be cast to class java.util.SortedSet
java.lang.ClassCastException: class java.util.ArrayList cannot be cast to class java.util.Deque
java.lang.ClassCastException: class java.util.HashMap cannot be cast to class java.util.SortedMap
java.lang.ClassCastException: class java.util.HashSet cannot be cast to class com.google.common.collect.ImmutableSet
java.lang.ClassCastException: class java.util.ArrayList cannot be cast to class com.google.common.collect.ImmutableList

All of these work correctly with enable-reflection-free-serializers=false (see lloydmeta/quarkus-jackson-reflection-free-bug#3)

How to Reproduce?

Reproducer: https://github.com/lloydmeta/quarkus-jackson-reflection-free-bug

git clone https://github.com/lloydmeta/quarkus-jackson-reflection-free-bug.git
cd quarkus-jackson-reflection-free-bug
./gradlew clean test

10 tests, 5 fail. The pure JDK cases (SortedSet, Deque, SortedMap) don't need any external dependencies. The Guava cases (ImmutableSet, ImmutableList) are included because they're common in practice (test summary)

Output of uname -a or ver

Darwin Mac-M2Pro 25.4.0 Darwin Kernel Version 25.4.0: Thu Mar 19 19:31:17 PDT 2026; root:xnu-12377.101.15~1/RELEASE_ARM64_T6020 arm64

Output of java -version

openjdk 25.0.1 2025-10-21

Quarkus version or git rev

3.34.3

Build tool (ie. output of mvnw --version or gradlew --version)

Gradle 9.4.1

Additional information

No response

Metadata

Metadata

Assignees

Labels

area/jacksonIssues related to Jackson (JSON library)kind/bugSomething isn't working

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions