///|
/// Create a schema that validates JSON objects.
///
/// Each key in `spec` defines a required field and its schema.
/// By default, extra fields in the input JSON are silently stripped (Strip mode).
/// Use `.passthrough()` to allow extra fields, or `.strict()` to reject them.
pub fn object(
spec : Map[String, Schema],
required_error? : String = "",
invalid_type_error? : String = "",
) -> Schema {
{
schema_type: ObjectType(spec, Strip),
rules: [],
description: "",
required_error,
invalid_type_error,
name: "",
brand: "",
}
}
///|
/// Reject fields not defined in the schema spec.
pub fn Schema::strict(self : Schema) -> Schema {
match self.schema_type {
ObjectType(spec, _) => { ..self, schema_type: ObjectType(spec, Strict) }
_ => abort("strict() is only valid for object schemas")
}
}
///|
/// Allow fields not defined in the schema spec.
pub fn Schema::passthrough(self : Schema) -> Schema {
match self.schema_type {
ObjectType(spec, _) =>
{ ..self, schema_type: ObjectType(spec, Passthrough) }
_ => abort("passthrough() is only valid for object schemas")
}
}
///|
/// Silently strip extra fields not defined in the schema spec.
/// This is the default mode.
pub fn Schema::strip(self : Schema) -> Schema {
match self.schema_type {
ObjectType(spec, _) => { ..self, schema_type: ObjectType(spec, Strip) }
_ => abort("strip() is only valid for object schemas")
}
}
///|
/// Select only the specified fields from an object schema.
/// Returns a new object schema with the same mode (Strip/Passthrough/Strict).
/// Keys not present in the original spec are silently ignored.
///
/// # Example
/// ```
/// let s = object({"a": string(), "b": number()}).pick(["a"])
/// s.parse(json) // validates only field "a"
/// ```
pub fn Schema::pick(self : Schema, keys : Array[String]) -> Schema {
match self.schema_type {
ObjectType(spec, mode) => {
let new_spec : Map[String, Schema] = {}
for key in keys {
match spec.get(key) {
Some(s) => new_spec.set(key, s)
None => ()
}
}
{
schema_type: ObjectType(new_spec, mode),
rules: self.rules,
description: self.description,
required_error: self.required_error,
invalid_type_error: self.invalid_type_error,
name: self.name,
brand: self.brand,
}
}
_ => abort("pick() is only valid for object schemas")
}
}
///|
/// Omit the specified fields from an object schema.
/// Returns a new object schema with the same mode (Strip/Passthrough/Strict).
///
/// # Example
/// ```
/// let s = object({"a": string(), "b": number()}).omit(["b"])
/// s.parse(json) // validates only field "a"
/// ```
pub fn Schema::omit(self : Schema, keys : Array[String]) -> Schema {
match self.schema_type {
ObjectType(spec, mode) => {
let new_spec : Map[String, Schema] = {}
for key in spec.keys() {
if !value_in_array(key, keys) {
match spec.get(key) {
Some(s) => new_spec.set(key, s)
None => ()
}
}
}
{
schema_type: ObjectType(new_spec, mode),
rules: self.rules,
description: self.description,
required_error: self.required_error,
invalid_type_error: self.invalid_type_error,
name: self.name,
brand: self.brand,
}
}
_ => abort("omit() is only valid for object schemas")
}
}
///|
/// Make all fields of an object schema optional.
/// Useful for partial updates (e.g., PATCH operations).
/// Existing optional fields remain optional.
///
/// # Example
/// ```
/// let s = object({"name": string(), "age": number()}).partial()
/// s.parse(json) // all fields are now optional
/// ```
pub fn Schema::partial(self : Schema) -> Schema {
match self.schema_type {
ObjectType(spec, mode) => {
let new_spec : Map[String, Schema] = {}
for key in spec.keys() {
match spec.get(key) {
Some(s) => new_spec.set(key, s.optional())
None => ()
}
}
{
schema_type: ObjectType(new_spec, mode),
rules: self.rules,
description: self.description,
required_error: self.required_error,
invalid_type_error: self.invalid_type_error,
name: self.name,
brand: self.brand,
}
}
_ => abort("partial() is only valid for object schemas")
}
}
///|
/// Extend an object schema with additional fields.
/// Fields in `extension` override fields with the same name in the base schema.
pub fn Schema::extend_with(
self : Schema,
extension : Map[String, Schema],
) -> Schema {
match self.schema_type {
ObjectType(spec, mode) => {
let new_spec : Map[String, Schema] = {}
for key in spec.keys() {
match spec.get(key) {
Some(s) => new_spec.set(key, s)
None => ()
}
}
for key in extension.keys() {
match extension.get(key) {
Some(s) => new_spec.set(key, s)
None => ()
}
}
{
schema_type: ObjectType(new_spec, mode),
rules: self.rules,
description: self.description,
required_error: self.required_error,
invalid_type_error: self.invalid_type_error,
name: self.name,
brand: self.brand,
}
}
_ => abort("extend_with() is only valid for object schemas")
}
}
///|
/// Merge two object schemas.
/// Fields from `other` override fields with the same name in `self`.
/// The merged schema inherits the right schema's object mode.
pub fn Schema::merge(self : Schema, other : Schema) -> Schema {
match (self.schema_type, other.schema_type) {
(ObjectType(spec, _), ObjectType(other_spec, other_mode)) => {
let new_spec : Map[String, Schema] = {}
for key in spec.keys() {
match spec.get(key) {
Some(s) => new_spec.set(key, s)
None => ()
}
}
for key in other_spec.keys() {
match other_spec.get(key) {
Some(s) => new_spec.set(key, s)
None => ()
}
}
{
schema_type: ObjectType(new_spec, other_mode),
rules: self.rules,
description: self.description,
required_error: self.required_error,
invalid_type_error: self.invalid_type_error,
name: self.name,
brand: self.brand,
}
}
(_, ObjectType(_, _)) => abort("merge() is only valid for object schemas")
_ => abort("merge() requires an object schema")
}
}
///|
pub fn Schema::parse_object(
self : Schema,
spec : Map[String, Schema],
mode : ObjectMode,
json : Json,
path_stack : Array[String],
) -> SchemaResult {
match json {
Object(input_map) => {
let errors : Array[ValidationError] = []
let parsed_fields : Map[String, Json] = {}
for field_name in spec.keys() {
match spec.get(field_name) {
Some(field_schema) => {
path_stack.push(field_name)
match input_map.get(field_name) {
None =>
if !is_optional_schema(field_schema) {
let path = format_path(path_stack)
let req_msg = if !field_schema.required_error.is_empty() {
field_schema.required_error
} else {
"Required"
}
errors.push(ValidationError::{
path,
message: req_msg,
got: Json::null(),
})
}
Some(field_json) => {
let result = parse_inner(field_schema, field_json, path_stack)
match result {
Err(field_errors) =>
for e in field_errors {
errors.push(e)
}
Ok(parsed) => parsed_fields.set(field_name, parsed)
}
}
}
let _ = path_stack.pop()
}
None => ()
}
}
match mode {
Strict =>
for key in input_map.keys() {
match input_map.get(key) {
Some(value) =>
if !spec.contains(key) {
path_stack.push(key)
let path = format_path(path_stack)
let _ = path_stack.pop()
errors.push(ValidationError::{
path,
message: "Unexpected field",
got: value,
})
}
None => ()
}
}
_ => ()
}
collect_errors(errors, path_stack, json, self.rules)
if errors.is_empty() {
match mode {
Strip => Ok(Json::object(parsed_fields))
_ => Ok(json)
}
} else {
Err(errors)
}
}
_ => {
let path = format_path(path_stack)
Err([ValidationError::{ path, message: type_error_msg(self), got: json }])
}
}
}