Skip to content

Releases: jwcarman/codec

0.10.0

Choose a tag to compare

@jwcarman jwcarman released this 20 Sep 23:15
1ea981d

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

0.9.0

Choose a tag to compare

@jwcarman jwcarman released this 07 Sep 14:29
e88b83d

Added

  • TypeRef.listOf, setOf, optionalOf, mapOf and parameterized build a
    parameterized type reference from the references of its arguments, so generic
    code can create a Codec<List<O>> or Codec<Envelope<O>> from a caller's
    TypeRef<O>. A built reference equals the same type captured by an anonymous
    subclass. parameterized takes the class as Class<? super T>, so the
    compiler rejects a class that is not the declared type's; a class with no type
    parameters, a wrong argument count and primitive arguments are rejected at
    construction. The argument list against the declared type's own arguments is
    the caller's to get right

Changed

  • codec-spring-boot-starter now brings codec-versioned along with
    codec-core, codec-transforms and codec-autoconfigure: the module has
    no dependencies of its own, and versioned storage is part of the toolkit the
    starter promises

Fixed

  • codec-kafka no longer resolves the lz4-java 1.10.1 and zstd-jni 1.5.6 that
    kafka-clients declares: the parent now manages both at the versions codec
    ships (1.11.2 and 1.5.7), which closes GHSA-xx22-p4ch-683r (CVE-2026-59949, a
    JVM crash in lz4-java's native XXHash on invalid array ranges) on that path

0.8.0

Choose a tag to compare

@jwcarman jwcarman released this 07 Sep 05:39

Breaking changes

  • Every failure Codec.encode and Codec.decode report is now a
    CodecException from codec-core, in one of four families keyed to what the
    caller does next: InvalidValueException (fix the value),
    InvalidPayloadException (quarantine), UnsupportedFormatException (hold for
    a newer reader), TransientCodecException (retry). Anything catching
    IllegalArgumentException, IllegalStateException, UncheckedIOException,
    ClassCastException, or a Jackson/Gson/JSON-B/Fory/protobuf exception from
    encode/decode must catch the family (or CodecException) instead; the
    library's exception is still there as the cause. Construction-time exceptions
    are unchanged. codec-crypto's DecryptionException, KeyAccessException
    and EncryptionException keep their names and messages and now extend
    InvalidPayloadException, TransientCodecException and
    TransientCodecException respectively; an unknown envelope version or
    algorithm id is now UnsupportedFormatException, not a rejection.
    codec-versioned's UnknownVersionException now extends
    UnsupportedFormatException and is no longer a VersionedFormatException.
    See the new Handling failures
    guide.
  • Base32Codec.standard() and .hex() decode canonically: upper case only, and
    non-zero trailing bits in the final symbol are rejected (MZ====== is an
    error; MY====== is f). Callers who fed lower-case input opt back in with
    .caseInsensitive()

Changed

  • The build compiles with -Xlint:all,-processing,-unchecked -Werror: any javac
    warning in an enabled category fails compilation. unchecked is off because
    TypeRef.rawClass() is the one unchecked cast the type system cannot express;
    no other is permitted. The Gson, Protobuf and Fory backends no longer carry
    unchecked casts, and every exception class declares a serialVersionUID
  • codec-gson encodes by the codec's declared type (Gson.toJson(value, type))
    rather than the value's runtime class, matching what decode already did; a
    polymorphic value now serialises as its declared T
  • codec-fory: Apache Fory 1.1.0 → 1.7.1. ForyCodecFactory.of(...) takes Fory's
    defaults, and Fory's default changed from schema-consistent to compatible mode
    in 1.2.0 (without a release note), so payloads now carry their class schema:
    a reader whose class has gained or lost a field decodes correctly where
    schema-consistent mode silently returned a mis-shifted object. It costs a few
    bytes per class per message — a lone four-field record roughly doubles, a
    100-item order grows 2%; throughput is unchanged. Payloads written by earlier
    releases still decode, but bytes written by this release cannot be read by a
    schema-consistent Fory. Callers who want a specific mode build their own
    ThreadSafeFory and pass it to the constructor. Per Fory's security guidance
    the helper also sets deserializeUnknownClass(false), so a payload naming an
    unregistered class is rejected rather than materialised as an anonymous struct
  • JacksonCodecFactory, Jackson2CodecFactory, and GsonCodecFactory now reject a
    null engine or TypeRef with a labelled NullPointerException, matching the
    JSON-B and Fory factories; CompressionStreamCodec likewise rejects null on
    encode and decode before touching a stream, matching the other transforms

Added

  • TypeRef.rawClass(): the erased class of the captured type, typed Class<T>,
    so a backend can use the checked Class.cast instead of an unchecked cast of
    its own. It is the single unchecked cast in the codebase and its Javadoc says so
  • codec-versioned: VersionedCodec prefixes each payload with a magic and
    version header and dispatches decoding on it, so a storage strategy can change
    without a flag day — old versions stay registered and readable while an
    explicit write version makes a two-phase rollout possible
  • codec-benchmarks: JMH benchmarks for every compression transform (deflate
    and zstd at several levels), encoding, backend (a small record and a
    100-item object graph), and the envelope-encryption strategies (not
    published), with the results and a benchmarks page in the docs
  • Base32Codec.of(alphabet) and of(alphabet, pad): a strict codec over any
    32-symbol ASCII alphabet, with caseInsensitive() and aliasing(...) as
    decode-side opt-ins; crockford() (ULIDs), zBase32() and geohash()
    presets. The encode and decode loops are specialised for whole groups, about
    4x the throughput of the previous implementation

Documentation

  • Fixed a newcomer-walkthrough review's findings: the Composition guide's
    chaining sample and the Codec.andThen Javadoc snippet now build
    EnvelopeCodec (there is no AesCodec); the landing page states the Java
    25 / Spring Boot 4.x requirement; the landing page and Getting Started link
    Apache Kafka and Spring Data Redis; the Guides nav orders Handling Failures
    before Evolving Stored Formats; the Kafka guide covers
    RecordDeserializationException/ErrorHandlingDeserializer and declares
    its config variable; the Spring Boot guide states what happens with no
    backend on the classpath; and the Redis guide turns its auto-configuration
    prerequisites into a checklist and adds a codec.redis.cache.* properties
    table
  • Benchmarks re-run on 2026-09-06 with Fory 1.7.1, zstd-jni 1.5.7-15, and
    lz4-java 1.11.2: zstd decodes 1 MB payloads 18–29% faster at every level, the
    backend numbers and encoded sizes reflect Fory's compatible mode, and the Fory
    guide gains a schema-evolution section
  • Compression claims corrected against the benchmarks: the JDK codecs are
    fastest on payloads under a few hundred bytes, zstd's default level is not
    smaller than gzip on prose, and LZ4-HC is a decode-side optimisation
  • New guide, Evolving Stored Formats:
    how codec-versioned works, a worked upgrade, the fleet rollout order, and
    what belongs inside the versioned codec versus outside it
  • The aggregated Javadoc for every module is published with the site at
    jwcarman.github.io/codec/api, one set
    per release, built from the release tag

0.7.0

Choose a tag to compare

@jwcarman jwcarman released this 26 Aug 10:35

Breaking changes

  • The built-in transforms moved out of codec-core into a new
    codec-transforms module, and into packages by kind:
    org.jwcarman.codec.transform.compress (CompressionStreamCodec,
    GzipCodec, DeflateCodec) and org.jwcarman.codec.transform.encoding
    (Base64Codec). codec-core is now the SPI alone. Migration: add
    codec-transforms (the Spring Boot starter already includes it) and update
    the imports. codec-zstd now depends on codec-transforms.

Added

  • codec-kafka: CodecSerializer, CodecDeserializer, and CodecSerde adapt
    any codec to Kafka's client serialization interfaces; tombstones map to
    null in both directions
  • codec-spring-data-redis: CodecRedisSerializer adapts any codec to Spring
    Data Redis's RedisSerializer (and, via serializationPair(), the cache
    abstraction); Spring Boot auto-configuration serializes the cache values named
    in codec.redis.cache.caches through the application's CodecFactory
  • Codec.nullSafe(): an explicit wrapper that maps null to null in both
    directions for integrations whose contract treats null as absent
  • Codec.xmap(forward, backward): derive a codec for another type from an
    existing one — the domain-type-versus-wire-type tool for backends that only
    serialize generated or registered classes
  • StringCodec in codec-transforms (transform.text): text as its own bytes
    in any charset, with strict decoding
  • ChecksumCodec in codec-transforms (transform.checksum): a 32-bit
    checksum trailer for corruption detection, CRC-32C by default
  • HexCodec in codec-transforms (RFC 4648 base16): lower- or upper-case
    output, strict case-insensitive decoding
  • Base32Codec in codec-transforms (RFC 4648 §6 Base32 and §7 base32hex),
    strict case-insensitive decoding
  • codec-lz4: LZ4 frame-format compression transform (Lz4Codec) with fast and
    high-compression (LZ4-HC) modes and the same decompression-bomb cap as the
    built-in transforms

