Repository navigation
Breaking changes
codec-core moved from org.jwcarman.codec.spi to org.jwcarman.codec.
Migration is one find-and-replace:
org.jwcarman.codec.spi -> org.jwcarman.codec
No type, member or behaviour changed, and the Automatic-Module-Name is unchanged, so nothing but the import line moves.
Codec, CodecFactory, TypeRef and the five exception types now live in the package their module is named for, matching every other module (org.jwcarman.codec.crypto, .versioned, .transform.compress and the rest), none of which carries a role suffix. The spi name described only CodecFactory, the one interface a backend implements; the other seven types are what callers use.
Fixed
TypeRef captured the wrong type argument through an indirect subclass. The constructor read the immediate superclass's first type argument, which is only TypeRef's T when the subclass extends TypeRef directly. If you wrapped TypeRef in an abstraction of your own — abstract class EnvelopeCodec<E> extends TypeRef<Envelope<E>> — you got a codec built for the wrong type, and the failure surfaced far away as a ClassCastException or a decode into the wrong shape.
| Declaration | Before | After |
|---|---|---|
class Mid<A, B> extends TypeRef<B> |
captured A |
captures B |
class Wrapping<X> extends TypeRef<List<X>> |
captured X |
captures List<X> |
class Concrete extends TypeRef<String>, new Concrete() {} |
ClassCastException |
captures String |
T is now resolved through the whole hierarchy, following a binding that leads to another binding (class Nested<X> extends Mid<X, List<X>>) and substituting into parameterized types, generic arrays and wildcard bounds alike, since a half-substituted type claims to be concrete while a type variable is still sitting in it.
Extending TypeRef raw binds nothing to T, which leaves T standing for itself and is rejected by the same IllegalArgumentException that already caught a captured type variable.
If you only ever wrote new TypeRef<List<Person>>() {} directly, this bug never affected you.
Documentation
rawClass() no longer claims its unchecked cast is "sound by construction" unconditionally. That holds for a reference whose T the compiler established — captured, or built by listOf and the other typed combinators — and not for one from parameterized, whose T the caller asserts.
parameterized documents one shape it cannot build: an inner class of a generic outer class. A class literal has already discarded the outer's arguments, so the built type is not equal to the captured one and will not find it as a cache key. The class-level cache-key guarantee names this exception.
Requirements
Java 25. Spring Boot 4.1.x for the starter and auto-configuration modules.
Full changelog: https://github.com/jwcarman/codec/blob/main/CHANGELOG.md