AvroJson
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.
Attributes
- Source
- AvroJson.scala
- Graph
-
- Supertypes
-
class Objecttrait Matchableclass Any
- Self type
-
AvroJson.type