-
Notifications
You must be signed in to change notification settings - Fork 1
Codecs
What this page covers: the Codec<V> serialization SPI — the built-in JacksonJsonCodec
(compact vs pretty) and JacksonYamlCodec, why isJsonCodec() decides which backends accept a
codec, and how to write your own. (The ref-aware codec for the manager add-on lives elsewhere — see
the note at the end.)
import br.com.finalcraft.everydatabase.codec.Codec;
import br.com.finalcraft.everydatabase.codec.JacksonJsonCodec;
import br.com.finalcraft.everydatabase.codec.JacksonYamlCodec;
// Compact JSON — the default; what a database wants.
Codec<PlayerData> json = new JacksonJsonCodec<>(PlayerData.class);
// Indented JSON — for human-readable LocalFile output.
Codec<PlayerData> pretty = JacksonJsonCodec.pretty(PlayerData.class);
// YAML — LocalFile only.
Codec<PlayerData> yaml = new JacksonYamlCodec<>(PlayerData.class);
// Attach one to the descriptor:
EntityDescriptor.builder(UUID.class, PlayerData.class)
.collection("players")
.keyExtractor(PlayerData::getUuid)
.codec(json) // <- the serialization strategy
.build();A Codec is just entity ↔ bytes plus a content type. The backend takes it from there.
public interface Codec<V> {
byte[] encode(V value) throws CodecException;
V decode(byte[] data) throws CodecException;
String contentType(); // e.g. "application/json", "application/yaml"
default boolean isJsonCodec() { return contentType().contains("json"); }
default String fileExtension(){ /* "json" / "yml" derived from contentType() */ }
}-
encode/decodeare the round-trip; failures throwCodecException. -
contentType()is the MIME type of the encoded form. Backends that store raw bytes (LocalFile) use it to log/pick a format; the two defaults below are derived from it. -
isJsonCodec()is the backend-compatibility seam (next section). -
fileExtension()drives per-entity filenames in LocalFile ("json"→players/<key>.json,"yml"→.yml).
This single predicate decides where a codec is allowed to run. Several backends — SQL, MongoDB, InMemory — parse or store the payload as native JSON, so they require a JSON codec. Only LocalFile treats the payload as opaque bytes, so it's the one backend that also accepts YAML (or anything else).
| Backend | Accepts | Why |
|---|---|---|
| MySQL / MariaDB | JSON only | stored in a native JSON column |
| PostgreSQL | JSON only | stored in a native JSON column |
| H2 | JSON only | stored as JSON text in a TEXT column |
| MongoDB | JSON only | parsed into a native BSON sub-document |
| InMemory | JSON only | parses/stores the payload as JSON |
| LocalFile | JSON or YAML | opaque bytes, format is the codec's business |
// ✗ A YAML codec on a JSON-requiring backend is rejected.
Storage sql = Storages.createSQL(sqlConfig);
sql.repository(EntityDescriptor.builder(UUID.class, PlayerData.class)
.collection("players")
.keyExtractor(PlayerData::getUuid)
.codec(new JacksonYamlCodec<>(PlayerData.class)) // not JSON -> IllegalArgumentException
.build());
// ✓ The same YAML codec is fine on LocalFile.
Storage file = Storages.createLocalFile(new LocalFileConfig(Path.of("data")));
file.repository(/* ...descriptor with the YAML codec... */);
⚠️ Gotcha — pairing a non-JSON codec with SQL, Mongo, or InMemory fails withIllegalArgumentException. If you write a custom codec whosecontentType()doesn't contain the string"json"but does emit JSON, overrideisJsonCodec()to returntrueor those backends will reject it.
The default output is compact (no indentation) — databases persist or re-parse the payload
verbatim, so pretty-print whitespace would only inflate storage and I/O. For human-readable
per-entity files in LocalFile, use the pretty(...) factory:
Codec<PlayerData> compact = new JacksonJsonCodec<>(PlayerData.class); // {"uuid":"…","name":"Alice","score":100}
Codec<PlayerData> nice = JacksonJsonCodec.pretty(PlayerData.class); // indented, multi-lineBoth report contentType() application/json, so both are accepted by every backend. The pretty
variant is meant for LocalFile, where a human opens the files.
The default mapper is batteries-included (see the profiles below),
so java.time, Optional, etc. already round-trip with no setup. Pass your own ObjectMapper only
for truly custom needs — or apply an EveryDatabase profile to your own mapper so it keeps the
storage defaults:
ObjectMapper mapper = JacksonConfig.storageSafe(new JsonMapper());
mapper.registerModule(new MyCustomModule()); // your extras, on top of the defaults
Codec<PlayerData> codec = new JacksonJsonCodec<>(PlayerData.class, mapper);📌 Note — Jackson here is 2.x (
com.fasterxml.jackson,jackson-databind). Keep your imports on thecom.fasterxml.jackson.*packages — not thetools.jacksonpackages of Jackson 3.x. Versions live on Dependency Versions & Overrides.
new JacksonJsonCodec<>(Type.class) (and the YAML codec) build their mapper with
JacksonConfig.storageSafe(...) — a batteries-included default tuned for storage:
-
java.time+Optionalmodules registered (Instant,LocalDate,Duration,Optional<T>, … just work); - dates as ISO-8601 text, not numeric epochs;
-
map entries ordered alphabetically by key → deterministic, diff-friendly bytes regardless of the
Mapimplementation or insertion order; - unknown properties tolerated on read, so a field removed in newer code doesn't break old stored data.
Three profiles are available via JacksonConfig; apply any of them to a JSON or YAML mapper
(YAMLMapper extends ObjectMapper):
| Behaviour | baseReadContract |
storageSafe (default) |
compact |
|---|---|---|---|
Instant / absolute dates |
numeric epoch | ISO-8601 text | ISO-8601 text |
LocalDate / LocalDateTime
|
numeric array [y,m,d,…]
|
ISO-8601 text | ISO-8601 text |
Duration |
numeric | ISO-8601 (PT1M30S) |
ISO-8601 |
| Map key order | alphabetical (sorted by key) | alphabetical (sorted by key) | alphabetical (sorted by key) |
null properties |
kept | kept | dropped |
empty Optional
|
kept (as null) |
kept (as null) |
dropped |
| Typical use | the shared read contract | everything | hot / large collections |
All three share one frozen read contract (the modules + tolerate-unknown + the alphabetical map
ordering). storageSafe and compact differ only in that compact omits null/absent properties
(JsonInclude.Include.NON_ABSENT) — same dates, same ordering. That makes them fully
interchange-compatible: any profile reads what any other wrote, so you can switch a collection
between storageSafe and compact without migrating data.
// All three accept any ObjectMapper subtype and return it, configured:
ObjectMapper safe = JacksonConfig.storageSafe(new JsonMapper()); // the codec default
ObjectMapper small = JacksonConfig.compact(new JsonMapper()); // drops null/absent
ObjectMapper yamlSafe= JacksonConfig.storageSafe(new YAMLMapper()); // same rules, YAML syntax🔎 Indexing uses the codec's own mapper. A Jackson codec exposes its
ObjectMapperthrough theObjectMapperAwarecapability, so the secondary-index tree is built with the same configuration the entity is persisted with — the indexed form of a field can never disagree with the stored form. A non-Jackson codec simply doesn't implement it, and indexing falls back to the default mapper.
Codec<PlayerData> yaml = new JacksonYamlCodec<>(PlayerData.class); // contentType() = "application/yaml"It reports application/yaml, so isJsonCodec() is false and fileExtension() is "yml".
That's exactly what lets LocalFile write players/<key>.yml, and exactly what stops you attaching it
to a JSON-requiring backend. Use it when you want config-style, hand-editable files on disk.
Implement the four methods (two are usually fine via the defaults). The contract:
public final class GzipJsonCodec<V> implements Codec<V> {
private final JacksonJsonCodec<V> delegate;
GzipJsonCodec(Class<V> type) { this.delegate = new JacksonJsonCodec<>(type); }
@Override public byte[] encode(V value) { return gzip(delegate.encode(value)); }
@Override public V decode(byte[] data) { return delegate.decode(gunzip(data)); }
@Override public String contentType() { return "application/json+gzip"; }
// contentType() still contains "json", so the default isJsonCodec() returns true.
// If it did NOT, you'd override it:
// @Override public boolean isJsonCodec() { return true; }
}Three rules for a well-behaved codec:
-
Round-trip faithfully —
decode(encode(v))must reconstruct an equal entity. -
Throw
CodecExceptionon (de)serialization failure, so the error propagates as the library expects. -
Get
isJsonCodec()right — if a JSON-requiring backend must accept your codec, ensure it returnstrue(either via the"json"substring convention or an explicit override).
💡 Tip —
fileExtension()only matters if you'll use the codec with LocalFile. Override it when yourcontentType()doesn't map cleanly to a conventional extension.
See Extending the Library for the broader set of extension seams.
If your entities hold Ref fields (the optional Caching & References), don't
attach a plain JacksonJsonCodec — a Ref needs Jackson taught to write it as just its key and
recover the target type on read. That ref-aware codec is RefCodecs.json(...), vended by a
RefRegistry (registry.codec(Type.class)). It lives in the everydatabase-manager module —
not in core — precisely because :core must not depend on :manager. A leaf entity with no
Ref fields can keep using JacksonJsonCodec.
// Manager module only — for entities that contain Ref fields.
RefRegistry refRegistry = new RefRegistry();
EntityDescriptor.builder(UUID.class, Player.class)
.collection("players")
.keyExtractor(Player::getUuid)
.codec(refRegistry.codec(Player.class)) // ref-aware; from everydatabase-manager
.build();Full story: Caching & References.
- Entities, Keys & Collections — what your entity must look like for a codec to serialize it.
- Defining Entities — attaching a codec on the descriptor builder.
- Architecture Overview — where the codec sits between repository and backend.
- Choosing a Backend — which backends require JSON vs accept YAML, and data-at-rest formats.
-
Caching & References —
RefCodecsand why the ref-aware codec lives in the manager module. - Extending the Library — custom codecs and other extension seams.
- Dependency Versions & Overrides — the Jackson 2.x version and how to override it.
EveryDatabase · Home · made by Petrus Pradella
Getting Started
Core Concepts
Working with Data
Backends
Manager Module
- Caching & References
- Typed References (Ref)
- Caching Managers
- Cache Policies & Freshness
- Cross-Process Cache Sync
- One Entity, Many Databases
Operations
Advanced
Reference
Contributing