JSON Codec
The JSON codec module provides complete, type-safe support for working with JSON data in ZIO Blocks. It includes an ADT for representing JSON values, a fluent navigation API (JsonSelection), configurable encoding/decoding (JsonCodec with WriterConfig/ReaderConfig), composable patches for transformations, a diff algorithm for computing minimal changes, and full JSON Schema 2020-12 support for validation and code generation.
Core types: Json, JsonCodec, JsonSelection, JsonPatch, JsonDiffer, JsonSchema, JsonType.
import zio.blocks.schema._
import zio.blocks.schema.json._
// Represent any JSON value
val person = Json.Object("name" -> Json.String("Alice"), "age" -> Json.Number(30))
// Navigate with fluent API
val age = person.get("age") // JsonSelection
// Encode to string
val encoded = person.print(WriterConfig)
// Compute minimal patches
val updated = person.set(p".age", Json.Number(31))
val patch = JsonPatch.diff(person, updated)
// Validate against a schema
case class Person(name: String, age: Int)
object Person { implicit val schema: Schema[Person] = Schema.derived }
val schema: JsonSchema = Schema[Person].toJsonSchema
schema.conforms(person) // true
Installation
The JSON codec is included in the ZIO Blocks Schema module. Add it to your build.sbt:
// Scala 2.13 and Scala 3.3 to 3.8
libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.56"
// Scala 3.9 and newer
libraryDependencies += "dev.zio" %% "zio-blocks-next-schema" % "0.0.56"
scalacOptions += "-experimental"
For Scala.js projects, use %%% instead of %%.
Supported Scala versions: 2.13.x and 3.x. See How to Add ZIO Blocks to Your Build for details.
The JSON codec is fully integrated into ZIO Blocks Schema and provides complete type-safe JSON support with no external dependencies beyond core ZIO libraries.
How They Work Together
The JSON module workflow moves through representation, navigation, encoding/decoding, diffing, patching, and validation:
Parse/Create JSON
↓
Json (ADT: Object, Array, String, Number, Boolean, Null)
├─ Navigate with JsonSelection (fluent query API)
├─ Understand with JsonType (type information)
└─ Transform via JsonPatch (composable operations)
↓
JsonCodec (encode/decode)
├─ WriterConfig (indent, escaping, order)
├─ ReaderConfig (strict validation, number handling)
└─ MergeStrategy (field conflict resolution)
↓
JsonDiffer (diff algorithm)
└─ Produces JsonPatch (minimal changes)
↓
JsonPatch.apply (transform JSON)
↓
JsonSchema (validate or generate)
├─ Derive from Scala types
├─ Validate JSON conformance
└─ Generate documentation
Type Relationships:
Jsonis the central value type; all operations start with or produce itJsonSelectionprovides fluent navigation through nestedJsonstructuresJsonCodecbridges Scala types ↔Jsonwith configurableWriterConfigandReaderConfigJsonDiffercomputesJsonPatchby comparing twoJsonvaluesJsonPatchtransformsJsonvalues with composable operationsJsonSchemavalidatesJsonconformance and derives from ScalaSchematypesJsonTypeprovides type information forJsonvalues at runtime
Common Patterns
Pattern 1: Parse, Transform, Serialize
Read JSON, modify it, and write it back:
import zio.blocks.schema._
import zio.blocks.schema.json.{Json, WriterConfig}
val jsonString = """{"name": "Alice", "age": 30}"""
val json = Json.parseUnsafe(jsonString)
// Transform
val updated = json.modify(p".age") {
case Json.Number(n) => Json.Number(n + 1)
case other => other
}
// Serialize
val output = updated.print(WriterConfig.withIndentionStep2)
Pattern 2: Diff and Apply
Compute changes and apply them:
import zio.blocks.schema.json.{Json, JsonPatch}
val original = Json.Object("count" -> Json.Number(0))
val target = Json.Object("count" -> Json.Number(1), "active" -> Json.Boolean(true))
// Compute the diff
val patch = JsonPatch.diff(original, target)
// Apply to reconstruct
val result = patch.apply(original)
// Right({"count": 1, "active": true})
Pattern 3: Validate with Schema
Generate and validate using schemas:
import zio.blocks.schema._
import zio.blocks.schema.json.{Json, JsonSchema}
case class User(name: String, email: String, age: Int)
object User {
implicit val schema: Schema[User] = Schema.derived
}
val jsonSchema: JsonSchema = Schema[User].toJsonSchema
val validJson = Json.Object(
"name" -> Json.String("Alice"),
"email" -> Json.String("alice@example.com"),
"age" -> Json.Number(30)
)
jsonSchema.conforms(validJson) // true
Pattern 4: Navigate and Extract with JsonSelection
Use fluent API to navigate nested structures and extract values:
import zio.blocks.schema._
import zio.blocks.schema.json.Json
val data = Json.Object(
"users" -> Json.Array(
Json.Object("name" -> Json.String("Alice"), "age" -> Json.Number(30)),
Json.Object("name" -> Json.String("Bob"), "age" -> Json.Number(25))
)
)
// Navigate to first user's age
val age: Either[SchemaError, Int] = data
.get("users") // JsonSelection
(0) // Navigate to first array element
.get("age") // Navigate to age field
.as[Int] // Decode to Int
// Check values
val ageValue = data.get("users")(0).get("age").one // Right(Json.Number(30))
Pattern 5: Encode and Decode with Configuration
Control output formatting and parsing behavior:
import zio.blocks.schema._
import zio.blocks.schema.json.{Json, WriterConfig}
case class Config(host: String, port: Int)
object Config {
implicit val schema: Schema[Config] = Schema.derived
}
val config = Config("localhost", 8080)
val json = config.toJson
// Encode with custom formatting
val pretty = json.print(WriterConfig.withIndentionStep(2))
// Decode: parse back to Json, then decode
val parsed: Json = Json.parseUnsafe(pretty)
val config2: Either[SchemaError, Config] = parsed.as[Config]
Pattern 6: Compose Multiple Patches
Chain multiple transformations into a single patch:
import zio.blocks.schema._
import zio.blocks.schema.json.{Json, JsonPatch}
val original = Json.Object("x" -> Json.Number(1), "y" -> Json.Number(2))
// Build patches independently
val patchX = JsonPatch.diff(
original,
original.set(p".x", Json.Number(10))
)
val patchY = JsonPatch.diff(
original.set(p".x", Json.Number(10)),
original.set(p".x", Json.Number(10)).set(p".y", Json.Number(20))
)
// Compose them
val combined = patchX ++ patchY
combined.apply(original)
// Right({"x": 10, "y": 20})
Integration Points
With other codecs: Json values convert to/from other formats (Avro, TOON, etc.) via DynamicValue:
import zio.blocks.schema.json.Json
import zio.blocks.schema.DynamicValue
val json = Json.parseUnsafe("""{"name": "Alice"}""")
val dynamic: DynamicValue = json.toDynamicValue
// Convert to other formats using DynamicValue
With Schema system: Schema enables automatic JSON encoding/decoding:
import zio.blocks.schema._
import zio.blocks.schema.json.Json
case class Person(name: String, age: Int)
object Person {
implicit val schema: Schema[Person] = Schema.derived
}
// Encode using Schema-based extension methods
val person = Person("Alice", 30)
val json = person.toJson
val jsonString = person.toJsonString
// Decode: parse and extract with Schema
val parsed: Json = Json.parseUnsafe(jsonString)
val decoded: Either[SchemaError, Person] = parsed.as[Person]
With patching system: JsonPatch and JsonDiffer integrate with generic Patch infrastructure:
import zio.blocks.schema.json.{Json, JsonPatch}
import zio.blocks.schema.patch.DynamicPatch
val jsonPatch: JsonPatch = JsonPatch.root(JsonPatch.Op.Set(Json.Null))
val dynamicPatch: DynamicPatch = jsonPatch.toDynamicPatch
With optics and navigation: JsonSelection provides query-like access complementary to Optic system:
JsonSelection— runtime navigation through unknown JSON structuresOptic— compile-time, type-safe navigation through known Scala types- Both support nested access, filtering, and composition
With validation: JsonSchema integrates with Schema Validation system:
- Derive
JsonSchemafrom ScalaSchema[A] - Validate raw JSON before type-safe decoding
- Compose with logical operators (allOf, anyOf, oneOf)
Type Pages
-
Json — The core ADT representing JSON values with six cases (Object, Array, String, Number, Boolean, Null). Covers construction, navigation, modification, transformation, filtering, merging, and encoding.
-
JsonPatch — Composable patches for transforming JSON values. Create patches via diff or manually, compose them with
++, and apply with different failure modes (Strict, Lenient, Clobber). -
JsonDiffer — Diff algorithm computing minimal patches. Uses smart strategies per type: NumberDelta for numbers, LCS-based StringEdit for strings, ArrayEdit with insertion/deletion for arrays, and ObjectEdit for fields.
-
JSON Schema — Full JSON Schema 2020-12 support for validation and code generation. Derive schemas from Scala types, validate JSON, construct schemas manually with builders, and combine with logical operators.
See also the Your First Schema tutorial, which round-trips a type through JSON end to end.
For throughput, see How to Tune JSON Encoding and Decoding for Throughput.
Coming from another JSON library? Coming from circe, zio-json or jsoniter-scala maps their codecs, configuration and annotations onto ZIO Blocks.