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 ofJsonCodecMaker.make[Whole]+ decoding the whole document, hold the bytes and read/write individual fields through JsoniterPrism.apply /.fieldwith leaf codecs only. Consuming code then follows the standard doctrine — consume via capability, construct via optic — leaving the CARRIER generic: signatures demandCanGetOption[T, WhatYouWant](orCanModify/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 viareadFromSubArrayand packsHit(snd = (bytes, start, end), b = decoded). On miss (path doesn't resolve, decode throws), packsMiss(fst = bytes)for pass-through.from: Affine[X, A] => Array[Byte](phase 2) — Hit encodesh.bviawriteToArrayand splices into the source bytes at the recorded[start, end)span. Threearraycopys 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
'''In place.''' A document inside a larger buffer (one network read holding several messages, say) is read where it lies by the in-place face: prism.inPlace — or JsoniterPrism.inPlace[A], to drill from there — is the same prism over a JsonSlice (JsonSlice(bytes, offset, length), a view, nothing copied), with the same laws. This prism's whole-array scan and splice are the (0, length) case of the in-place face's. See JsoniterInPlacePrism for what not copying asks of the caller, and what writes return.
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] needs only the leaf codec, and returns the prism in an Either (a path is data).
'''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 (1e0→1.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
todecodes eagerly, so.replaceonto a span whose current value doesn't decode asAis a Miss pass-through — template placeholders must be VALID encodings of the focus type. - '''A write that fails to ENCODE the new value is a silent pass-through''' (
fromis total by type — there is nowhere inOptic.fromto put the failure). On a drilled prism the original slice survives and the call reports success. Pinned byJsoniterPrismWriteSpec; seedocs/research/2026-09-22-exception-audit.md(class C) for the carrier-level fix, tracked as issue #117. - Only the ROOT prism (JsoniterPrism.apply, empty path) is additionally a lawful full-cover Prism: there reverseGet is a genuine build and
tomisses only on undecodable input.
Attributes
- Companion
- object
- Source
- JsoniterPrism.scala
- Graph
-
- Supertypes
-
trait Dynamictrait Serializableclass Objecttrait Matchableclass Any