This file provides guidance to Claude Code when working with code in this repository.
- Don’t assume. Don’t hide confusion. Surface tradeoffs.
- Minimum code that solves the problem. Limit speculative additions.
- Touch only what you must, clean up only your own mess -- but do suggest additional related fixes.
- Define success criteria. Loop until verified.
Jackson Dataformats Binary is a multi-module umbrella project containing Jackson binary
format backends: CBOR, Smile, Avro, Protobuf, and Ion. Each module provides Jackson
streaming (TokenStreamFactory / JsonParser / JsonGenerator) implementations for one
binary format, plus an ObjectMapper subtype.
This file documents work on the 3.x line: package prefix tools.jackson.dataformat.*,
group id tools.jackson.dataformat, Java 17 baseline. The 2.x line lives on its own
branches, uses com.fasterxml.jackson.dataformat.*, and has different conventions —
notably <Format>Parser.Feature / <Format>Generator.Feature instead of the standalone
feature enums described below.
Active branches, newest-first, with the version each carries:
| Branch | Role |
|---|---|
2.21 |
Oldest maintained 2.x patch line; most bug fixes start here |
2.22 |
Current 2.x patch line |
2.x |
Next 2.x minor; new 2.x features and API additions |
3.1 |
Oldest maintained 3.x patch line; where 2.x lands on the Jackson 3 side |
3.2 |
Current 3.x patch line |
3.x |
Jackson 3 development tip; new features and API additions |
For the version a branch carries, read it off origin, not a local ref, which may be
stale: git show origin/<branch>:pom.xml | head -12.
Two branches look active but are not:
3.0is closed.2.xmerges into3.1now, not3.0.masteris abandoned despiteorigin/HEADpointing at it: last touched April 2025, still carrying3.0.0-rc3-SNAPSHOT. Never target it.
Changes flow forward and are never cherry-picked backward:
2.21 → 2.22 → 2.x → 3.1 → 3.2 → 3.x
Note that 2.x merges into 3.1, the oldest maintained 3.x branch, not directly into 3.x.
Pick the target branch by what the change is:
- Bug fix present in 2.x too: the oldest maintained branch that has the bug — usually
2.21. It gets merged forward from there, and reaches 3.x automatically. Fixing it directly on a 3.x branch means the 2.x releases never get it, and the next merge-forward may conflict. - Bug fix in 3-only code: the oldest maintained 3.x branch that has the bug — usually
3.1. - New feature, new API, new config flag:
3.x(and2.xif it should also ship on the 2 line). Patch branches take fixes only.
If unsure whether something counts as a fix or a feature, ask rather than guessing — targeting too new a branch means the fix never reaches released versions, and targeting too old a branch means an unwanted API change ships in a patch release.
Recent convention, matching what's on the remote:
tatu-claude/<target-branch>/<issue-number>-<short-slug>
Examples: tatu-claude/3.1/669-cbor-string-ref, tatu-claude/3.1/700-cbor-mapper-spi,
tatu-claude/3.2/22-smile-root-context-index.
The branch segment must match the branch the PR targets.
Always use the wrapper (./mvnw from root, ../mvnw from a module dir). Requires JDK 17+.
# Build all modules
./mvnw clean install
# Build without running tests
./mvnw clean install -DskipTests
# Verify all modules
./mvnw verifyPrefer running tests from inside the module directory — a -Dtest= filter run from the
root has to match in every module.
cd cbor && ../mvnw test # whole module
cd cbor && ../mvnw -Dtest=CBORParserTest test # one class
cd cbor && ../mvnw -Dtest=CBORParserTest#testSimpleArray test # one methodCoverage:
./mvnw test jacoco:report # reports land in <module>/target/site/jacoco/Each backend (avro/, cbor/, ion/, protobuf/, smile/) follows the same layout:
<format>/
├── src/main/java/
│ ├── module-info.java # JPMS descriptor: exports + provides
│ └── tools/jackson/dataformat/<format>/
│ ├── <Format>Factory.java # creates parsers/generators
│ ├── <Format>FactoryBuilder.java # builder for factory construction
│ ├── <Format>Parser.java # streaming reader
│ ├── <Format>Generator.java # streaming writer
│ ├── <Format>Mapper.java # ObjectMapper subclass; Ion differs, see below
│ ├── <Format>ReadFeature.java # format-specific parser features
│ ├── <Format>WriteFeature.java # format-specific generator features
│ └── PackageVersion.java.in # template; PackageVersion.java is generated at build time
├── src/main/resources/META-INF/services/
│ ├── tools.jackson.core.TokenStreamFactory
│ └── tools.jackson.databind.ObjectMapper
├── src/test/java/...
└── pom.xml
Registration is declared twice — once in module-info.java (provides ... with ...)
and once under META-INF/services/ for the classpath. Both must name the same classes; a
mismatch is invisible until runtime (this was issue #700).
There is no databind/ subpackage on 3.x — databind integration lives in the main package
or in format-specific subpackages (avro/schema/, protobuf/schema/, ion/ionvalue/).
All backends extend the same Jackson 3 core abstractions:
- Factory:
<Format>Factory extends BinaryTSFactory(aTokenStreamFactorysubtype — there is noJsonFactorybase class here as there is on 2.x). Configured via<Format>Factory.builder(). - Parser / Generator: the streaming layer.
<Format>Parserdecodes bytes to Jackson tokens;<Format>Generatorencodes tokens to bytes. Both are byte-oriented — character I/O (Reader/Writer) is not supported for binary formats. - Mapper: an
ObjectMappersubclass with format-specific helpers (CBORMapper.builder(),AvroMapper.schemaFor()). Named<Format>Mapperfor CBOR, Smile, Avro and Protobuf — but Ion breaks the pattern withIonObjectMapperandIonValueMapper. Check the actual class name before referencing it.
Jackson 3 throws unchecked JacksonException, not IOException: streaming methods are
declared throws JacksonException. Don't add IOException to signatures, and don't wrap
calls in try/catch just to satisfy the compiler.
- Avro: schema is mandatory. Schema generation via
AvroSchemaGenerator; schema handling inschema/. Schema evolution useswriterSchema.withReaderSchema(readerSchema).AvroJavaTimeModuleaddsjava.timesupport. - Protobuf: schema required. Schema representation in
schema/, generation inschemagen/. - CBOR & Smile: self-describing, no external schema needed. Simplest to work with.
- Ion: Amazon's format, self-describing with optional schema. Maintained by @tgregg.
These are not uniformly named — check before extending:
| Module | Base class |
|---|---|
| CBOR | CBORTestBase |
| Smile | BaseTestForSmile (async tests: AsyncTestBase) |
| Avro | AvroTestBase (interop tests: InteropTestBase) |
| Protobuf | ProtobufTestBase |
| Ion | (none — tests stand alone) |
Helpers vary by module. CBOR offers cborFactory(), cborMapper(), cborParser(byte[]),
cborGenerator(ByteArrayOutputStream), cborDoc(String json). Smile's equivalents are
smileFactory(), smileMapper(), and the underscore-prefixed _smileParser(...) /
_smileDoc(...). Avro instead exposes newMapper(), parseSchema(...), toAvro(...).
- JUnit 5 (
org.junit.jupiter.api.Test), with static-imported assertions. - Extend the module's base class where one exists, and reuse its helpers.
Tests reproducing an unfixed bug go in the module's tofix/ package and are annotated
@JacksonTestFailureExpected (from <module>/.../testutil/failure/). The annotation
inverts the result: the test fails the build if it unexpectedly passes.
When fixing such a bug, move the test out of tofix/ and drop the annotation.
Every user-visible change needs an entry, keyed by GitHub issue number. On 3.x the files
are the unsuffixed ones — VERSION-2.x and CREDITS-2.x are the frozen 2.x history
and should not be edited here:
release-notes/VERSION— one entry under the target release, e.g.#693: (avro) Incomplete number length validation in Avro decoder (for BigDecimal)release-notes/CREDITS— credit the reporter/contributor, with the fix version in parentheses on the following line.
Prefix the description with the module in parens — (avro), (cbor), (protobuf),
(smile), (ion).
mapper.reader(Type.class).with(schema) // reading
mapper.writer(schema) // writingAvro schemas can be generated from POJOs with AvroSchemaGenerator or AvroMapper.schemaFor().
New features target 3.x, not a patch branch.
- Add the constant to
<Format>ReadFeatureor<Format>WriteFeature(these are top-level enums implementingFormatFeature; on 2.x the same things are nested<Format>Parser.Feature/<Format>Generator.Feature). - Set its default state via the constructor arg picked up by
collectDefaults(). - Implement the behavior in the parser/generator.
- The factory builder's
enable/disable/configureoverloads pick it up from the enum type, so no new builder method is usually needed. - Add tests, and a release-notes entry.
This applies to CBOR, Smile, Avro and Ion. Protobuf has no feature enums at all —
there is no ProtobufReadFeature or ProtobufWriteFeature. Adding a Protobuf feature flag
means introducing that machinery first, so treat it as a larger change than the steps above
suggest.
module-info.java must export it, or it is invisible to consumers on the module path
while still working on the classpath — a discrepancy tests usually won't catch.
- Parent POM:
tools.jackson:jackson-base, pinned to the same version as this project. - Core dependency:
tools.jackson.core:jackson-core, same version line. - CI builds and tests on JDK 17, 21, and 25; the release build runs on JDK 17.
- Test heap is capped at
-Xmx1024m(<argLine>in the root pom) to surface allocation problems — don't raise it to make a test pass.
- All formats support Jackson's three API levels: streaming, databind, and tree model.
PackageVersion.javais generated fromPackageVersion.java.induring the build; edit the template, never the generated file.docs/holds generated javadocs — don't hand-edit it.