0.6.0

Choose a tag to compare

@jwcarman jwcarman released this 25 Aug 22:29

Four new modules and a text-safe transform. Nothing in the existing modules' APIs or wire formats changed; codec-crypto's frozen 0.5.0 wire format is unchanged.

Added

  • codec-fory: Apache Fory backend (ForyCodecFactory) for fast JVM-only
    binary serialization; class registration is mandatory and never relaxed by
    the module. Spring Boot auto-configuration activates only when the application
    defines a ThreadSafeFory bean, and then takes precedence over every
    classpath-detected backend
  • codec-jsonb: Jakarta JSON Binding backend (JsonbCodecFactory) with Spring
    Boot auto-configuration; backend precedence is now Jackson 3 → Jackson 2 →
    Gson → JSON-B → Protobuf
  • codec-zstd: Zstandard compression transform (ZstdCodec) with configurable
    level and the same decompression-bomb cap as the built-in transforms
  • Base64Codec in codec-core: a text-safe transform (basic, URL-safe, URL-safe
    without padding, MIME) for chains whose output must live in text columns,
    JSON strings, or URLs
  • Tests proving CBOR, Smile, YAML, and XML work as Jackson backends by mapper
    swap, and a Jackson Dataformats guide documenting it

Requirements

  • Java 25. codec-zstd bundles native libraries (via zstd-jni) for the common platforms; codec-fory generates serializers at runtime and requires class registration.

