JsoniterPrism

dev.constructive.eo.jsoniter.JsoniterPrism
See theJsoniterPrism companion object
final class JsoniterPrism[A] extends Optic[Array[Byte], Array[Byte], A, A, Affine], Dynamic

Read-write optic over a JSON byte buffer. Resolves a JSONPath subset against Array[Byte], decodes the focused slice via JsonValueCodec[A] on read, encodes-and-splices on write. No runtime AST.

'''Two usage modes''' — pick deliberately:

  • '''Layer on an existing codec''' (hot-path reads): you keep materialising your case class elsewhere and use a prism for the one or two fields on the hot path.
  • '''Replace the materialised model''' (optics-as-evidence): the wire Array[Byte] IS the data structure. Instead of JsonCodecMaker.make[Whole] + decoding the whole document, hold the bytes and read/write individual fields through JsoniterPrism.apply / JsoniterPrism.field with leaf codecs only. Consuming code then follows the standard doctrine — consume via capability, construct via optic — leaving the CARRIER generic: signatures demand CanGetOption[T, WhatYouWant] (or CanModify / CanFold), never naming the optic or the wire format,
   def validateId[T](idCarrier: T)(using CanGetOption[T, Id]): Boolean

and a prism given at the use site is the evidence that instantiates T = Array[Byte]. No JsonValueCodec[Whole] is ever derived — do NOT go looking for a codec-derivation API in this module; not needing one is the point. See the migration recipe in the jsoniter integration docs.

