Json
Json is an algebraic data type (ADT) for representing JSON values in ZIO Blocks. It provides a type-safe, schema-free way to work with JSON data, enabling navigation, transformation, merging, and querying without losing fidelity.
Overview​
The Json type represents all valid JSON values with six cases:
Json
├── Json.Object (key-value pairs, order-preserving)
├── Json.Array (ordered sequence of values)
├── Json.String (text)
├── Json.Number (arbitrary precision via BigDecimal)
├── Json.Boolean (true/false)
└── Json.Null (null)
Key design decisions:
- Objects use
Vector[(String, Json)]to preserve insertion order while providing order-independent equality - Numbers use
BigDecimalto preserve precision for financial and scientific data - All navigation returns
JsonSelectionfor fluent, composable chaining
Creating JSON Values​
Using Constructors​
import zio.blocks.schema.json.Json
// Object with named fields
val person = Json.Object(
"name" -> Json.String("Alice"),
"age" -> Json.Number(30),
"active" -> Json.Boolean(true)
)
// Array of values
val numbers = Json.Array(Json.Number(1), Json.Number(2), Json.Number(3))
// Primitive values
val name = Json.String("Bob")
val count = Json.Number(42)
val flag = Json.Boolean(false)
val nothing = Json.Null
Parsing JSON Strings​
import zio.blocks.schema.json.Json
import zio.blocks.schema.SchemaError
// Safe parsing (returns Either)
val parsed: Either[SchemaError, Json] = Json.parse("""{"name": "Alice", "age": 30}""")
// Unsafe parsing (throws on error)
val json = Json.parseUnsafe("""{"items": [1, 2, 3]}""")
String Interpolators​
ZIO Blocks provides compile-time validated string interpolators for JSON:
import zio.blocks.schema._
import zio.blocks.schema.json._
// JSON literal with compile-time validation
val person = json"""{"name": "Alice", "age": 30}"""
// With Scala value interpolation
val name = "Bob"
val age = 25
val person2 = json"""{"name": $name, "age": $age}"""
// Path interpolator for navigation
val path = p".users[0].name"
The json"..." interpolator validates JSON syntax at compile time, catching errors before runtime.
Type Testing and Access​
Unified Type Operations​
The Json type provides unified methods for type testing and narrowing with path-dependent return types.
JsonType also implements Json => Boolean, so it can be used directly as a predicate for filtering.
import zio.blocks.schema.json.{Json, JsonType}
val json: Json = Json.parseUnsafe("""{"count": 42}""")
// Type testing with is()
json.is(JsonType.Object) // true
json.is(JsonType.Array) // false
// Type narrowing with as() - returns Option[jsonType.Type]
val obj: Option[Json.Object] = json.as(JsonType.Object) // Some(Json.Object(...))
val arr: Option[Json.Array] = json.as(JsonType.Array) // None
// Value extraction with unwrap() - returns Option[jsonType.Unwrap]
val str: Json = Json.String("hello")
val strValue: Option[String] = str.unwrap(JsonType.String) // Some("hello")
val num: Json = Json.Number(42)
val numValue: Option[BigDecimal] = num.unwrap(JsonType.Number) // Some(42)
// JsonType as predicate - use directly in selection query
val strings = json.select.query(JsonType.String) // all string values in the JSON tree
Direct Value Access​
import zio.blocks.schema.json.Json
val obj = Json.Object("a" -> Json.Number(1))
obj.fields // Chunk(("a", Json.Number(1)))
val arr = Json.Array(Json.Number(1), Json.Number(2))
arr.elements // Chunk(Json.Number(1), Json.Number(2))
Navigation​
Simple Navigation​
Navigate into objects by key and arrays by index:
import zio.blocks.schema.json.Json
import zio.blocks.schema.SchemaError
val json = Json.parseUnsafe("""{
"users": [
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 25}
]
}""")
// Navigate to a field
val users = json.get("users") // JsonSelection
// Navigate to an array element
val firstUser = json.get("users")(0) // JsonSelection
// Chain navigation
val firstName = json.get("users")(0).get("name") // JsonSelection
// Extract the value
val name: Either[SchemaError, String] = firstName.as[String] // Right("Alice")
Path-Based Navigation with DynamicOptic​
Use DynamicOptic paths for complex navigation:
import zio.blocks.schema._
import zio.blocks.schema.json._
val json = Json.parseUnsafe("""{
"company": {
"employees": [
{"name": "Alice", "department": "Engineering"},
{"name": "Bob", "department": "Sales"}
]
}
}""")
// Using path interpolator
val path = p".company.employees[0].name"
val name = json.get(path).as[String] // Right("Alice")
// Equivalent to chained navigation
val sameName = json.get("company").get("employees")(0).get("name").as[String]
JsonSelection​
JsonSelection is a fluent wrapper for navigation results, enabling composable chaining:
import zio.blocks.schema.json.{Json, JsonSelection}
val json = Json.parseUnsafe("""{"users": [{"name": "Alice"}]}""")
// Fluent chaining
val result: JsonSelection = json
.get("users")
.arrays
.apply(0)
.get("name")
.strings
// Extract values
result.as[String] // Right("Alice")
result.one // Right(Json.String("Alice"))
result.isSuccess // true
result.isFailure // false
Terminal Operations​
import zio.blocks.schema.json.{Json, JsonSelection}
import zio.blocks.schema.SchemaError
val selection: JsonSelection = ???
// Get single value (exactly one required)
val oneValue: Either[SchemaError, Json] = selection.one
// Get any single value (first of many)
val anyValue: Either[SchemaError, Json] = selection.any
// Get all values condensed (wraps multiple in array)
val allValues: Either[SchemaError, Json] = selection.all
// Get underlying result
val underlying: Option[zio.blocks.chunk.Chunk[Json]] = selection.values
val asChunk: zio.blocks.chunk.Chunk[Json] = selection.toChunk // empty on error
// Decode to specific types
val asString: Either[SchemaError, String] = selection.as[String]
val asBigDecimal: Either[SchemaError, BigDecimal] = selection.as[BigDecimal]
val asBoolean: Either[SchemaError, Boolean] = selection.as[Boolean]
val asInt: Either[SchemaError, Int] = selection.as[Int]
val asLong: Either[SchemaError, Long] = selection.as[Long]
val asDouble: Either[SchemaError, Double] = selection.as[Double]
Modification​
Setting Values​
import zio.blocks.schema._
import zio.blocks.schema.json._
val json = Json.parseUnsafe("""{"user": {"name": "Alice", "age": 30}}""")
// Set a value at a path
val updated = json.set(p".user.name", Json.String("Bob"))
// {"user": {"name": "Bob", "age": 30}}
// Set with failure handling
val result = json.setOrFail(p".user.email", Json.String("alice@example.com"))
// Left(SchemaError) - path doesn't exist
Modifying Values​
import zio.blocks.schema._
import zio.blocks.schema.json._
val json = Json.parseUnsafe("""{"count": 10}""")
// Modify with a function
val incremented = json.modify(p".count") {
case Json.Number(n) => Json.Number(n + 1)
case other => other
}
// {"count": 11}
// Modify with failure on missing path
val result = json.modifyOrFail(p".count") {
case Json.Number(n) => Json.Number(n * 2)
}
// Right({"count": 20})
Deleting Values​
import zio.blocks.schema._
import zio.blocks.schema.json._
val json = Json.parseUnsafe("""{"a": 1, "b": 2, "c": 3}""")
// Delete a field
val withoutB = json.delete(p".b")
// {"a": 1, "c": 3}
// Delete with failure handling
val result = json.deleteOrFail(p".missing")
// Left(SchemaError) - path doesn't exist
Inserting Values​
import zio.blocks.schema._
import zio.blocks.schema.json._
val json = Json.parseUnsafe("""{"existing": 1}""")
// Insert a new field
val withNew = json.insert(p".newField", Json.String("value"))
// {"existing": 1, "newField": "value"}
Transformation​
Transform Up (Bottom-Up)​
Transform children before parents:
import zio.blocks.schema.json.Json
import zio.blocks.schema.DynamicOptic
val json = Json.parseUnsafe("""{"values": [1, 2, 3]}""")
// Double all numbers
val doubled = json.transformUp { (path, value) =>
value match {
case Json.Number(n) => Json.Number(n * 2)
case other => other
}
}
// {"values": [2, 4, 6]}
Transform Down (Top-Down)​
Transform parents before children:
import zio.blocks.schema.json.Json
import zio.blocks.schema.DynamicOptic
val json = Json.parseUnsafe("""{"items": [{"x": 1}, {"x": 2}]}""")
// Add a field to all objects
val withId = json.transformDown { (path, value) =>
value match {
case Json.Object(fields) if !fields.exists(_._1 == "id") =>
new Json.Object(("id" -> Json.String(path.toString)) +: fields)
case other => other
}
}
Transform Keys​
Rename object keys throughout the structure:
import zio.blocks.schema.json.Json
val json = Json.parseUnsafe("""{"user_name": "Alice", "user_age": 30}""")
// Convert snake_case to camelCase
val camelCase = json.transformKeys { (path, key) =>
key.split("_").zipWithIndex.map {
case (word, 0) => word
case (word, _) => word.capitalize
}.mkString
}
// {"userName": "Alice", "userAge": 30}
Filtering​
Filter Values​
Keep only values matching a predicate using retain, or remove values using prune:
import zio.blocks.schema.json.{Json, JsonType}
val json = Json.parseUnsafe("""{"a": 1, "b": null, "c": 2, "d": null}""")
// Remove nulls using prune (removes values matching predicate)
val noNulls = json.prune(_.is(JsonType.Null))
// {"a": 1, "c": 2}
// Keep only numbers using retain (keeps values matching predicate)
val onlyNumbers = json.retain(_.is(JsonType.Number))
// {"a": 1, "c": 2}
Project Paths​
Extract only specific paths:
import zio.blocks.schema._
import zio.blocks.schema.json._
val json = Json.parseUnsafe("""{
"user": {"name": "Alice", "email": "alice@example.com", "password": "secret"},
"metadata": {"created": "2024-01-01"}
}""")
// Keep only specific fields
val projected = json.project(p".user.name", p".user.email")
// {"user": {"name": "Alice", "email": "alice@example.com"}}
Partition​
Split based on a predicate:
import zio.blocks.schema.json.{Json, JsonType}
val json = Json.parseUnsafe("""{"a": 1, "b": "text", "c": 2}""")
// Separate numbers from non-numbers
val (numbers, nonNumbers) = json.partition(_.is(JsonType.Number))
// numbers: {"a": 1, "c": 2}
// nonNumbers: {"b": "text"}
Folding​
Fold Up (Bottom-Up)​
Accumulate values from children to parents:
import zio.blocks.schema.json.Json
val json = Json.parseUnsafe("""{"values": [1, 2, 3, 4, 5]}""")
// Sum all numbers
val sum = json.foldUp(BigDecimal(0)) { (path, value, acc) =>
value match {
case n: Json.Number => acc + n.value
case _ => acc
}
}
// sum = 15
Fold Down (Top-Down)​
Accumulate values from parents to children:
import zio.blocks.schema.json.Json
import zio.blocks.schema.DynamicOptic
val json = Json.parseUnsafe("""{"a": {"b": {"c": 1}}}""")
// Collect all paths
val paths = json.foldDown(Vector.empty[DynamicOptic]) { (path, value, acc) =>
acc :+ path
}
Merging​
Combine two JSON values using different strategies:
import zio.blocks.schema.json.{Json, MergeStrategy}
val base = Json.parseUnsafe("""{"a": 1, "b": {"x": 10}}""")
val overlay = Json.parseUnsafe("""{"b": {"y": 20}, "c": 3}""")
// Auto strategy (default) - deep merge objects, concat arrays
val merged = base.merge(overlay)
// {"a": 1, "b": {"x": 10, "y": 20}, "c": 3}
// Shallow merge (only top-level)
val shallow = base.merge(overlay, MergeStrategy.Shallow)
// Replace (right wins)
val replaced = base.merge(overlay, MergeStrategy.Replace)
// {"b": {"y": 20}, "c": 3}
// Concat arrays
val concat = base.merge(overlay, MergeStrategy.Concat)
// Custom strategy
val custom = base.merge(overlay, MergeStrategy.Custom { (path, left, right) =>
// Your merge logic here
right
})
Merge Strategies​
| Strategy | Objects | Arrays | Primitives |
|---|---|---|---|
Auto | Deep merge | Concatenate | Replace |
Deep | Recursive merge | Concatenate | Replace |
Shallow | Top-level only | Concatenate | Replace |
Replace | Right wins | Right wins | Right wins |
Concat | Merge keys | Concatenate | Replace |
Custom(f) | User-defined | User-defined | User-defined |
Normalization​
Clean up JSON values:
import zio.blocks.schema.json.Json
val json = Json.parseUnsafe("""{
"z": 1,
"a": null,
"m": {"empty": {}},
"b": 2
}""")
// Sort object keys alphabetically
val sorted = json.sortKeys
// {"a": null, "b": 2, "m": {"empty": {}}, "z": 1}
// Remove null values
val noNulls = json.dropNulls
// {"z": 1, "m": {"empty": {}}, "b": 2}
// Remove empty objects and arrays
val noEmpty = json.dropEmpty
// {"z": 1, "a": null, "b": 2}
// Apply all normalizations
val normalized = json.normalize
// {"b": 2, "z": 1}
Encoding and Decoding​
Type Classes​
ZIO Blocks provides JsonEncoder and JsonDecoder type classes for converting between Scala types and Json:
import zio.blocks.schema.json.{Json, JsonEncoder, JsonDecoder}
// Encode Scala values to Json
val intJson = JsonEncoder[Int].encode(42) // Json.Number(42)
val strJson = JsonEncoder[String].encode("hello") // Json.String("hello")
// Decode Json to Scala values
val intResult = JsonDecoder[Int].decode(Json.Number(42)) // Right(42)
val strResult = JsonDecoder[String].decode(Json.String("hello")) // Right("hello")
Built-in Encoders/Decoders​
import zio.blocks.schema.json.{JsonEncoder, JsonDecoder}
// Primitives
JsonEncoder[String]
JsonEncoder[Int]
JsonEncoder[Long]
JsonEncoder[Double]
JsonEncoder[Boolean]
JsonEncoder[BigDecimal]
// Collections
JsonEncoder[List[Int]]
JsonEncoder[Vector[String]]
JsonEncoder[Map[String, Int]]
JsonEncoder[Option[String]]
// Java time types
JsonEncoder[java.time.Instant]
JsonEncoder[java.time.LocalDate]
JsonEncoder[java.time.ZonedDateTime]
JsonEncoder[java.util.UUID]
Schema-Based Derivation​
For complex types, use Schema-based derivation:
import zio.blocks.schema.Schema
import zio.blocks.schema.json.{Json, JsonEncoder, JsonDecoder}
case class Person(name: String, age: Int)
object Person {
implicit val schema: Schema[Person] = Schema.derived
// Derived from schema (lower priority)
implicit val encoder: JsonEncoder[Person] = JsonEncoder.fromSchema
implicit val decoder: JsonDecoder[Person] = JsonDecoder.fromSchema
}
val person = Person("Alice", 30)
val json = JsonEncoder[Person].encode(person)
val decoded = JsonDecoder[Person].decode(json)
Extension Syntax​
When a Schema is in scope, you can use convenient extension methods directly on values:
import zio.blocks.schema._
case class Person(name: String, age: Int)
object Person {
implicit val schema: Schema[Person] = Schema.derived
}
val person = Person("Alice", 30)
// Convert to Json AST
val json = person.toJson // Json.Object(...)
// Convert directly to JSON string
val jsonString = person.toJsonString // {"name":"Alice","age":30}
// Convert to UTF-8 bytes
val jsonBytes = person.toJsonBytes // Array[Byte]
// Parse JSON string back to a typed value
val parsed = """{"name":"Bob","age":25}""".fromJson[Person] // Right(Person("Bob", 25))
// Parse from bytes
val fromBytes = jsonBytes.fromJson[Person] // Right(Person("Alice", 30))
These extension methods provide a more ergonomic API compared to explicitly creating encoders/decoders.
Using the as Method​
import zio.blocks.schema.json.Json
import zio.blocks.schema.json.JsonDecoder
import zio.blocks.schema.{Schema, SchemaError}
case class Person(name: String, age: Int)
object Person {
implicit val schema: Schema[Person] = Schema.derived
implicit val decoder: JsonDecoder[Person] = JsonDecoder.fromSchema
}
val json = Json.parseUnsafe("""{"name": "Alice", "age": 30}""")
// Decode to a specific type
val person: Either[SchemaError, Person] = json.as[Person]
// Unsafe version (throws on error)
val personUnsafe: Person = json.asUnsafe[Person]
Printing JSON​
Basic Printing​
import zio.blocks.schema.json.Json
val json = Json.Object("name" -> Json.String("Alice"), "age" -> Json.Number(30))
// Compact output
val compact: String = json.print
// {"name":"Alice","age":30}
With Writer Config​
import zio.blocks.schema.json.Json
import zio.blocks.schema.json.WriterConfig
val json = Json.Object("name" -> Json.String("Alice"))
// Pretty-printed output (2-space indentation)
val pretty = json.print(WriterConfig.withIndentionStep2)
// {
// "name": "Alice"
// }
// Custom indentation
val indented4 = json.print(WriterConfig.withIndentionStep(4))
WriterConfig Options​
WriterConfig controls JSON output formatting:
| Option | Default | Description |
|---|---|---|
indentionStep | 0 | Spaces per indentation level (0 = compact) |
escapeUnicode | false | Escape non-ASCII characters as \uXXXX |
preferredBufSize | 32768 | Internal buffer size in bytes |
import zio.blocks.schema.json.WriterConfig
// Compact output (default)
val compact = WriterConfig
// Pretty-printed with 2-space indentation
val pretty = WriterConfig.withIndentionStep(2)
// Escape Unicode for ASCII-only output
val ascii = WriterConfig.withEscapeUnicode(true)
// Combine options
val custom = WriterConfig
.withIndentionStep(2)
.withEscapeUnicode(true)
.withPreferredBufSize(65536)
ReaderConfig Options​
ReaderConfig controls JSON parsing behavior:
| Option | Default | Description |
|---|---|---|
preferredBufSize | 32768 | Preferred byte buffer size |
preferredCharBufSize | 4096 | Preferred char buffer size for strings |
maxBufSize | 33554432 | Maximum byte buffer size (32MB) |
maxCharBufSize | 4194304 | Maximum char buffer size (4MB) |
checkForEndOfInput | true | Error on trailing non-whitespace |
import zio.blocks.schema.json.ReaderConfig
// Default configuration
val default = ReaderConfig
// Allow trailing content (useful for streaming)
val lenient = ReaderConfig.withCheckForEndOfInput(false)
// Increase buffer sizes for large documents
val largeDoc = ReaderConfig
.withPreferredBufSize(65536)
.withPreferredCharBufSize(8192)
To Bytes​
import zio.blocks.schema.json.Json
val json = Json.Object("x" -> Json.Number(1))
// As byte array
val bytes: Array[Byte] = json.printBytes
Query Operations​
Query with Predicate​
Find all values matching a condition:
import zio.blocks.schema.json.Json
val json = Json.parseUnsafe("""{
"users": [
{"name": "Alice", "active": true},
{"name": "Bob", "active": false},
{"name": "Charlie", "active": true}
]
}""")
// Find all active users using queryBoth on a selection
val activeUsers = json.select.queryBoth { (path, value) =>
value.get("active").as[Boolean].getOrElse(false)
}
Convert to Key-Value Pairs​
Flatten to path-value pairs:
import zio.blocks.schema.json.Json
import zio.blocks.schema.DynamicOptic
import zio.blocks.chunk.Chunk
val json = Json.parseUnsafe("""{"a": {"b": 1, "c": 2}}""")
val pairs: Chunk[(DynamicOptic, Json)] = json.toKV
// Chunk(
// ($.a.b, Json.Number(1)),
// ($.a.c, Json.Number(2))
// )
Comparison and Equality​
Object Equality​
Objects are compared order-independently (keys are compared as sorted sets):
import zio.blocks.schema.json.Json
val obj1 = Json.parseUnsafe("""{"a": 1, "b": 2}""")
val obj2 = Json.parseUnsafe("""{"b": 2, "a": 1}""")
obj1 == obj2 // true (order-independent)
Ordering​
JSON values have a total ordering for sorting:
import zio.blocks.schema.json.Json
val values = List(
Json.String("z"),
Json.Number(1),
Json.Null,
Json.Boolean(true)
)
// Sort by type, then by value
val sorted = values.sortWith((a, b) => a.compare(b) < 0)
// [null, true, 1, "z"]
Type ordering: Null < Boolean < Number < String < Array < Object
JSON Diffing​
JsonDiffer computes the difference between two JSON values, producing a JsonPatch that transforms the source into the target:
import zio.blocks.schema.json.{Json, JsonPatch}
val source = Json.parseUnsafe("""{"name": "Alice", "age": 30}""")
val target = Json.parseUnsafe("""{"name": "Alice", "age": 31, "active": true}""")
// Compute the diff
val patch: JsonPatch = JsonPatch.diff(source, target)
// The patch describes the minimal changes:
// - NumberDelta for age: 30 -> 31
// - Add field "active": true
The differ uses optimal operations:
- NumberDelta for numeric changes (stores the delta, not the new value)
- StringEdit for string changes when edits are more compact than replacement
- ArrayEdit with LCS-based Insert/Delete operations for arrays
- ObjectEdit with Add/Remove/Modify operations for objects
JSON Patching​
JsonPatch represents a sequence of operations that transform a JSON value. Patches are composable and can be applied with different failure modes:
Computing and Applying Patches​
import zio.blocks.schema.json.{Json, JsonPatch}
import zio.blocks.schema.patch.PatchMode
import zio.blocks.schema.SchemaError
val original = Json.parseUnsafe("""{"count": 10, "items": ["a", "b"]}""")
val modified = Json.parseUnsafe("""{"count": 15, "items": ["a", "b", "c"]}""")
// Compute the patch
val patch = JsonPatch.diff(original, modified)
// Apply with default (Strict) mode - fails on any precondition violation
val result1: Either[SchemaError, Json] = patch(original)
// Apply with Lenient mode - skips failing operations
val result2 = patch(original, PatchMode.Lenient)
// Apply with Clobber mode - forces changes on conflicts
val result3 = patch(original, PatchMode.Clobber)
Patch Modes​
| Mode | Behavior |
|---|---|
Strict | Fail immediately on any precondition violation |
Lenient | Skip operations that fail preconditions |
Clobber | Force changes, overwriting on conflicts |
Composing Patches​
import zio.blocks.schema.json.{Json, JsonPatch}
val patch1 = JsonPatch.diff(
Json.parseUnsafe("""{"x": 1}"""),
Json.parseUnsafe("""{"x": 2}""")
)
val patch2 = JsonPatch.diff(
Json.parseUnsafe("""{"x": 2}"""),
Json.parseUnsafe("""{"x": 2, "y": 3}""")
)
// Compose patches - applies patch1, then patch2
val combined = patch1 ++ patch2
// Apply the combined patch
val result = combined(Json.parseUnsafe("""{"x": 1}"""))
// Right({"x": 2, "y": 3})
Converting to DynamicPatch​
JsonPatch can be converted to and from DynamicPatch for interoperability with the typed patching system:
import zio.blocks.schema.json.JsonPatch
import zio.blocks.schema.patch.DynamicPatch
import zio.blocks.schema.SchemaError
val jsonPatch: JsonPatch = ???
// Convert to DynamicPatch
val dynamicPatch: DynamicPatch = jsonPatch.toDynamicPatch
// Convert from DynamicPatch (may fail for unsupported operations)
val restored: Either[SchemaError, JsonPatch] = JsonPatch.fromDynamicPatch(dynamicPatch)
Conversion to DynamicValue​
Convert JSON to ZIO Blocks' semi-structured DynamicValue:
import zio.blocks.schema.json.Json
import zio.blocks.schema.DynamicValue
val json = Json.parseUnsafe("""{"name": "Alice"}""")
val dynamic: DynamicValue = json.toDynamicValue
This enables interoperability with other ZIO Blocks formats (Avro, TOON, etc.).
Error Handling​
SchemaError​
Errors include path information for debugging:
import zio.blocks.schema.json.Json
import zio.blocks.schema.SchemaError
val json = Json.parseUnsafe("""{"users": [{"name": "Alice"}]}""")
val result = json.get("users")(5).get("name").as[String]
// Left(SchemaError: Index 5 out of bounds at path $.users[5])
Error Properties​
import zio.blocks.schema.SchemaError
import zio.blocks.schema.DynamicOptic
val error: SchemaError = ???
error.message // Error description
error.errors.head.source // DynamicOptic path to error location
Cross-Platform Support​
The Json type works across all platforms:
- JVM - Full functionality
- Scala.js - Browser and Node.js
String interpolators use compile-time validation that works on all platforms.