JsonSelection
JsonSelection is a fluent wrapper type that enables composable, chainable navigation through JSON structures. It wraps Either[SchemaError, Chunk[Json]], allowing operations that may fail gracefully or return multiple values.
Overviewβ
JsonSelection makes it easy to navigate unknown or deeply nested JSON at runtime without needing to match on Either at each step. Chain operations like JsonSelection#get, JsonSelection#apply, JsonSelection#filter, and JsonSelection#as to build powerful queries.
Key characteristics:
- Fluent chaining: Operations chain naturally without unwrapping intermediate results
- Multi-value support: Can contain zero, one, or many
Jsonvalues - Error propagation: Errors short-circuit further operations; querying an error selection returns the same error
- Type extraction: Use
.as[Type]to decode to Scala types with Schema-based derivation
Creating JsonSelectionβ
You can create JsonSelection instances in multiple ways: from existing Json values, or using companion object constructors. Each approach is useful for different scenariosβdirect navigation for existing JSON, and explicit construction for building selections programmatically.
From Json Valuesβ
Create a JsonSelection directly from a Json value by calling navigation methods:
import zio.blocks.schema.json.{Json, JsonSelection}
val json = Json.parseUnsafe("""{"name": "Alice"}""")
// Get a single field
val name: JsonSelection = json.get("name")
// Get array element by index
val values = Json.parseUnsafe("[1, 2, 3]")
val selected: JsonSelection = values.get(0)
From Companion Objectβ
Construct JsonSelection instances programmatically using the companion object methods for empty, successful, and failed selections:
import zio.blocks.schema._
import zio.blocks.schema.json.{Json, JsonSelection}
// Empty successful selection
val empty = JsonSelection.empty
// Succeed with a single value
val success = JsonSelection.succeed(Json.String("hello"))
// Succeed with multiple values
val many = JsonSelection.succeedMany(
zio.blocks.chunk.Chunk.from(Seq(Json.Number(1), Json.Number(2)))
)
// Fail with an error
val failure = JsonSelection.fail(SchemaError("not found"))
Navigation Operationsβ
Navigate through JSON structures using three complementary approaches: field access for object properties, array indexing for elements, and path expressions for complex nested navigation.
Field Navigationβ
Navigate to object fields using JsonSelection#get with a field name:
import zio.blocks.schema.json.Json
val data = Json.parseUnsafe("""{
"user": {
"name": "Bob",
"email": "bob@example.com"
}
}""")
// Single field access
val user = data.get("user")
// Chained field access
val name = data.get("user").get("name")
// Multiple levels of nesting
val email = data.get("user").get("email")
Array Navigationβ
Access array elements by index using JsonSelection#apply or JsonSelection#get:
import zio.blocks.schema.json.Json
val data = Json.parseUnsafe("""[
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"}
]""")
// Index access (0-based)
val first = data.get(0)
// Chained index and field access
val firstName = data.get(0).get("name")
val secondId = data.get(1).get("id")
Path-Based Navigationβ
Use path interpolators (e.g., p".company.employees[0].name") to navigate deeply nested structures in a single operation:
import zio.blocks.schema._
import zio.blocks.schema.json.Json
val data = Json.Object(
"company" -> Json.Object(
"employees" -> Json.Array(
Json.Object("name" -> Json.String("Alice"), "department" -> Json.String("Engineering")),
Json.Object("name" -> Json.String("Bob"), "department" -> Json.String("Sales"))
)
)
)
// Navigate using path interpolator
val path = p".company.employees[0].name"
val firstEmpName = data.get(path) // JsonSelection(Right(Chunk(Json.String("Alice"))))
Filtering and Queryingβ
Reduce selections to only the values you need by filtering by JSON type or custom predicates. This enables working with heterogeneous JSON arrays where values may be of different types.
Filter by Typeβ
Keep only values of a specific JSON type using type-filtering methods like JsonSelection#strings, JsonSelection#numbers, and JsonSelection#booleans:
import zio.blocks.schema.json.{Json, JsonSelection}
val mixed = Json.parseUnsafe("""["text", 42, true, "more text"]""")
// Create a selection containing all array elements, then filter by type
val allElements = JsonSelection.succeedMany(mixed.elements)
val strings = allElements.strings // JsonSelection with string values
val numbers = allElements.numbers // JsonSelection with number values
val booleans = allElements.booleans // JsonSelection with boolean values
Filter with Predicateβ
Use custom predicates with JsonSelection#filter to keep only values that match specific conditions:
import zio.blocks.schema.json.{Json, JsonSelection}
val data = Json.parseUnsafe("[1, 2, 3, 4]")
// Create selection and filter with predicate
val allElements = JsonSelection.succeedMany(data.elements)
val evenOnly = allElements.filter { json =>
json match {
case Json.Number(n) => n.toInt % 2 == 0
case _ => false
}
}
Extracting Valuesβ
Extract concrete values from selections by decoding them to Scala types, checking single vs. multiple values, or inspecting selection state without extraction.
Type Decodingβ
Decode a single selected value to a Scala type using JsonSelection#as, which fails if more than one value is selected:
import zio.blocks.schema._
import zio.blocks.schema.json.Json
// Decode to Scala types
val selection = Json.parseUnsafe("""{"count": 42}""")
val count: Either[SchemaError, Int] = selection.get("count").as[Int]
val str: Either[SchemaError, String] = selection.get("count").as[String] // Left (type mismatch)
Multiple Decodingβ
Decode all selected values to a collection using JsonSelection#asAll, which succeeds even if the selection is empty:
import zio.blocks.schema._
import zio.blocks.schema.json.{Json, JsonSelection}
val data = Json.parseUnsafe("""["Alice", "Bob", "Charlie"]""")
// Create selection of all array elements and decode
val allElements = JsonSelection.succeedMany(data.elements)
val names: Either[SchemaError, Seq[String]] = allElements.asAll[String].map(_.toSeq)
Extract Single Valueβ
Use JsonSelection#one to extract exactly one value, failing if the selection contains zero or more than one value:
import zio.blocks.schema._
import zio.blocks.schema.json.{Json, JsonSelection}
val arr = Json.parseUnsafe("[1, 2]")
// Get exactly one value from a selection (fails if 0 or more than 1)
val single = arr.get(0).one // Right(Json.Number(1))
val multiple = JsonSelection.succeedMany(arr.elements).one // Left(SchemaError(...))
Check and Getβ
Inspect selection state without extraction using properties like JsonSelection#values, JsonSelection#error, JsonSelection#isSuccess, and JsonSelection#isFailure:
import zio.blocks.schema.json.{Json, JsonSelection}
val data = Json.Object("x" -> Json.Number(1))
// Safe option extraction
val x: Option[Json] = data.get("x").values.flatMap(_.headOption)
// Check if selection succeeded
val success = data.get("exists").isSuccess // true if "exists" field found
val failed = data.get("notFound").isFailure // true if field not found
Size and Existence Checksβ
Query selection size and emptiness using JsonSelection#size, JsonSelection#isEmpty, and JsonSelection#nonEmpty:
import zio.blocks.schema.json.{Json, JsonSelection}
val data = Json.parseUnsafe("[1, 2, 3]")
// Create selection and check size
val selection = JsonSelection.succeedMany(data.elements)
val size = selection.size // 3
val isEmpty = selection.isEmpty // false
val nonEmpty = selection.nonEmpty // true
Modifying Selectionsβ
Use JsonSelection to update values at specific paths using set for replacement or modify for transformation:
import zio.blocks.schema._
import zio.blocks.schema.json.Json
val original = Json.Object("count" -> Json.Number(0))
// Set a new value
val updated = original.set(p".count", Json.Number(1))
// Modify with a function
val modified = original.modify(p".count") {
case Json.Number(n) => Json.Number(n + 1)
case other => other
}
Error Handlingβ
Selections propagate errors through the chain:
import zio.blocks.schema._
import zio.blocks.schema.json.Json
val data = Json.Object("name" -> Json.String("Alice"))
// Missing field returns error
val missing = data.get("age").error // Some(SchemaError("field not found"))
// Chain continues with error
val stillMissing = data.get("age").get("nested") // Still carries the error
// Check error state
val result = data.get("x")
val maybeError: Option[SchemaError] = result.error
val maybeValues = result.values // None if error
Integration with Codecsβ
JsonSelection integrates seamlessly with JsonCodec and Schema:
import zio.blocks.schema._
import zio.blocks.schema.json.{Json, JsonSelection}
import zio.blocks.chunk.Chunk
case class User(name: String, email: String)
object User { implicit val schema: Schema[User] = Schema.derived }
val users = Json.parseUnsafe("""[
{"name": "Alice", "email": "alice@example.com"},
{"name": "Bob", "email": "bob@example.com"}
]""")
// Navigate and decode in one chain
val firstUser: Either[SchemaError, User] = users.get(0).as[User]
val allUsers: Either[SchemaError, Chunk[User]] = JsonSelection.succeedMany(users.elements).asAll[User]
Performance Notesβ
- Zero-allocation navigation:
JsonSelectionitself is a value type (AnyVal) and compiles to no allocation - Lazy chaining: Operations chain without intermediate allocations; only final extraction materializes values
- Error short-circuiting: Failed selections don't execute further operations
- Streaming-friendly: For large JSON, use
JsonSelection#getselectively rather than iterating over all values