0.5.0

Choose a tag to compare

@jwcarman jwcarman released this 25 Aug 16:48

Added

codec-crypto — envelope encryption as a Codec<byte[]> transform. New module, first release.

Codec<Order> codec = factory.create(Order.class)
    .andThen(new GzipCodec())
    .andThen(EnvelopeCodec.builder(provider).build());
  • AES-256-GCM with a versioned, self-describing wire format; the full header is authenticated as GCM associated data.
  • Pluggable key management through the DataKeyProvider SPI — an in-process JCE provider ships (JceDataKeyProvider, AES-KW/RFC 3394 wrapped data keys); remote KMS providers (AWS KMS, Vault, GCP KMS) are implemented by consumers against the documented contract.
  • DataKeyStrategy SPI for data-key lifecycle: a fresh key per message by default (nonce-misuse-immune by construction), with BoundedDataKeyStrategy as an opt-in cache bounded by message count and age.
  • Decode-side key admission (allowsKeyId on the provider, or an explicit builder predicate) runs before any unwrap, so a KMS the application is merely permitted to use cannot be turned into a forgery oracle.
  • A clear error taxonomy: DecryptionException means the data is bad; KeyAccessException means the key infrastructure is unavailable — never quarantine data on the latter.
  • Optional java.security.Provider injection on both builders, resolved and fail-fast checked at build time, so a FIPS-validated provider can be pinned per instance.
  • Zero external dependencies: codec-crypto depends on codec-core only.

Compress-then-encrypt is the correct composition order (.andThen(gzip).andThen(crypto)); the guide documents this and the CRIME/BREACH caveat.

Documentation: Encryption guide · Threat model

Assurance

