dev.constructive.eo.avro.circe
Structural Avro ↔ circe bridge (AvroJson): move between Avro generic runtime values, payload bytes and io.circe.Json with no typed case class in the middle. circe is an '''Optional''' dependency of cats-eo-avro — add circe-core to use this package.
Attributes
Members list
Type members
Classlikes
Structural Avro ↔ circe bridge: move between an Avro generic runtime value and a circe io.circe.Json document '''without''' a typed case class in the middle. The Avro mirror of dev.constructive.eo.circe. Lives inside cats-eo-avro with circe as an Optional dependency — the API surface names io.circe.Json, so any caller already depends on circe directly; add circe-core to use this package.
Structural Avro ↔ circe bridge: move between an Avro generic runtime value and a circe io.circe.Json document '''without''' a typed case class in the middle. The Avro mirror of dev.constructive.eo.circe. Lives inside cats-eo-avro with circe as an Optional dependency — the API surface names io.circe.Json, so any caller already depends on circe directly; add circe-core to use this package.
The bidirectional entry points are record, a lawful Prism[Json, IndexedRecord] per schema — getOption is the strict schema-guided parse (Json → record, misses on any shape the schema does not pin), reverseGet the total structural walk avroToJson (record → Json) — and its polymorphic diagonal family rooted at valuePrism — pPrism, bytesPrism, recordPrism and pRecord pre-compose its inputs via tearFrom / mendFrom, tearing generic values / payload bytes / records into a typed A and mending back out as Json.
==Rendering conventions (record → Json)==
avroToJson is a purely structural walk of the Avro value model:
IndexedRecord→ object (field name fromgetSchema.getFields, recurse on value, schema declaration order);java.util.Map→ object (stringify keys, recurse on values) — matchesEncoder[Map[String, ?]];java.util.List→ array (recurse);CharSequence(incl.org.apache.avro.util.Utf8) →Json.fromString;Integer/Long→Json.fromLong(Json.fromInt(i) == Json.fromLong(i.toLong));Double→Json.fromDoubleOrNull(matches circe'sEncoder[Double]);Float→Json.fromFloatOrNull(matches circe'sEncoder[Float]). NB widening float→double first would change the value (0.1f→0.10000000149…) and breakJsonNumberequality, so the float branch must not;Boolean→Json.fromBoolean; a resolvednullunion branch →Json.Null;GenericEnumSymbol→Json.fromString;ByteBuffer/GenericFixed→ array of signed byte ints (circe'sEncoder[Array[Byte]]convention);- any other runtime type →
Json.fromString(value.toString)— a lenient last resort (the walk is total), not a convention to rely on.
Unions are resolved at the value level (the runtime value IS the branch), so dispatch on the runtime type needs no union special-casing.
==Parsing conventions (Json → record, the prism's getOption)==
The parse inverts the walk under the schema, and is '''strict''' so the prism stays lawful (getOption(j).map(reverseGet) must reproduce j, so nothing may be guessed, defaulted, or dropped):
- a record object's key set must equal the schema's field names exactly — an extra key would be silently dropped on re-render, a missing one has NO schema default applied; both miss;
int/longparse viaJsonNumber.toInt/toLong— non-integral or out-of-range numbers miss;float/doubleaccept any JSON number (toFloat/toDouble);enumrequires a string among the schema's symbols;fixedrequires the exact byte length;bytes/fixedparse the signed-byte-int array rendering;- a
unionis matched '''first branch that parses wins''' (Json.Nullonly ever matches anullbranch). For the ubiquitous["null", X]this is exact; a union whose branches overlap structurally (e.g.["int", "long"]) resolves to the first — and the prism laws only hold up to that choice.
==Non-goals (deliberate)==
The bridge sees only the '''runtime''' Avro value, never the logical type or a case-class field type — so a source whose circe encoder does a '''non-structural''' transform is not reproducible structurally and the bridge is not a drop-in there. Two representative cases:
- '''logical types''': an
Instantstored as timestamp-millis is alongat runtime; the bridge rendersJson.fromLong, not the ISO-8601 string aEncoder[Instant]would emit; - '''stringified numerics''': an encoder that renders a
longas a decimal '''string''' has no structural counterpart — the bridge rendersJson.fromLong.
These are the caller's concern (post-process the Json, or decode the typed value): the walk is defined by the wire shape, not the intended semantic type.
==Drilled cursors (.json and .avro faces)==
The full AvroPrism cursor sugar — .field(_.x) / .fields(...) / .at(i) / .union[B] / .each / Dynamic selection — reaches this bridge through the json extensions on AvroPrism / AvroTraversal (drill first, flip last, like .record): reads yield the typed focus, writes render the whole modified document as Json. render is the focus-as-standalone-Json terminal for the read side. The reverse cursor is the avro extension on dev.constructive.eo.circe.JsonPrism: the drilled focus itself converts — subtree ↔ Avro binary, both directions the structural walks above. Same face family as dev.constructive.eo.avro.jsoniter, landing on the AST instead of bytes; the .avro face is why cats-eo-circe is a second Optional dependency next to circe-core.
Attributes
- Source
- AvroJson.scala
- Supertypes
-
class Objecttrait Matchableclass Any
- Self type
-
AvroJson.type
Extensions
Extensions
JSON-carried face of a drilled dev.constructive.eo.avro.AvroPrism — the circe sibling of the jsoniter module's .json face, byte-identical in shape but landing on the io.circe.Json AST: drill with the full cursor sugar (.field(_.x) / .fields(...) / .at(i) / .union[B] / Dynamic selection) and flip last, like .record:
JSON-carried face of a drilled dev.constructive.eo.avro.AvroPrism — the circe sibling of the jsoniter module's .json face, byte-identical in shape but landing on the io.circe.Json AST: drill with the full cursor sugar (.field(_.x) / .fields(...) / .at(i) / .union[B] / Dynamic selection) and flip last, like .record:
import dev.constructive.eo.avro.codecPrism
import dev.constructive.eo.avro.circe.*
codecPrism[Person].name.json.modify(_.toUpperCase)(avroBytes) // Json of the WHOLE doc,
// name uppercased
Reads (.getOption) still yield the typed focus A; every write (.modify / .replace) rebuilds the record through the record face's single-walk writer and renders it via AvroJson.avroToJson. Routing through .record is what makes .at(i) navigable (index steps are unsupported by the byte-span locate) and gives single-walk writes. A path / branch / decode Miss renders the document unchanged; genuinely malformed bytes throw at the eager parse.
Attributes
- Source
- AvroJson.scala
Avro-carried face of a drilled dev.constructive.eo.circe.JsonPrism — the reverse cursor, mirror of the jsoniter .avro face: drill a Json document with the circe cursor sugar (.field(_.x) / .at(i) / Dynamic selection) and flip last; the '''drilled focus itself''' is the unit of conversion:
Avro-carried face of a drilled dev.constructive.eo.circe.JsonPrism — the reverse cursor, mirror of the jsoniter .avro face: drill a Json document with the circe cursor sugar (.field(_.x) / .at(i) / Dynamic selection) and flip last; the '''drilled focus itself''' is the unit of conversion:
import dev.constructive.eo.circe.JsonPrism
import dev.constructive.eo.avro.circe.*
val nameA = JsonPrism[Person].name.avro // Optic[Json, Json,
// AvroBytes, AvroBytes, Affine]
nameA.getOption(jsonDoc) // Some(Avro binary of the name alone)
nameA.replace(avroName)(jsonDoc) // Json doc with the subtree spliced back
Both directions are structural, schema-directed (the AvroCodec[A] is schema evidence only — no typed A is ever materialised, matching codecPrism's doctrine): reads take the focused subtree raw (dev.constructive.eo.circe.JsonPrism.raw) and run the strict parse AvroJson.jsonToValue before encoding to Avro binary — a subtree the schema does not pin is a '''Miss''', exactly like a path miss; writes parse the Avro binary to a generic value, render it with the structural walk, and splice the subtree back. A write whose Avro bytes do not parse under the schema passes the document through unchanged (from has no failure channel).
Attributes
- Source
- AvroJson.scala