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

object 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.

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 valuePrismpPrism, 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 from getSchema.getFields, recurse on value, schema declaration order);
  • java.util.Map → object (stringify keys, recurse on values) — matches Encoder[Map[String, ?]];
  • java.util.List → array (recurse);
  • CharSequence (incl. org.apache.avro.util.Utf8) → Json.fromString;
  • Integer / LongJson.fromLong (Json.fromInt(i) == Json.fromLong(i.toLong));
  • DoubleJson.fromDoubleOrNull (matches circe's Encoder[Double]);
  • FloatJson.fromFloatOrNull (matches circe's Encoder[Float]). NB widening float→double first would change the value (0.1f0.10000000149…) and break JsonNumber equality, so the float branch must not;
  • BooleanJson.fromBoolean; a resolved null union branch → Json.Null;
  • GenericEnumSymbolJson.fromString;
  • ByteBuffer / GenericFixed → array of signed byte ints (circe's Encoder[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 / long parse via JsonNumber.toInt / toLong — non-integral or out-of-range numbers miss; float / double accept any JSON number (toFloat / toDouble);
  • enum requires a string among the schema's symbols; fixed requires the exact byte length; bytes / fixed parse the signed-byte-int array rendering;
  • a union is matched '''first branch that parses wins''' (Json.Null only ever matches a null branch). 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 Instant stored as timestamp-millis is a long at runtime; the bridge renders Json.fromLong, not the ISO-8601 string a Encoder[Instant] would emit;
  • '''stringified numerics''': an encoder that renders a long as a decimal '''string''' has no structural counterpart — the bridge renders Json.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
Supertypes
class Object
trait Matchable
class Any
Self type
AvroJson.type