// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
// Schemas built out of other schemas: the applicators, the branches they
// choose, and the properties left over at the end. Run it with
// `moon run examples/composition`.
///|
fn main raise {
// `allOf` applies every branch and reports every branch that refused, so a
// value can fail twice for two different reasons.
let summed = @moonschema.Schema::new(
@json.parse(
(
#|{
#| "allOf": [
#| { "type": "integer" },
#| { "minimum": 1 },
#| { "maximum": 10 }
#| ]
#|}
),
),
)
println("allOf — an integer from 1 to 10:")
for instance in [Json::number(5.0), Json::number(0.0), Json::number(11.0)] {
println(" \{instance.stringify()} -> \{summed.valid(instance)}")
for fault in summed.faults(instance) {
println(" " + fault.to_string())
}
}
// `anyOf` passes when one branch does, and a branch that failed is not a
// fault of the instance: only every branch failing is. So the answer is one
// fault, not the reasons of each branch.
let either = @moonschema.Schema::new(
@json.parse(
(
#|{ "anyOf": [ { "type": "string" }, { "type": "integer" } ] }
),
),
)
println("\nanyOf — a string or an integer:")
for instance in [Json::string("x"), Json::boolean(true)] {
println(" \{instance.stringify()} -> \{either.valid(instance)}")
for fault in either.faults(instance) {
println(" " + fault.to_string())
}
}
// `oneOf` wants exactly one branch. Two branches matching is its own fault,
// and the message says how many did.
let exactly = @moonschema.Schema::new(
@json.parse(
(
#|{ "oneOf": [ { "type": "integer" }, { "minimum": 0 } ] }
),
),
)
println("\noneOf — an integer, or a number from 0 up:")
for instance in [Json::number(-2.0), Json::number(3.0)] {
println(" \{instance.stringify()} -> \{exactly.valid(instance)}")
for fault in exactly.faults(instance) {
println(" " + fault.to_string())
}
}
// `if` never fails on its own: it picks the branch, and the reasons come from
// whichever branch it picked.
let shipping = @moonschema.Schema::new(
@json.parse(
(
#|{
#| "type": "object",
#| "properties": { "country": { "type": "string" }, "postcode": { "type": "string" } },
#| "if": { "properties": { "country": { "const": "US" } } },
#| "then": { "properties": { "postcode": { "pattern": "^[0-9]{5}$" } } },
#| "else": { "properties": { "postcode": { "minLength": 3 } } }
#|}
),
),
)
println(
"\nif / then / else — a US postcode is five digits, any other is at least three characters:",
)
for
text in [
"{\"country\": \"US\", \"postcode\": \"94107\"}", "{\"country\": \"US\", \"postcode\": \"9410\"}",
"{\"country\": \"NL\", \"postcode\": \"10\"}",
] {
let instance = @json.parse(text)
println(" \{text}")
println(" -> \{shipping.valid(instance)}")
for fault in shipping.faults(instance) {
println(" " + fault.to_string())
}
}
// `unevaluatedProperties` reads what every other keyword in the same schema
// left untouched — including the branches of an `allOf`, which is what makes
// it the way to close a composed object. Nothing evaluated `email`, so it is
// the one that fails.
let closed = @moonschema.Schema::new(
@json.parse(
(
#|{
#| "allOf": [
#| { "properties": { "name": { "type": "string" } } },
#| { "properties": { "age": { "type": "integer", "minimum": 0 } } }
#| ],
#| "unevaluatedProperties": false
#|}
),
),
)
println(
"\nunevaluatedProperties — name and age are known, nothing else is:",
)
for
text in [
"{\"name\": \"Ada\", \"age\": 36}", "{\"name\": \"Ada\", \"email\": \"a@b.test\"}",
] {
let instance = @json.parse(text)
println(" \{text} -> \{closed.valid(instance)}")
for fault in closed.faults(instance) {
println(" " + fault.to_string())
}
}
// `patternProperties` and `additionalProperties` divide the same object up:
// a name one of the patterns matched belongs to it, and only the rest is
// "additional".
let headers = @moonschema.Schema::new(
@json.parse(
(
#|{
#| "type": "object",
#| "patternProperties": { "^x-": { "type": "string" } },
#| "additionalProperties": { "type": "integer" }
#|}
),
),
)
println("\npatternProperties then additionalProperties:")
for
text in [
"{\"x-trace\": \"abc\", \"retries\": 3}", "{\"x-trace\": 1, \"retries\": \"three\"}",
] {
let instance = @json.parse(text)
println(" \{text} -> \{headers.valid(instance)}")
for fault in headers.faults(instance) {
println(" " + fault.to_string())
}
}
}