use regex::Regex;
use serde_json::Value;
use std::fmt;
#[derive(Debug, Clone)]
pub struct ValidationError {
pub path: String,
pub message: String,
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.path, self.message)
}
}
impl std::error::Error for ValidationError {}
pub type ValidationResult = Result<(), Vec<ValidationError>>;
pub fn validate(schema: &Value, value: &Value) -> ValidationResult {
let mut errors = Vec::new();
validate_internal(schema, value, "root", &mut errors);
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
pub fn validate_strict(schema: &Value, value: &Value) -> ValidationResult {
let strict_schema = make_strict_schema(schema);
validate(&strict_schema, value)
}
fn make_strict_schema(schema: &Value) -> Value {
match schema {
Value::Object(obj) => {
let mut new_obj = obj.clone();
if let Some(type_val) = obj.get("type") {
let is_object_type = type_val == "object"
|| type_val
.as_array()
.is_some_and(|arr| arr.iter().any(|t| t == "object"));
if is_object_type && !obj.contains_key("additionalProperties") {
new_obj.insert("additionalProperties".to_string(), Value::Bool(false));
}
}
if let Some(Value::Object(props)) = obj.get("properties") {
let strict_props: serde_json::Map<String, Value> = props
.iter()
.map(|(k, v)| (k.clone(), make_strict_schema(v)))
.collect();
new_obj.insert("properties".to_string(), Value::Object(strict_props));
}
if let Some(additional) = obj.get("additionalProperties") {
if additional.is_object() {
new_obj.insert(
"additionalProperties".to_string(),
make_strict_schema(additional),
);
}
}
if let Some(items) = obj.get("items") {
new_obj.insert("items".to_string(), make_strict_schema(items));
}
if let Some(Value::Array(arr)) = obj.get("prefixItems") {
let strict_items: Vec<Value> = arr.iter().map(make_strict_schema).collect();
new_obj.insert("prefixItems".to_string(), Value::Array(strict_items));
}
Value::Object(new_obj)
}
Value::Array(arr) => {
Value::Array(arr.iter().map(make_strict_schema).collect())
}
_ => schema.clone(),
}
}
fn validate_internal(schema: &Value, value: &Value, path: &str, errors: &mut Vec<ValidationError>) {
if let Some(b) = schema.as_bool() {
if !b {
errors.push(ValidationError {
path: path.to_string(),
message: "schema rejects all values".to_string(),
});
}
return;
}
let Some(schema_obj) = schema.as_object() else {
return; };
if let Some(type_val) = schema_obj.get("type") {
if !validate_type(type_val, value) {
let expected = type_val
.as_str()
.map(String::from)
.or_else(|| type_val.as_array().map(|arr| format!("{arr:?}")))
.unwrap_or_else(|| "unknown".to_string());
errors.push(ValidationError {
path: path.to_string(),
message: format!("expected type {expected}, got {}", json_type_name(value)),
});
return; }
}
if let Some(enum_val) = schema_obj.get("enum") {
if let Some(enum_arr) = enum_val.as_array() {
if !enum_arr.contains(value) {
errors.push(ValidationError {
path: path.to_string(),
message: format!("value must be one of: {enum_arr:?}"),
});
}
}
}
if let Some(const_val) = schema_obj.get("const") {
if value != const_val {
errors.push(ValidationError {
path: path.to_string(),
message: format!("value must equal {const_val}"),
});
}
}
match value {
Value::Object(obj) => {
validate_object(schema_obj, obj, path, errors);
}
Value::Array(arr) => {
validate_array(schema_obj, arr, path, errors);
}
Value::String(s) => {
validate_string(schema_obj, s, path, errors);
}
Value::Number(n) => {
validate_number(schema_obj, n, path, errors);
}
_ => {}
}
}
fn validate_type(type_val: &Value, value: &Value) -> bool {
match type_val {
Value::String(t) => matches_type(t, value),
Value::Array(types) => types.iter().any(|t| {
t.as_str()
.is_some_and(|type_str| matches_type(type_str, value))
}),
_ => true, }
}
fn matches_type(type_name: &str, value: &Value) -> bool {
match type_name {
"string" => value.is_string(),
"number" => value.is_number(),
"integer" => value.is_i64() || value.is_u64(),
"boolean" => value.is_boolean(),
"object" => value.is_object(),
"array" => value.is_array(),
"null" => value.is_null(),
_ => true, }
}
fn json_type_name(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(n) => {
if n.is_i64() || n.is_u64() {
"integer"
} else {
"number"
}
}
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
fn validate_object(
schema: &serde_json::Map<String, Value>,
obj: &serde_json::Map<String, Value>,
path: &str,
errors: &mut Vec<ValidationError>,
) {
if let Some(required) = schema.get("required").and_then(|v| v.as_array()) {
for req in required {
if let Some(req_name) = req.as_str() {
if !obj.contains_key(req_name) {
errors.push(ValidationError {
path: path.to_string(),
message: format!("missing required field: {req_name}"),
});
}
}
}
}
if let Some(properties) = schema.get("properties").and_then(|v| v.as_object()) {
for (key, value) in obj {
if let Some(prop_schema) = properties.get(key) {
let prop_path = format!("{path}.{key}");
validate_internal(prop_schema, value, &prop_path, errors);
}
}
}
if let Some(additional) = schema.get("additionalProperties") {
let properties = schema.get("properties").and_then(|v| v.as_object());
for (key, value) in obj {
let is_defined_property = properties.is_some_and(|p| p.contains_key(key));
if !is_defined_property {
match additional {
Value::Bool(false) => {
errors.push(ValidationError {
path: path.to_string(),
message: format!("additional property not allowed: {key}"),
});
}
Value::Object(_) => {
let prop_path = format!("{path}.{key}");
validate_internal(additional, value, &prop_path, errors);
}
_ => {}
}
}
}
}
if let Some(min) = schema
.get("minProperties")
.and_then(serde_json::Value::as_u64)
{
if (obj.len() as u64) < min {
errors.push(ValidationError {
path: path.to_string(),
message: format!("object must have at least {min} properties"),
});
}
}
if let Some(max) = schema
.get("maxProperties")
.and_then(serde_json::Value::as_u64)
{
if (obj.len() as u64) > max {
errors.push(ValidationError {
path: path.to_string(),
message: format!("object must have at most {max} properties"),
});
}
}
}
fn validate_array(
schema: &serde_json::Map<String, Value>,
arr: &[Value],
path: &str,
errors: &mut Vec<ValidationError>,
) {
let mut prefix_len = 0;
if let Some(prefix_items) = schema.get("prefixItems").and_then(|v| v.as_array()) {
prefix_len = prefix_items.len();
for (i, item_schema) in prefix_items.iter().enumerate() {
if let Some(item) = arr.get(i) {
let item_path = format!("{path}[{i}]");
validate_internal(item_schema, item, &item_path, errors);
}
}
}
if let Some(items_schema) = schema.get("items") {
if items_schema.is_array() && prefix_len == 0 {
if let Some(items_arr) = items_schema.as_array() {
for (i, item_schema) in items_arr.iter().enumerate() {
if let Some(item) = arr.get(i) {
let item_path = format!("{path}[{i}]");
validate_internal(item_schema, item, &item_path, errors);
}
}
}
} else if items_schema.is_object() || items_schema.is_boolean() {
for (i, item) in arr.iter().enumerate().skip(prefix_len) {
let item_path = format!("{path}[{i}]");
validate_internal(items_schema, item, &item_path, errors);
}
}
}
if let Some(min) = schema.get("minItems").and_then(serde_json::Value::as_u64) {
if (arr.len() as u64) < min {
errors.push(ValidationError {
path: path.to_string(),
message: format!("array must have at least {min} items"),
});
}
}
if let Some(max) = schema.get("maxItems").and_then(serde_json::Value::as_u64) {
if (arr.len() as u64) > max {
errors.push(ValidationError {
path: path.to_string(),
message: format!("array must have at most {max} items"),
});
}
}
if schema
.get("uniqueItems")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false)
{
let mut seen = std::collections::HashSet::with_capacity(arr.len());
for (i, item) in arr.iter().enumerate() {
let key = serde_json::to_string(item).unwrap_or_default();
if !seen.insert(key) {
errors.push(ValidationError {
path: format!("{path}[{i}]"),
message: "duplicate item in array".to_string(),
});
}
}
}
}
fn validate_string(
schema: &serde_json::Map<String, Value>,
s: &str,
path: &str,
errors: &mut Vec<ValidationError>,
) {
let len = s.chars().count();
if let Some(min) = schema.get("minLength").and_then(serde_json::Value::as_u64) {
if (len as u64) < min {
errors.push(ValidationError {
path: path.to_string(),
message: format!("string must be at least {min} characters"),
});
}
}
if let Some(max) = schema.get("maxLength").and_then(serde_json::Value::as_u64) {
if (len as u64) > max {
errors.push(ValidationError {
path: path.to_string(),
message: format!("string must be at most {max} characters"),
});
}
}
if let Some(pattern) = schema.get("pattern").and_then(serde_json::Value::as_str) {
match Regex::new(pattern) {
Ok(re) => {
if !re.is_match(s) {
errors.push(ValidationError {
path: path.to_string(),
message: format!("string does not match pattern {pattern:?}"),
});
}
}
Err(e) => {
errors.push(ValidationError {
path: path.to_string(),
message: format!("invalid schema pattern {pattern:?}: {e}"),
});
}
}
}
}
fn validate_number(
schema: &serde_json::Map<String, Value>,
n: &serde_json::Number,
path: &str,
errors: &mut Vec<ValidationError>,
) {
let val = n.as_f64().unwrap_or(0.0);
if let Some(min) = schema.get("minimum").and_then(serde_json::Value::as_f64) {
if val < min {
errors.push(ValidationError {
path: path.to_string(),
message: format!("value must be >= {min}"),
});
}
}
if let Some(max) = schema.get("maximum").and_then(serde_json::Value::as_f64) {
if val > max {
errors.push(ValidationError {
path: path.to_string(),
message: format!("value must be <= {max}"),
});
}
}
if let Some(min) = schema
.get("exclusiveMinimum")
.and_then(serde_json::Value::as_f64)
{
if val <= min {
errors.push(ValidationError {
path: path.to_string(),
message: format!("value must be > {min}"),
});
}
}
if let Some(max) = schema
.get("exclusiveMaximum")
.and_then(serde_json::Value::as_f64)
{
if val >= max {
errors.push(ValidationError {
path: path.to_string(),
message: format!("value must be < {max}"),
});
}
}
if let Some(multiple) = schema.get("multipleOf").and_then(serde_json::Value::as_f64) {
if multiple != 0.0 && (val % multiple).abs() > f64::EPSILON {
errors.push(ValidationError {
path: path.to_string(),
message: format!("value must be a multiple of {multiple}"),
});
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_type_validation_string() {
let schema = json!({"type": "string"});
assert!(validate(&schema, &json!("hello")).is_ok());
assert!(validate(&schema, &json!(123)).is_err());
}
#[test]
fn test_type_validation_number() {
let schema = json!({"type": "number"});
assert!(validate(&schema, &json!(123)).is_ok());
assert!(validate(&schema, &json!(12.5)).is_ok());
assert!(validate(&schema, &json!("hello")).is_err());
}
#[test]
fn test_type_validation_integer() {
let schema = json!({"type": "integer"});
assert!(validate(&schema, &json!(123)).is_ok());
assert!(validate(&schema, &json!(12.5)).is_err());
}
#[test]
fn test_type_validation_boolean() {
let schema = json!({"type": "boolean"});
assert!(validate(&schema, &json!(true)).is_ok());
assert!(validate(&schema, &json!(false)).is_ok());
assert!(validate(&schema, &json!(1)).is_err());
}
#[test]
fn test_type_validation_object() {
let schema = json!({"type": "object"});
assert!(validate(&schema, &json!({})).is_ok());
assert!(validate(&schema, &json!({"a": 1})).is_ok());
assert!(validate(&schema, &json!([])).is_err());
}
#[test]
fn test_type_validation_array() {
let schema = json!({"type": "array"});
assert!(validate(&schema, &json!([])).is_ok());
assert!(validate(&schema, &json!([1, 2, 3])).is_ok());
assert!(validate(&schema, &json!({})).is_err());
}
#[test]
fn test_type_validation_null() {
let schema = json!({"type": "null"});
assert!(validate(&schema, &json!(null)).is_ok());
assert!(validate(&schema, &json!(0)).is_err());
}
#[test]
fn test_type_validation_union() {
let schema = json!({"type": ["string", "number"]});
assert!(validate(&schema, &json!("hello")).is_ok());
assert!(validate(&schema, &json!(123)).is_ok());
assert!(validate(&schema, &json!(true)).is_err());
}
#[test]
fn test_required_fields() {
let schema = json!({
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name"]
});
assert!(validate(&schema, &json!({"name": "Alice"})).is_ok());
assert!(validate(&schema, &json!({"name": "Alice", "age": 30})).is_ok());
assert!(validate(&schema, &json!({"age": 30})).is_err());
assert!(validate(&schema, &json!({})).is_err());
}
#[test]
fn test_enum_validation() {
let schema = json!({"enum": ["red", "green", "blue"]});
assert!(validate(&schema, &json!("red")).is_ok());
assert!(validate(&schema, &json!("yellow")).is_err());
}
#[test]
fn test_const_validation() {
let schema = json!({"const": "fixed"});
assert!(validate(&schema, &json!("fixed")).is_ok());
assert!(validate(&schema, &json!("other")).is_err());
}
#[test]
fn test_string_length() {
let schema = json!({
"type": "string",
"minLength": 2,
"maxLength": 5
});
assert!(validate(&schema, &json!("ab")).is_ok());
assert!(validate(&schema, &json!("abcde")).is_ok());
assert!(validate(&schema, &json!("a")).is_err());
assert!(validate(&schema, &json!("abcdef")).is_err());
}
#[test]
fn test_string_pattern() {
let schema = json!({
"type": "string",
"pattern": "^[a-z]+$"
});
assert!(validate(&schema, &json!("hello")).is_ok());
assert!(validate(&schema, &json!("Hello")).is_err());
assert!(validate(&schema, &json!("hello123")).is_err());
}
#[test]
fn test_string_pattern_invalid_regex_is_error() {
let schema = json!({
"type": "string",
"pattern": "("
});
assert!(validate(&schema, &json!("anything")).is_err());
}
#[test]
fn test_number_range() {
let schema = json!({
"type": "number",
"minimum": 0,
"maximum": 100
});
assert!(validate(&schema, &json!(0)).is_ok());
assert!(validate(&schema, &json!(50)).is_ok());
assert!(validate(&schema, &json!(100)).is_ok());
assert!(validate(&schema, &json!(-1)).is_err());
assert!(validate(&schema, &json!(101)).is_err());
}
#[test]
fn test_number_exclusive_range() {
let schema = json!({
"type": "number",
"exclusiveMinimum": 0,
"exclusiveMaximum": 10
});
assert!(validate(&schema, &json!(1)).is_ok());
assert!(validate(&schema, &json!(9)).is_ok());
assert!(validate(&schema, &json!(0)).is_err());
assert!(validate(&schema, &json!(10)).is_err());
}
#[test]
fn test_array_items() {
let schema = json!({
"type": "array",
"items": {"type": "integer"}
});
assert!(validate(&schema, &json!([1, 2, 3])).is_ok());
assert!(validate(&schema, &json!([])).is_ok());
assert!(validate(&schema, &json!([1, "two", 3])).is_err());
}
#[test]
fn test_array_length() {
let schema = json!({
"type": "array",
"minItems": 1,
"maxItems": 3
});
assert!(validate(&schema, &json!([1])).is_ok());
assert!(validate(&schema, &json!([1, 2, 3])).is_ok());
assert!(validate(&schema, &json!([])).is_err());
assert!(validate(&schema, &json!([1, 2, 3, 4])).is_err());
}
#[test]
fn test_unique_items() {
let schema = json!({
"type": "array",
"uniqueItems": true
});
assert!(validate(&schema, &json!([1, 2, 3])).is_ok());
assert!(validate(&schema, &json!([1, 1, 2])).is_err());
}
#[test]
fn test_nested_object() {
let schema = json!({
"type": "object",
"properties": {
"person": {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name"]
}
}
});
assert!(validate(&schema, &json!({"person": {"name": "Alice"}})).is_ok());
assert!(validate(&schema, &json!({"person": {"name": "Alice", "age": 30}})).is_ok());
assert!(validate(&schema, &json!({"person": {"age": 30}})).is_err());
}
#[test]
fn test_additional_properties_false() {
let schema = json!({
"type": "object",
"properties": {
"name": {"type": "string"}
},
"additionalProperties": false
});
assert!(validate(&schema, &json!({"name": "Alice"})).is_ok());
assert!(validate(&schema, &json!({})).is_ok());
assert!(validate(&schema, &json!({"name": "Alice", "extra": 1})).is_err());
}
#[test]
fn test_boolean_schema() {
assert!(validate(&json!(true), &json!("anything")).is_ok());
assert!(validate(&json!(true), &json!(123)).is_ok());
assert!(validate(&json!(false), &json!("anything")).is_err());
}
#[test]
fn test_multiple_errors() {
let schema = json!({
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"]
});
let result = validate(&schema, &json!({}));
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 2); }
#[test]
fn test_error_path() {
let schema = json!({
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {"type": "integer"}
}
}
});
let result = validate(&schema, &json!({"items": [1, "two", 3]}));
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].path, "root.items[1]");
}
#[test]
fn test_validate_strict_rejects_extra_properties() {
let schema = json!({
"type": "object",
"properties": {
"name": {"type": "string"}
}
});
assert!(validate(&schema, &json!({"name": "Alice", "extra": 123})).is_ok());
assert!(validate_strict(&schema, &json!({"name": "Alice", "extra": 123})).is_err());
assert!(validate_strict(&schema, &json!({"name": "Alice"})).is_ok());
}
#[test]
fn test_validate_strict_nested_objects() {
let schema = json!({
"type": "object",
"properties": {
"person": {
"type": "object",
"properties": {
"name": {"type": "string"}
}
}
}
});
assert!(
validate(
&schema,
&json!({
"person": {"name": "Alice", "age": 30}
})
)
.is_ok()
);
assert!(
validate_strict(
&schema,
&json!({
"person": {"name": "Alice", "age": 30}
})
)
.is_err()
);
assert!(
validate_strict(
&schema,
&json!({
"person": {"name": "Alice"}
})
)
.is_ok()
);
}
#[test]
fn test_validate_strict_preserves_explicit_additional_properties() {
let schema = json!({
"type": "object",
"properties": {
"name": {"type": "string"}
},
"additionalProperties": {"type": "integer"}
});
assert!(
validate_strict(
&schema,
&json!({
"name": "Alice",
"count": 42
})
)
.is_ok()
);
assert!(
validate_strict(
&schema,
&json!({
"name": "Alice",
"count": "not an integer"
})
)
.is_err()
);
}
#[test]
fn test_validate_strict_array_items() {
let schema = json!({
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "integer"}
}
}
});
assert!(
validate(
&schema,
&json!([
{"id": 1, "extra": "value"}
])
)
.is_ok()
);
assert!(
validate_strict(
&schema,
&json!([
{"id": 1, "extra": "value"}
])
)
.is_err()
);
assert!(
validate_strict(
&schema,
&json!([
{"id": 1}
])
)
.is_ok()
);
}
#[test]
fn test_validate_strict_empty_schema() {
let schema = json!({});
assert!(validate_strict(&schema, &json!({"anything": "goes"})).is_ok());
}
#[test]
fn test_validate_strict_non_object_types() {
let string_schema = json!({"type": "string"});
assert!(validate_strict(&string_schema, &json!("hello")).is_ok());
let number_schema = json!({"type": "number"});
assert!(validate_strict(&number_schema, &json!(42)).is_ok());
let array_schema = json!({"type": "array"});
assert!(validate_strict(&array_schema, &json!([1, 2, 3])).is_ok());
}
#[test]
fn validation_error_display_and_error_trait() {
let err = ValidationError {
path: "root.name".to_string(),
message: "expected string".to_string(),
};
assert_eq!(err.to_string(), "root.name: expected string");
let _: &dyn std::error::Error = &err;
}
#[test]
fn validation_error_debug_and_clone() {
let err = ValidationError {
path: "root".to_string(),
message: "missing".to_string(),
};
let debug = format!("{err:?}");
assert!(debug.contains("ValidationError"));
let cloned = err.clone();
assert_eq!(cloned.path, "root");
assert_eq!(cloned.message, "missing");
}
#[test]
fn json_type_name_all_types() {
assert_eq!(json_type_name(&json!(null)), "null");
assert_eq!(json_type_name(&json!(true)), "boolean");
assert_eq!(json_type_name(&json!(42)), "integer");
assert_eq!(json_type_name(&json!(3.14)), "number");
assert_eq!(json_type_name(&json!("hello")), "string");
assert_eq!(json_type_name(&json!([])), "array");
assert_eq!(json_type_name(&json!({})), "object");
}
#[test]
fn number_multiple_of() {
let schema = json!({"type": "number", "multipleOf": 3});
assert!(validate(&schema, &json!(9)).is_ok());
assert!(validate(&schema, &json!(6)).is_ok());
assert!(validate(&schema, &json!(0)).is_ok());
assert!(validate(&schema, &json!(7)).is_err());
}
#[test]
fn object_min_max_properties() {
let schema = json!({
"type": "object",
"minProperties": 1,
"maxProperties": 2
});
assert!(validate(&schema, &json!({"a": 1})).is_ok());
assert!(validate(&schema, &json!({"a": 1, "b": 2})).is_ok());
assert!(validate(&schema, &json!({})).is_err());
assert!(validate(&schema, &json!({"a": 1, "b": 2, "c": 3})).is_err());
}
#[test]
fn prefix_items_tuple_validation() {
let schema = json!({
"type": "array",
"prefixItems": [
{"type": "string"},
{"type": "integer"}
]
});
assert!(validate(&schema, &json!(["hello", 42])).is_ok());
assert!(validate(&schema, &json!(["hello", 42, true])).is_ok()); assert!(validate(&schema, &json!([123, "wrong"])).is_err()); }
#[test]
fn prefix_items_with_additional_items_schema() {
let schema = json!({
"type": "array",
"prefixItems": [
{"type": "string"}
],
"items": {"type": "integer"}
});
assert!(validate(&schema, &json!(["hello", 1, 2])).is_ok());
assert!(validate(&schema, &json!(["hello", "bad"])).is_err());
}
#[test]
fn items_as_array_draft4_fallback() {
let schema = json!({
"type": "array",
"items": [
{"type": "string"},
{"type": "integer"}
]
});
assert!(validate(&schema, &json!(["hello", 42])).is_ok());
assert!(validate(&schema, &json!([123, "wrong"])).is_err());
}
#[test]
fn additional_properties_as_schema() {
let schema = json!({
"type": "object",
"properties": {
"name": {"type": "string"}
},
"additionalProperties": {"type": "integer"}
});
assert!(validate(&schema, &json!({"name": "Alice", "count": 42})).is_ok());
assert!(validate(&schema, &json!({"name": "Alice", "bad": "string"})).is_err());
}
#[test]
fn strict_schema_with_prefix_items() {
let schema = json!({
"type": "array",
"prefixItems": [
{
"type": "object",
"properties": {
"id": {"type": "integer"}
}
}
]
});
assert!(validate_strict(&schema, &json!([{"id": 1}])).is_ok());
assert!(validate_strict(&schema, &json!([{"id": 1, "extra": "val"}])).is_err());
}
#[test]
fn strict_schema_with_union_type() {
let schema = json!({
"type": ["object", "null"],
"properties": {
"name": {"type": "string"}
}
});
assert!(validate_strict(&schema, &json!(null)).is_ok());
assert!(validate_strict(&schema, &json!({"name": "Alice"})).is_ok());
assert!(validate_strict(&schema, &json!({"name": "Alice", "extra": 1})).is_err());
}
#[test]
fn unknown_type_in_matches_type_is_permissive() {
let schema = json!({"type": "custom_extension"});
assert!(validate(&schema, &json!("anything")).is_ok());
assert!(validate(&schema, &json!(42)).is_ok());
}
#[test]
fn invalid_schema_not_an_object() {
assert!(validate(&json!(42), &json!("anything")).is_ok());
assert!(validate(&json!("bad_schema"), &json!(123)).is_ok());
}
}