dev.constructive.eo.jsoniter

Byte-level JSON optics over jsoniter-scala: JsoniterPrism and JsoniterTraversal focus a JSONPath '''directly in the encoded byte buffer''' — scanning for the span, decoding only the focused slice, and splicing writes back — so reads and writes never materialise the full document AST.

Attributes

Members list

Grouped members

Scanner

Hand-rolled JSON byte scanner. Resolves a PathStep list against an Array[Byte] JSON document and returns the byte span [start, end) of the resolved value, or -1 for the start if the path doesn't match.

Hand-rolled JSON byte scanner. Resolves a PathStep list against an Array[Byte] JSON document and returns the byte span [start, end) of the resolved value, or -1 for the start if the path doesn't match.

Why hand-rolled rather than reusing jsoniter-scala-core's JsonReader: the scanner only needs to skip-with-backtracking, not decode. Embedding JsonReader would force us to thread its mark / rollback state through every call site, and its API doesn't expose byte offsets directly. 100-ish LoC of dispatch is cheaper than the impedance mismatch.

The scanner is permissive about whitespace (skips it everywhere) and strict about structure (unbalanced quotes / brackets short-circuit to "miss"). It does NOT validate the document; an invalid JSON whose path step happens to resolve will return a span — decoding is the layer that surfaces validity failures.

Attributes

Source
JsonPathScanner.scala
Supertypes
class Object
trait Matchable
class Any
Self type

Parser

object PathParser

Parser for the JSONPath subset we accept in JsoniterPrism / JsoniterTraversal. Grammar:

Parser for the JSONPath subset we accept in JsoniterPrism / JsoniterTraversal. Grammar:

 path  := '$' (step)*
 step  := '.' ident | '[' int ']' | '[*]'
 ident := [A-Za-z_][A-Za-z_0-9]*
 int   := [0-9]+

[*] is the wildcard step — only meaningful inside a multi-focus JsoniterTraversal; JsoniterPrism rejects paths containing it at construction. Filters / recursive descent are still out of scope.

Attributes

Source
PathParser.scala
Supertypes
class Object
trait Matchable
class Any
Self type
PathParser.type

Optics

object JsoniterPrism

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.

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.

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.

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

Attributes

Source
JsoniterPrism.scala
Supertypes
class Object
trait Matchable
class Any
Self type

Read-WRITE Traversal over JSON byte buffers. Resolves a wildcard-bearing JSONPath against Array[Byte], decodes each matched span via JsonValueCodec[A] on read, and encodes-and- splices every focus back in one pass on write. No runtime AST.

Read-WRITE Traversal over JSON byte buffers. Resolves a wildcard-bearing JSONPath against Array[Byte], decodes each matched span via JsonValueCodec[A] on read, and encodes-and- splices every focus back in one pass on write. No runtime AST.

Carrier: MultiFocus[PSVec] — the same carrier Traversal.each uses, peer to all the classical Traversal idioms. The choice over MultiFocus[List] matters for downstream composition: JsoniterTraversal[A].andThen(Traversal.each[List, A]) and similar same-carrier chains work via the existing mfAssocPSVec AssociativeFunctor instance, with zero-copy per-element reassembly. A List-carrier traversal would force a Composer hop or a manual .modify rebuild.

  • type X = (Array[Byte], List[JsonPathScanner.Span]) — original bytes + each KEPT focus's span, 1:1 aligned with the foci (spans whose decode throws are dropped TOGETHER with their focus, so the write side can trust the pairing).
  • to: Array[Byte] => (X, PSVec[A]) — runs JsonPathScanner.findAll, decodes every span via the codec into a single Array[AnyRef] allocation, wraps it as a PSVec. Spans whose decode throws are silently dropped (matches the read semantic "fold the focuses that exist") — and stay byte-untouched on write.
  • from: ((X, PSVec[A])) => Array[Byte] — encodes each focus via writeToArray and splices all spans back in a single pass (segments between spans are arraycopyd verbatim). A span↔foci length mismatch (an aggregate from after a shape-changing carrier op) passes the original bytes through unchanged, as does an empty focus set.

Composability: standard cats-eo extensions on Optic[..., MultiFocus[PSVec]] light up automatically via mfFold[PSVec], mfFunctor[PSVec], mfAssocPSVec, etc. — .foldMap, .modify, .headOption, .length, .exists, .modifyA, same-carrier .andThen all work without anything new shipping in this module. Mirrors dev.constructive.eo.avro.AvroTraversal's byte-carried write semantics. JsoniterPrism's '''Laws & preconditions''' apply verbatim: laws hold up to canonical re-encoding of the focused slices, and writes need decodable current focuses.

Attributes

Source
JsoniterTraversal.scala
Supertypes
class Object
trait Matchable
class Any
Self type

AST

enum PathStep

One step of a JSON path expression:

One step of a JSON path expression:

  • .field — descend into an object property by exact name (powers JsoniterPrism).
  • [i] — descend into an array element by zero-based index (powers JsoniterPrism).
  • [*] — fan out across every element of the current array (powers JsoniterTraversal).

Filters / recursive descent are out of scope.

Attributes

Source
PathStep.scala
Supertypes
trait Enum
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all