Baboon's generated JSON codecs aim for predictable, deterministic shapes that round‑trip across all supported backends: Scala (Circe), C# (System.Text.Json), Rust (serde), TypeScript, Python, Kotlin (Jackson), Java (Jackson), and Dart (dart:convert). This document explains the layout and corner cases so you can interoperate or write custom codecs.
- Field names match schema identifiers verbatim.
- Collections are JSON arrays.
- Maps are JSON objects; keys are stringified (see “Map keys”).
- Options emit
null/omission vs. value depending on target:- Scala:
Nonebecomesnull;Someencodes the wrapped value. - C#:
nullreference is encoded as JSONnull; value types useWriteOptionValand producenullor the wrapped value.
- Scala:
- ADT branches are represented as their branch object; when
wrappedAdtBranchCodecsis enabled for a target, branches are wrapped as{ "<TypeName>": { ...payload... } }. - Primitives use native JSON numbers/booleans/strings; 64‑bit unsigned become decimal strings to avoid loss of precision in JavaScript.
bit→ JSON boolean.- Signed ints/floats → JSON numbers.
- Unsigned ints:
u08/u16→ JSON numbers.u32→ JSON number in Scala, number in C# (fits 53 bits).u64→ JSON string (Scala usestoUnsignedBigInt, C# usesBigInteger), because it may exceed JS safe integer range.
f128→ JSON number viaBigDecimal/decimal.str→ JSON string (UTF‑8).bytes→ Base64 string in C# (ByteString.Encode()), and.Parseon read.uid→ JSON string (canonical GUID string).tsu/tso→ JSON string usingBaboonTimeFormats(formatTsu/formatTsoon Scala,ToStringon C#).
Map keys are always strings in JSON. The string form depends on the key type:
- Builtins →
ToString/parseof the scalar (timestamps use formatted strings; booleans/numbers use culture‑invariant formats). - Enums → encoded enum value to string.
- Foreign types (when allowed as keys) → encoded using their codec and then
ToString(). - Other user types are not permitted as map keys.
- Branch selection is structural: the caller chooses which branch codec to call. With
wrappedAdtBranchCodecs=truethe encoder wraps branches in a single‑entry object keyed by the branch name; the decoder unwraps accordingly. - Contract fields are enforced at typechecking; codecs simply serialize the resulting field set.
- Arrays and objects are written in declaration order of fields.
- No whitespace/pretty printing is emitted; callers can post‑process if needed.
- Missing required fields throw during decode; unexpected fields are ignored by generated decoders (runtime may differ per target version).
Generated codecs contain placeholder instances for foreign types; you must override them (e.g. BaboonCodecs#Register in C#, BaboonCodecs.register in Scala) with real implementations that follow these conventions. Refer to the foreign type registration mechanism for your target language.
Schema:
data User { id: uid, name: str, age: opt[i32], tags: lst[str] }
Value:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Ada",
"age": 42,
"tags": ["core","beta"]
}
If age is missing/null, it decodes to None/null depending on target.
Schema:
adt PaymentMethod {
data Card { pan: str }
data Wallet { provider: str }
}
- Default (no wrapping): encode a
Cardbranch as{ "pan": "1234" }. - With
wrappedAdtBranchCodecs=true: encode as{ "Card": { "pan": "1234" } }.
Schema:
data Inventory {
stock: map[u64, u32]
}
Value:
{
"stock": {
"18446744073709551615": 10,
"42": 5
}
}
u64 keys and values are stringified when needed to preserve precision.