Carrier: dev.constructive.eo.data.Affine. Shape Optic[Array[Byte], Array[Byte], A, A, Affine]:

  • type X = (Array[Byte], (Array[Byte], Int, Int))
    • Fst[X] = Array[Byte] — original source bytes (Miss carries this for pass-through)
    • Snd[X] = (Array[Byte], Int, Int) — bytes + the focused span (Hit carries this; phase-2 splice writes use the span to memcpy the new encoding back in)
  • to: Array[Byte] => Affine[X, A] runs the path scanner; on hit, decodes the slice via readFromSubArray and packs Hit(snd = (bytes, start, end), b = decoded). On miss (path doesn't resolve, decode throws), packs Miss(fst = bytes) for pass-through.
  • from: Affine[X, A] => Array[Byte] (phase 2) — Hit encodes h.b via writeToArray and splices into the source bytes at the recorded [start, end) span. Three arraycopys into a fresh buffer; cost is O(src.length). Miss returns the original bytes unchanged.

Composability: the standard cats-eo extension methods on Optic[..., Affine] light up automatically — .foldMap, .modify, .replace, .andThen, etc. No new Composer / AssociativeFunctor needed; reuses the existing Affine machinery.

Typed drilling (compile-time checked against the case-class schema, mirroring eo-circe's JsonPrism surface):

val streetP = JsoniterPrism[Person].field(_.address).field(_.street)
val street2 = JsoniterPrism[Person].address.street        // Dynamic sugar, same optic
val firstT  = JsoniterPrism[Basket].field(_.items).at(0)  // array index
val allT    = JsoniterPrism[Basket].field(_.items).each   // JsoniterTraversal

Each drilled step needs a JsonValueCodec for its focus type in scope (derive leaf codecs via JsonCodecMaker.make from jsoniter-scala-macros, which callers add themselves). To skip intermediate codecs entirely, use the string-path factory: JsoniterPrism.fromPath[String]("$.a.b") needs only the leaf codec.

'''Laws & preconditions''' (normative):

  • The Optional laws hold '''up to canonical re-encoding of the focused slice''': modify(identity) re-encodes the focus through the codec, normalising number forms (1e01.0), escapes, and key order INSIDE the span. Bytes outside the span (whitespace, sibling formatting) are never touched. Byte-for-byte identity holds only for focus slices already in the codec's canonical form.
  • '''Writes require a decodable current focus''': the Affine to decodes eagerly, so .replace onto a span whose current value doesn't decode as A is a Miss pass-through — template placeholders must be VALID encodings of the focus type.
  • Only the ROOT prism (JsoniterPrism.apply, empty path) is additionally a lawful full-cover Prism: there reverseGet is a genuine build and to misses only on undecodable input.

Attributes

Companion
object
Source
JsoniterPrism.scala
Graph
Supertypes
trait Dynamic
trait Optic[Array[Byte], Array[Byte], A, A, Affine]
class Object
trait Matchable
class Any

Members list

Grouped members

Operations

inline def cross[C, D](o: Optic[Array[Byte], Array[Byte], C, D, Affine])(using Accessor[Affine], ReverseAccessor[Affine]): Optic[A, A, C, D, Affine]

Build-then-observe across the build-output ⇄ read-input seam, preserving structure, on a shared carrier F. Flip self (it must be reversible — Accessor[F] and ReverseAccessor[F], i.e. an Iso or Review over Direct) so it reads T from B, then andThen that under the same carrier. The result is the full Optic[B, A, C, D, F], not a collapsed getter: its read capability follows the carrier (.get for Direct), and self's read focus A survives as the composite's write-back focus.

Build-then-observe across the build-output ⇄ read-input seam, preserving structure, on a shared carrier F. Flip self (it must be reversible — Accessor[F] and ReverseAccessor[F], i.e. an Iso or Review over Direct) so it reads T from B, then andThen that under the same carrier. The result is the full Optic[B, A, C, D, F], not a collapsed getter: its read capability follows the carrier (.get for Direct), and self's read focus A survives as the composite's write-back focus.

This is exactly self.reverse.andThen(that). The motivating case is ana.cross(cata): a Review (the unfold) crossed with a getter on the built S (the fold) — a (materializing) hylomorphism whose .get reads the folded value. When that sits on a different carrier (a Prism, a Fold, …), the cross-carrier cross overload below is selected instead.

Seam: that's source is self's T and its back-type is self's S.

Attributes

Inherited from:
Optic
Source
Optic.scala

Type members

Types

type X = (Array[Byte], (Array[Byte], Int, Int))

Existential leftover carried alongside the focus — the type-level witness the carrier uses to rebuild T. Concrete at construction (Lens.apply sets X = S, Prism.apply sets X = S, …) and abstract when the optic is bound to Optic[…, F] without refinement.

Existential leftover carried alongside the focus — the type-level witness the carrier uses to rebuild T. Concrete at construction (Lens.apply sets X = S, Prism.apply sets X = S, …) and abstract when the optic is bound to Optic[…, F] without refinement.

Attributes

Source
JsoniterPrism.scala

Value members

Concrete methods

transparent inline def at(i: Int): Any
Extension method from JsoniterPrism

.at(i) — drill into the i-th array element.

.at(i) — drill into the i-th array element.

Attributes

Source
JsoniterPrism.scala
transparent inline def each: Any
Extension method from JsoniterPrism

.each — split into a JsoniterTraversal over the iterated array.

.each — split into a JsoniterTraversal over the iterated array.

Attributes

Source
JsoniterPrism.scala
transparent inline def field[B](inline selector: A => B)(using codecB: JsonValueCodec[B]): JsoniterPrism[B]
Extension method from JsoniterPrism

.field(_.x) — drill via selector lambda, compile-time checked against A's case fields.

.field(_.x) — drill via selector lambda, compile-time checked against A's case fields.

Attributes

Source
JsoniterPrism.scala
def from(aff: Affine[X, A]): Array[Byte]

Close the carrier: given a modified focus B (and the leftover X already inside the F), reassemble the result T.

Close the carrier: given a modified focus B (and the leftover X already inside the F), reassemble the result T.

Attributes

Source
JsoniterPrism.scala
def raw: JsoniterPrism[Array[Byte]]

Re-focus this prism on the '''raw encoded slice''': same path, same span scan, but the focus is the focused value's own JSON bytes — captured verbatim on read (readRawValAsBytes), spliced verbatim on write (writeRawVal). No typed decode ever runs. The seam for format-level consumers that transform the slice as a document of its own — e.g. the avro bridge's structural .avro face (dev.constructive.eo.avro.jsoniter).

Re-focus this prism on the '''raw encoded slice''': same path, same span scan, but the focus is the focused value's own JSON bytes — captured verbatim on read (readRawValAsBytes), spliced verbatim on write (writeRawVal). No typed decode ever runs. The seam for format-level consumers that transform the slice as a document of its own — e.g. the avro bridge's structural .avro face (dev.constructive.eo.avro.jsoniter).

Attributes

Source
JsoniterPrism.scala
def reverseGet(a: A): Array[Byte]

Encode a standalone. Lawful only for the ROOT prism (a real Prism.reverseGetArray[Byte] A whole-document); on a drilled prism it cannot restore siblings, which is why the Optic seam carries the source bytes instead of calling this.

Encode a standalone. Lawful only for the ROOT prism (a real Prism.reverseGetArray[Byte] A whole-document); on a drilled prism it cannot restore siblings, which is why the Optic seam carries the source bytes instead of calling this.

Attributes

Source
JsoniterPrism.scala
transparent inline def selectDynamic(inline name: String): Any

Dynamic field sugar — JsoniterPrism[Person].name lowers to JsoniterPrism[Person].field(_.name). Compile-time checked against A's case fields; the field's JsonValueCodec is summoned at the call site.

Dynamic field sugar — JsoniterPrism[Person].name lowers to JsoniterPrism[Person].field(_.name). Compile-time checked against A's case fields; the field's JsonValueCodec is summoned at the call site.

Attributes

Source
JsoniterPrism.scala
def to(bytes: Array[Byte]): Affine[X, A]

Push the source S into the carrier, extracting the focus A and packing the leftover X. Paired with from to reconstruct T.

Push the source S into the carrier, extracting the focus A and packing the leftover X. Paired with from to reconstruct T.

Attributes

Source
JsoniterPrism.scala

Inherited methods

def andThen[C, IB, G[_, _]](inner: Optic[A, Unit, C, IB, G])(using rc: ReadCompose[Affine, G]): rc.Out[Array[Byte], C]

ANY outer ∘ read-only inner — the inner is honestly one-way (T = Unit: a Getter, AffineFold, or Fold), so only the two READ sides matter and the composite collapses to the read-only join of their strengths via compose.ReadCompose (Getter / PickFold / ForgetFold).

ANY outer ∘ read-only inner — the inner is honestly one-way (T = Unit: a Getter, AffineFold, or Fold), so only the two READ sides matter and the composite collapses to the read-only join of their strengths via compose.ReadCompose (Getter / PickFold / ForgetFold).

A trait member (not an extension in the companion) deliberately: once a receiver is statically one of the fused concrete classes, its andThen member overloads enter resolution and Scala 3 never falls back to extension methods when they all fail — the collapse must be in the member overload set to be reachable without an expected-type ascription.

Only the inner's T is pinned to Unit; its B stays free (IB) even though read-only inners always have B = Unit. That keeps this overload strictly LESS specific than the same-carrier andThen above (which accepts every argument this one does whenever B = Unit at the receiver), so a same-carrier read-only ∘ read-only call resolves unambiguously to the AssociativeFunctor path and this one fires exactly on the cross-seam cells the generic member cannot type.

Attributes

Inherited from:
Optic
Source
Optic.scala
inline def andThen[C, D](o: Optic[A, A, C, D, Affine]): Optic[Array[Byte], Array[Byte], C, D, Affine]

Compose with another optic under the shared carrier F. Requires AssociativeFunctor[F]. Cross-carrier composition (Lens → Optional, Lens → Traversal, …) goes through the Morph-summoning overload of this same method.

Compose with another optic under the shared carrier F. Requires AssociativeFunctor[F]. Cross-carrier composition (Lens → Optional, Lens → Traversal, …) goes through the Morph-summoning overload of this same method.

Attributes

Example
case class Address(street: String)
case class Person(address: Address)
val streetLens = lens[Person](_.address).andThen(lens[Address](_.street))
Inherited from:
Optic
Source
Optic.scala