This is new cryptographic code. Before release it was measured against external references and mechanical adversaries, not only against itself:

  • Known-answer tests against NIST CAVP GCM vectors and the RFC 3394 key-wrap vector, run through the production code paths.
  • Jazzer fuzz targets on the decoder with a committed seed corpus (replayed in every build) and a -Pfuzz profile for live fuzzing; ~48M executions with no findings.
  • PIT mutation testing gates the build: 149/149 mutants killed, verified stable across repeated runs.
  • SpotBugs with find-sec-bugs at effort Max / threshold Low, clean. One documented exclusion: the analyzer does not recognize AES key wrap as integrity-protected (RFC 3394's ICV is verified on every unwrap).
  • Two adversarial design reviews and a whole-branch final review.

Not yet done: independent human review. No outside cryptographer has examined this module. The threat model page carries the checklist for one.

Known limitations — read before storing encrypted data

Straight from the threat model, in priority order:

  1. Ciphertext substitution between records is not defended by default. Associated data is fixed per codec instance, so under one codec and KEK an attacker with datastore write access can move record A's ciphertext into record B's row and decode succeeds. Use a distinct codec per context where rows must not be interchangeable, or bind identity inside the plaintext and verify after decode.
  2. AES-GCM is not key-committing. Algorithm id 0x02 is reserved for a committing suite; it is not built. The single-wrapped-DEK envelope with keyId admission does not expose the multi-recipient surface that motivated key commitment elsewhere, but confirm this for your provider topology.
  3. Random nonces under BoundedDataKeyStrategy: up to 2^24 messages per data key (recommended 2^20). In environments that snapshot and clone VMs, duplicated RNG state can repeat a nonce under a shared cached key — prefer the default DirectDataKeyStrategy there, or roll on resume.
  4. FIPS providers: the build-time probe verifies the transform name only. A provider that rejects a key or RNG at init surfaces on first use, not at build().
  5. Data keys are not destroyed: they live as SecretKeySpec instances for the lifetime of the DataKey (and the bounded cache); only the transient generation buffer is zeroed.
  6. Timing side channels are explicitly out of scope; the uniform cryptographic-failure message covers exception content only.

Also in this release

  • CI now enforces Apache 2.0 license headers on every module, including the parentless BOM.
  • SonarCloud quality gate clean with zero open issues.

Requirements

  • Java 25. codec-crypto uses only JDK-provided JCE primitives (AES/GCM/NoPadding, AES/KW/NoPadding).

0.4.0

Choose a tag to compare

@jwcarman jwcarman released this 24 Aug 15:17
bc86b04

Breaking changes

codec-bom no longer inherits from codec-parent, and no longer re-exports Spring Boot's dependency management.

A child inherits its parent's dependencyManagement, and importing a BOM pulls in that BOM's effective dependencyManagement — after inheritance. Because codec-parent imports spring-boot-dependencies for its own build, codec-bom was re-exporting all of it.

Measured on 0.3.0:

entries
exported by codec-bom 1921
Codec's own 7
leaked from spring-boot-dependencies 1914

Importing codec-bom therefore overrode your versions for JUnit, AssertJ, Jackson, Guava, Netty and much else — silently, and in a way that surfaced far from the BOM. One consumer hit a NoSuchMethodError at JUnit test discovery caused by a partially-downgraded JUnit, which resolves cleanly and fails only at runtime.

This affected 0.1.0, 0.2.0 and 0.3.0. The BOM now manages exactly the seven Codec artifacts.

If you import codec-bom, check whether any version you relied on was actually coming from it. Anything outside org.jwcarman.codec now needs its own pin — most likely your test stack, since Spring Boot's JUnit and AssertJ versions were the ones being leaked.

Fixed

  • codec-core's published description no longer claims auto-configuration, which moved to codec-autoconfigure in 0.2.0.

Internal

  • The publish workflow passes -DprocessAllModules=true to versions:set. A parentless BOM is skipped otherwise, which would publish it still carrying -SNAPSHOT.
  • The release tag is passed through env: rather than interpolated into the shell command.

0.3.0

Choose a tag to compare

@jwcarman jwcarman released this 24 Aug 13:49

Breaking changes

Unused dependencies were removed from the published compile surface. The parent pom declared slf4j-api in <dependencies>, so it landed on every artifact including codec-bom. codec-core additionally carried spring-boot-autoconfigure, left over from before auto-configuration moved into codec-autoconfigure.

Neither was used: nothing in the codebase logs, and codec-core has no Spring imports.

codec-core now publishes with zero transitive dependencies.

If your build was relying on either of these arriving transitively through Codec, declare it directly:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>2.0.17</version>
</dependency>

Nothing in Codec's own API changed. This affects only what lands on your classpath.

Changed

  • codec-jackson2 declares jackson-core explicitly rather than inheriting it transitively from jackson-databind.
  • codec-autoconfigure declares spring-context and spring-beans, which it compiles against, plus the four engine jars it references for @ConditionalOnClass as optional dependencies. It previously relied on transitives of optional dependencies.
  • protobuf.version moved to the parent pom alongside every other version property.

Added

  • CI-only dependency hygiene gates in the ci profile: dependency:analyze-only with failOnWarning, and maven-enforcer-plugin with dependencyConvergence, requireUpperBoundDeps, banDuplicatePomDependencyVersions, and banDynamicVersions. The default build is unaffected, so contributors do not hit these locally.

Requirements

  • Spring Boot 4.1.1 (from 4.0.5)
  • protobuf-java 4.36.0 (from 4.35.0)

Documentation

  • MkDocs Material documentation site deployed to GitHub Pages, linked from the README.

0.2.0

Choose a tag to compare

@jwcarman jwcarman released this 24 Aug 03:57

Breaking changes

  • Spring Boot auto-configuration moved out of the backend modules into the new
    codec-autoconfigure module. Spring Boot applications should now depend on
    codec-spring-boot-starter plus a backend module; the backend modules
    themselves are Spring-free and no longer register anything on their own.

    Migration: add codec-spring-boot-starter alongside your existing backend
    dependency. Non-Spring users are unaffected.

  • Backend codec implementation classes (JacksonCodec, GsonCodec,
    ProtobufCodec) are now package-private. Interact with codecs through the
    Codec<T> interface returned by the factories.

  • Removed the empty *CodecProperties configuration records (codec.jackson,
    codec.gson, codec.protobuf prefixes had no properties).

Added

  • Codec composition: Codec.andThen(Codec<byte[]>) layers byte-level
    transforms (compression, encryption, ...) onto any codec, unwinding them
    automatically in reverse order on decode
  • CompressionStreamCodec base class for stream-based compression transforms,
    with built-in protection against decompression bombs (64 MiB default cap,
    tunable per instance)
  • GzipCodec (gzip/RFC 1952 framing) and DeflateCodec (zlib/RFC 1950
    framing with configurable compression level) built-in transforms
  • Jackson 2.x backend (codec-jackson2) for projects still on
    com.fasterxml.jackson
  • codec-autoconfigure module with deterministic backend precedence when
    multiple backends are present (Jackson 3 → Jackson 2 → Gson → Protobuf);
    a user-defined CodecFactory bean always wins
  • codec-spring-boot-starter bundling codec-core and codec-autoconfigure
  • Automatic-Module-Name manifest entries (org.jwcarman.codec.*) in all jars

Changed

  • The parent pom no longer inherits spring-boot-starter-parent; Spring Boot
    versions are managed via a spring-boot-dependencies BOM import and build
    plugin versions are pinned explicitly

Documentation

0.1.0

Choose a tag to compare

@jwcarman jwcarman released this 07 Apr 13:08

Codec 0.1.0

Initial release of Codec — a type-safe serialization abstraction for Java.

Highlights

  • Core SPI — Codec<T>, CodecFactory, and TypeRef<T> super type token with proper equals()/hashCode() for use as cache keys
  • Jackson backend — JSON serialization via ObjectMapper, supports generic types through TypeRef
  • Gson backend — JSON serialization via Gson, supports generic types through TypeRef
  • Protocol Buffers backend — Binary serialization using idiomatic Parser<T> API (Protobuf 4.x)
  • Spring Boot auto-configuration — Drop a backend on the classpath and get a CodecFactory bean automatically
  • BOM module — codec-bom for dependency version alignment

Requirements

  • Java 25+
  • Spring Boot 4.x

Getting Started

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.jwcarman.codec</groupId>
            <artifactId>codec-bom</artifactId>
            <version>0.1.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependency>
    <groupId>org.jwcarman.codec</groupId>
    <artifactId>codec-jackson</artifactId>
</dependency>
@Service
public class MyService {
    private final Codec<Person> codec;

    public MyService(CodecFactory factory) {
        this.codec = factory.create(Person.class);
    }
}

See the README for full documentation.