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:
- The fallback type isn't assignable to the declared type (e.g.
HashSet doesn't implement SortedSet), so the generated constructor call ClassCastExceptions
- 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
- 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.
- 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
Describe the bug
Follow-up to #53408. PR #53414 (shipped in 3.34.3) fixed concrete non-standard JDK collections (
LinkedList,LinkedHashSet) andFAIL_ON_UNKNOWN_PROPERTIES, but abstract/interface collection and map types still produceClassCastExceptionat runtime.This affects pure JDK types (
SortedSet,SortedMap,Deque) and third-party ones like Guava'sImmutableSet/ImmutableList(viajackson-datatype-guava). It's still a regression vs the reflection-based deserialisers, and still relevant givenenable-reflection-free-serializersbecomes 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()inJacksonDeserializerFactory. When the declared type is abstract or an interface, it falls back toHashSet/ArrayList:These fallback types then get passed to
TypeFactory.constructCollectionType()/constructMapType(), which builds aCollectionType/MapTypefor the wrong concrete class. As far as I can tell, two things go wrong:HashSetdoesn't implementSortedSet), so the generated constructor callClassCastExceptionsjackson-datatype-guava'sImmutableSetdeserialiser) are bypassed entirely, because Jackson seesHashSetand uses its default collection deserialiserThe 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 ofconstructCollectionType/constructMapTypewith a hardcoded concrete fallback. I thinkconstructParametricTypewould 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->HashSetetc.) 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/ImmutableMapare pretty widely used, and having them work with the reflection-free deserialisers before 3.35 makes the default switch much smoother.Expected behavior
POSTendpoint accepting e.g.record Foo(SortedSet<Token> tokens)orrecord Bar(Deque<Item> items)should deserialise elements as the correct types, just like the reflection-based deserialiser does.jackson-datatype-guavashould be respected.Actual behavior
The generated deserialiser substitutes the wrong concrete collection/map type, causing
ClassCastException: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
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 -aorverDarwin 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 -versionopenjdk 25.0.1 2025-10-21
Quarkus version or git rev
3.34.3
Build tool (ie. output of
mvnw --versionorgradlew --version)Gradle 9.4.1
Additional information
No response