dev.constructive.eo.circe

Cross-representation optics bridging native Scala types and their circe-serialised form.

The entry point is JsonPrism.apply (aliased as codecPrism for the read-aloud API):

 val personPrism: JsonPrism[Person] = codecPrism[Person]
 val streetPrism: JsonPrism[String] =
   personPrism.field(_.address).field(_.street)
 streetPrism.modify(_.toUpperCase)(personJson)
 // → Ior.Right of the same Json, with .address.street upper-cased —
 //   no Person ever materialised.

From a root prism the whole surface lights up: .field (and its Dynamic sugar) drills deeper, .at(i) indexes into an array, .each widens to a JsonTraversal over every element, .fields(_.a, _.b) focuses a bundle of case fields as a Scala 3 NamedTuple. Independent of the path-walkers, platedJson makes Json a recursive self-traversal for whole-document rewrites.

Every operation comes in two tiers: the default methods return Ior[Chain[JsonFailure], _]Ior.Both is partial success, every skip documented in the chain — and the *Unsafe siblings are the silent pass-through hot path.

Coming from circe-optics JsonPath or raw cursors: this is the compile-time-checked equivalent — codecPrism[Person].address.streetroot.address.street.string ≈ an hcursor.downField chain, with typed per-step codecs instead of stringly paths.

Attributes

Members list

Type members

Classlikes

Structured failure surfaced by the default Ior-bearing surface of JsonPrism / JsonTraversal.

Structured failure surfaced by the default Ior-bearing surface of JsonPrism / JsonTraversal.

Every case carries a PathStep so the walk that produced the failure can point at the specific cursor position that refused. The default enum toString keeps the structural representation for testability; message gives a human-readable diagnostic.

Attributes

Companion
object
Source
JsonFailure.scala
Supertypes
trait Enum
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
object JsonFailure

Attributes

Companion
enum
Source
JsonFailure.scala
Supertypes
trait Sum
trait Mirror
class Object
trait Matchable
class Any
Self type
object JsonPrism

Attributes

Companion
class
Source
JsonPrism.scala
Supertypes
class Object
trait Matchable
class Any
Self type
JsonPrism.type
final class JsonPrism[A] extends Optic[Json, Json, A, A, Affine], Dynamic

Specialised optic from io.circe.Json to native A.

Specialised optic from io.circe.Json to native A.

 JsonPrism[A]  Json)  // Fst = source (miss); Snd = single-walk writer

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

  • '''Layer on an existing model''' (hot-path edits): you keep decoding 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 Json — or the wire String, parsed on the fly — IS the data structure. json.as[Whole] never runs — do NOT go looking for a whole-document decode step; not needing one is the point. Hold the Json and read/write individual fields through codecPrism with leaf codecs only. Consuming code then follows the standard doctrine — consume via capability, construct via optic — leaving the CARRIER generic:
   def widenStreet[T](t: T)(using cm: CanModify[T, String]): T = cm.replace("Broadway")(t)

and a JsonPrism given at the use site is the evidence that instantiates T = Json. See the migration recipe in the circe integration docs.

'''An Optional, not a Prism.''' A drilled focus lives INSIDE a document, so rebuilding needs the siblings — which a Prism's from(reverseGet) cannot see (it gets the focus alone). Carrying the source on the Affine seam fixes that at the carrier level: to captures a writer over the walk it already did, and from(Hit) applies it — so .modify / .replace preserve siblings whether called directly, upcast to Optic[…, Affine], or composed via andThen. Only the root full-cover prism is also a lawful Prism, and reverseGet stays for it. Mirrors dev.constructive.eo.avro.AvroRecordPrism.

Two call-surface tiers:

  • Default Ior-bearing: modify / get etc. accumulate Chain[JsonFailure]; partial success surfaces as Ior.Both(chain, inputJson).
  • *Unsafe: silent pass-through hot path.

Storage decomposition: a JsonPrism[A] holds a JsonFocus (Leaf vs Fields).

Attributes

Companion
object
Source
JsonPrism.scala
Supertypes
trait Dynamic
trait Optic[Json, Json, A, A, Affine]
class Object
trait Matchable
class Any

Macros backing JsonPrism.field(_.fieldName), selectDynamic, at(i), each, fields. Extracts the field name from the selector AST (same pattern as eo-generics' lens[S](_.field)) and emits a widenPath call.

Macros backing JsonPrism.field(_.fieldName), selectDynamic, at(i), each, fields. Extracts the field name from the selector AST (same pattern as eo-generics' lens[S](_.field)) and emits a widenPath call.

 val streetPrism: JsonPrism[String] =
   codecPrism[Person].field(_.address).field(_.street)

Attributes

Source
JsonPrismMacro.scala
Supertypes
class Object
trait Matchable
class Any
Self type
final class JsonTraversal[A] extends Dynamic

Multi-focus counterpart to JsonPrism: walks the JSON to some array, then applies the focus update to every element. Two pieces: prefix: Array[PathStep] (root-to-array, walked once) and focus: JsonFocus[A] (per-element). The Leaf-vs-Fields split lives in focus.

Multi-focus counterpart to JsonPrism: walks the JSON to some array, then applies the focus update to every element. Two pieces: prefix: Array[PathStep] (root-to-array, walked once) and focus: JsonFocus[A] (per-element). The Leaf-vs-Fields split lives in focus.

Two tiers (Ior-bearing default + *Unsafe), same shape as JsonPrism.

Attributes

Companion
object
Source
JsonTraversal.scala
Supertypes
trait Dynamic
class Object
trait Matchable
class Any
object JsonTraversal

Attributes

Companion
class
Source
JsonTraversal.scala
Supertypes
class Object
trait Matchable
class Any
Self type
enum PathStep

One step on a JsonPrism's flat navigation path — a field name or an array index.

One step on a JsonPrism's flat navigation path — a field name or an array index.

The path walker dispatches on the case to decide which of circe's representations to pierce: JsonObject for named fields, or the underlying Vector[Json] for array indices.

Public so users can read PathStep values off JsonFailure instances exposed by the default JsonPrism / JsonTraversal Ior-bearing surface.

Attributes

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

Value members

Concrete methods

Root-level Prism from Json to a native type S. Alias for JsonPrism.apply that reads more naturally when composed with .field.

Root-level Prism from Json to a native type S. Alias for JsonPrism.apply that reads more naturally when composed with .field.

Attributes

Source
package.scala

Givens

Givens

A dev.constructive.eo.optics.Plated over the JSON tree itself — the immediate children of a node are an array's elements or an object's field values (a primitive has none). With it, Plated.transform / rewrite / universe walk a whole Json document recursively: redact every field at any depth, rewrite every string, round every number, rename keys throughout. Pure and total — rebuilding an array/object from new children needs no decode — and stack-safe via the cats.Eval trampoline in the combinators.

A dev.constructive.eo.optics.Plated over the JSON tree itself — the immediate children of a node are an array's elements or an object's field values (a primitive has none). With it, Plated.transform / rewrite / universe walk a whole Json document recursively: redact every field at any depth, rewrite every string, round every number, rename keys throughout. Pure and total — rebuilding an array/object from new children needs no decode — and stack-safe via the cats.Eval trampoline in the combinators.

 import dev.constructive.eo.circe.given
 import dev.constructive.eo.optics.Plated
 // Uppercase every string anywhere in the document:
 Plated.transform[Json](j => j.asString.fold(j)(s => Json.fromString(s.toUpperCase)))(doc)

Attributes

Source
package.scala