#![allow(clippy::unnecessary_wraps)]
#[cfg(all(
target_arch = "wasm32",
target_os = "unknown",
any(feature = "resolve-file", feature = "resolve-http")
))]
compile_error!(
"Features 'resolve-http' and 'resolve-file' are not supported on wasm32-unknown-unknown"
);
#[cfg(all(
not(target_arch = "wasm32"),
feature = "resolve-http",
not(any(feature = "tls-aws-lc-rs", feature = "tls-ring"))
))]
compile_error!(
"Feature `resolve-http` requires a TLS provider: enable `tls-aws-lc-rs` \
(default) or `tls-ring`."
);
pub(crate) mod bundler;
pub mod canonical;
pub(crate) mod compiler;
mod content_encoding;
mod content_media_type;
pub(crate) mod dereferencer;
mod ecma;
pub mod error;
mod evaluation;
#[doc(hidden)]
pub mod ext;
mod http;
mod keywords;
mod node;
mod options;
pub mod output;
pub mod paths;
pub(crate) mod properties;
pub(crate) mod regex;
mod retriever;
pub mod types;
mod validator;
pub use error::{
ErrorIterator, MaskedValidationError, ValidationError, ValidationErrorParts, ValidationErrors,
};
pub use evaluation::{
AnnotationEntry, ErrorEntry, Evaluation, FlagOutput, HierarchicalOutput, ListOutput,
};
pub use http::HttpOptions;
pub use keywords::custom::Keyword;
pub use options::{EmailOptions, FancyRegex, PatternOptions, Regex, ValidationOptions};
pub use referencing::{
uri, Draft, Error as ReferencingError, Registry, RegistryBuilder, Resource, Retrieve, Uri,
};
#[cfg(all(feature = "resolve-http", not(target_arch = "wasm32")))]
pub use retriever::{HttpRetriever, HttpRetrieverError};
pub use types::{JsonType, JsonTypeSet, JsonTypeSetIterator};
pub use validator::{ValidationContext, Validator, ValidatorMap};
#[cfg(feature = "resolve-async")]
pub use referencing::AsyncRetrieve;
#[cfg(all(
feature = "resolve-http",
feature = "resolve-async",
not(target_arch = "wasm32")
))]
pub use retriever::AsyncHttpRetriever;
use serde_json::Value;
#[must_use]
#[inline]
pub fn is_valid(schema: &Value, instance: &Value) -> bool {
validator_for(schema)
.expect("Invalid schema")
.is_valid(instance)
}
#[inline]
pub fn validate<'i>(schema: &Value, instance: &'i Value) -> Result<(), ValidationError<'i>> {
validator_for(schema)
.expect("Invalid schema")
.validate(instance)
}
#[must_use]
#[inline]
pub fn evaluate(schema: &Value, instance: &Value) -> Evaluation {
validator_for(schema)
.expect("Invalid schema")
.evaluate(instance)
}
pub fn validator_for(schema: &Value) -> Result<Validator, ValidationError<'static>> {
Validator::new(schema)
}
pub fn validator_map_for(schema: &Value) -> Result<ValidatorMap, ValidationError<'static>> {
options().build_map(schema)
}
pub fn bundle(schema: &Value) -> Result<Value, ReferencingError> {
options().bundle(schema)
}
pub fn dereference(schema: &Value) -> Result<Value, ReferencingError> {
options().dereference(schema)
}
#[cfg(feature = "resolve-async")]
pub async fn async_bundle(schema: &Value) -> Result<Value, ReferencingError> {
async_options().bundle(schema).await
}
#[cfg(feature = "resolve-async")]
pub async fn async_dereference(schema: &Value) -> Result<Value, ReferencingError> {
async_options().dereference(schema).await
}
#[cfg(feature = "resolve-async")]
pub async fn async_validator_for(schema: &Value) -> Result<Validator, ValidationError<'static>> {
Validator::async_new(schema).await
}
#[cfg(feature = "resolve-async")]
pub async fn async_validator_map_for(
schema: &Value,
) -> Result<ValidatorMap, ValidationError<'static>> {
async_options().build_map(schema).await
}
#[must_use]
pub fn options<'i>() -> ValidationOptions<'i> {
Validator::options()
}
#[cfg(feature = "resolve-async")]
#[must_use]
pub fn async_options<'i>() -> ValidationOptions<'i, std::sync::Arc<dyn AsyncRetrieve>> {
Validator::async_options()
}
pub mod meta {
use crate::{error::ValidationError, Draft, Registry};
use ahash::AHashSet;
use referencing::Retrieve;
use serde_json::Value;
pub use validator_handle::MetaValidator;
#[must_use]
pub fn options<'a>() -> MetaSchemaOptions<'a> {
MetaSchemaOptions::default()
}
#[derive(Clone, Default)]
pub struct MetaSchemaOptions<'a> {
registry: Option<&'a Registry<'a>>,
}
impl<'a> MetaSchemaOptions<'a> {
#[must_use]
pub fn with_registry(mut self, registry: &'a Registry<'a>) -> Self {
self.registry = Some(registry);
self
}
#[must_use]
pub fn is_valid(&self, schema: &Value) -> bool {
match try_meta_validator_for(schema, self.registry) {
Ok(validator) => validator.as_ref().is_valid(schema),
Err(e) => panic!("Failed to resolve meta-schema: {e}"),
}
}
pub fn validate<'schema>(
&self,
schema: &'schema Value,
) -> Result<(), ValidationError<'schema>> {
let validator = try_meta_validator_for(schema, self.registry)?;
validator.as_ref().validate(schema)
}
}
mod validator_handle {
use crate::Validator;
use std::{marker::PhantomData, ops::Deref};
pub struct MetaValidator<'a>(MetaValidatorInner<'a>);
enum MetaValidatorInner<'a> {
#[cfg(not(target_family = "wasm"))]
Borrowed(&'a Validator),
Owned(Box<Validator>, PhantomData<&'a Validator>),
}
#[cfg_attr(target_family = "wasm", allow(clippy::elidable_lifetime_names))]
impl<'a> MetaValidator<'a> {
#[cfg(not(target_family = "wasm"))]
pub(crate) fn borrowed(validator: &'a Validator) -> Self {
Self(MetaValidatorInner::Borrowed(validator))
}
pub(crate) fn owned(validator: Validator) -> Self {
Self(MetaValidatorInner::Owned(Box::new(validator), PhantomData))
}
}
impl AsRef<Validator> for MetaValidator<'_> {
fn as_ref(&self) -> &Validator {
match &self.0 {
#[cfg(not(target_family = "wasm"))]
MetaValidatorInner::Borrowed(validator) => validator,
MetaValidatorInner::Owned(validator, _) => validator,
}
}
}
impl Deref for MetaValidator<'_> {
type Target = Validator;
fn deref(&self) -> &Self::Target {
self.as_ref()
}
}
}
pub(crate) mod validators {
use crate::Validator;
#[cfg(not(target_family = "wasm"))]
use std::sync::LazyLock;
fn build_validator(schema: &serde_json::Value) -> Validator {
crate::options()
.without_schema_validation()
.build(schema)
.expect("Meta-schema should be valid")
}
#[cfg(not(target_family = "wasm"))]
pub(crate) static DRAFT4_META_VALIDATOR: LazyLock<Validator> =
LazyLock::new(|| build_validator(&referencing::meta::DRAFT4));
#[cfg(target_family = "wasm")]
pub(crate) fn draft4_meta_validator() -> Validator {
build_validator(&referencing::meta::DRAFT4)
}
#[cfg(not(target_family = "wasm"))]
pub(crate) static DRAFT6_META_VALIDATOR: LazyLock<Validator> =
LazyLock::new(|| build_validator(&referencing::meta::DRAFT6));
#[cfg(target_family = "wasm")]
pub(crate) fn draft6_meta_validator() -> Validator {
build_validator(&referencing::meta::DRAFT6)
}
#[cfg(not(target_family = "wasm"))]
pub(crate) static DRAFT7_META_VALIDATOR: LazyLock<Validator> =
LazyLock::new(|| build_validator(&referencing::meta::DRAFT7));
#[cfg(target_family = "wasm")]
pub(crate) fn draft7_meta_validator() -> Validator {
build_validator(&referencing::meta::DRAFT7)
}
#[cfg(not(target_family = "wasm"))]
pub(crate) static DRAFT201909_META_VALIDATOR: LazyLock<Validator> =
LazyLock::new(|| build_validator(&referencing::meta::DRAFT201909));
#[cfg(target_family = "wasm")]
pub(crate) fn draft201909_meta_validator() -> Validator {
build_validator(&referencing::meta::DRAFT201909)
}
#[cfg(not(target_family = "wasm"))]
pub(crate) static DRAFT202012_META_VALIDATOR: LazyLock<Validator> =
LazyLock::new(|| build_validator(&referencing::meta::DRAFT202012));
#[cfg(target_family = "wasm")]
pub(crate) fn draft202012_meta_validator() -> Validator {
build_validator(&referencing::meta::DRAFT202012)
}
}
pub(crate) fn validator_for_draft(draft: Draft) -> MetaValidator<'static> {
#[cfg(not(target_family = "wasm"))]
{
match draft {
Draft::Draft4 => MetaValidator::borrowed(&validators::DRAFT4_META_VALIDATOR),
Draft::Draft6 => MetaValidator::borrowed(&validators::DRAFT6_META_VALIDATOR),
Draft::Draft7 => MetaValidator::borrowed(&validators::DRAFT7_META_VALIDATOR),
Draft::Draft201909 => {
MetaValidator::borrowed(&validators::DRAFT201909_META_VALIDATOR)
}
_ => MetaValidator::borrowed(&validators::DRAFT202012_META_VALIDATOR),
}
}
#[cfg(target_family = "wasm")]
{
let validator = match draft {
Draft::Draft4 => validators::draft4_meta_validator(),
Draft::Draft6 => validators::draft6_meta_validator(),
Draft::Draft7 => validators::draft7_meta_validator(),
Draft::Draft201909 => validators::draft201909_meta_validator(),
_ => validators::draft202012_meta_validator(),
};
MetaValidator::owned(validator)
}
}
#[must_use]
pub fn is_valid(schema: &Value) -> bool {
match try_meta_validator_for(schema, None) {
Ok(validator) => validator.as_ref().is_valid(schema),
Err(error) => panic!("Failed to resolve meta-schema: {error}"),
}
}
pub fn validate(schema: &Value) -> Result<(), ValidationError<'_>> {
let validator = try_meta_validator_for(schema, None)?;
validator.as_ref().validate(schema)
}
pub fn validator_for(
schema: &Value,
) -> Result<MetaValidator<'static>, ValidationError<'static>> {
try_meta_validator_for(schema, None)
}
fn try_meta_validator_for<'a>(
schema: &Value,
registry: Option<&'a Registry<'a>>,
) -> Result<MetaValidator<'a>, ValidationError<'static>> {
let draft = Draft::default().detect(schema);
if draft == Draft::Unknown {
if let Some(meta_schema_uri) = schema
.as_object()
.and_then(|obj| obj.get("$schema"))
.and_then(|s| s.as_str())
{
if let Some(registry) = registry {
let (custom_meta_schema, resolved_draft) =
resolve_meta_schema_with_registry(meta_schema_uri, registry)?;
let validator = crate::options()
.with_draft(resolved_draft)
.with_registry(registry)
.with_base_uri(meta_schema_uri.trim_end_matches('#'))
.without_schema_validation()
.build(&custom_meta_schema)?;
return Ok(MetaValidator::owned(validator));
}
let (custom_meta_schema, resolved_draft) =
resolve_meta_schema_chain(meta_schema_uri)?;
let validator = crate::options()
.with_draft(resolved_draft)
.without_schema_validation()
.build(&custom_meta_schema)?;
return Ok(MetaValidator::owned(validator));
}
}
Ok(validator_for_draft(draft))
}
fn resolve_meta_schema_with_registry(
uri: &str,
registry: &Registry<'_>,
) -> Result<(Value, Draft), ValidationError<'static>> {
let resolver = registry.resolver(referencing::uri::from_str(uri)?);
let first_resolved = resolver.lookup("")?;
let first_meta_schema = first_resolved.contents().clone();
let draft = walk_meta_schema_chain(uri, |current_uri| {
let resolver = registry.resolver(referencing::uri::from_str(current_uri)?);
let resolved = resolver.lookup("")?;
Ok(resolved.contents().clone())
})?;
Ok((first_meta_schema, draft))
}
fn resolve_meta_schema_chain(uri: &str) -> Result<(Value, Draft), ValidationError<'static>> {
let retriever = crate::retriever::DefaultRetriever;
let first_meta_uri = referencing::uri::from_str(uri)?;
let first_meta_schema = retriever
.retrieve(&first_meta_uri)
.map_err(|e| referencing::Error::unretrievable(uri, e))?;
let draft = walk_meta_schema_chain(uri, |current_uri| {
let meta_uri = referencing::uri::from_str(current_uri)?;
retriever
.retrieve(&meta_uri)
.map_err(|e| referencing::Error::unretrievable(current_uri, e))
})?;
Ok((first_meta_schema, draft))
}
pub(crate) fn walk_meta_schema_chain(
start_uri: &str,
mut fetch: impl FnMut(&str) -> Result<Value, referencing::Error>,
) -> Result<Draft, referencing::Error> {
let mut visited = AHashSet::new();
let mut current_uri = start_uri.to_string();
loop {
if !visited.insert(current_uri.clone()) {
return Err(referencing::Error::circular_metaschema(current_uri));
}
let meta_schema = fetch(¤t_uri)?;
let draft = Draft::default().detect(&meta_schema);
if draft != Draft::Unknown {
return Ok(draft);
}
current_uri = meta_schema
.get("$schema")
.and_then(|s| s.as_str())
.expect("`$schema` must exist when draft is Unknown")
.to_string();
}
}
}
pub mod draft4 {
use super::{Draft, ValidationError, ValidationOptions, Validator, Value};
pub fn new(schema: &Value) -> Result<Validator, ValidationError<'static>> {
options().build(schema)
}
#[must_use]
pub fn is_valid(schema: &Value, instance: &Value) -> bool {
new(schema).expect("Invalid schema").is_valid(instance)
}
pub fn validate<'i>(schema: &Value, instance: &'i Value) -> Result<(), ValidationError<'i>> {
new(schema).expect("Invalid schema").validate(instance)
}
#[must_use]
pub fn options<'i>() -> ValidationOptions<'i> {
crate::options().with_draft(Draft::Draft4)
}
pub mod meta {
use crate::{meta::MetaValidator, ValidationError};
use serde_json::Value;
#[must_use]
pub fn validator() -> MetaValidator<'static> {
crate::meta::validator_for_draft(super::Draft::Draft4)
}
#[must_use]
#[inline]
pub fn is_valid(schema: &Value) -> bool {
validator().as_ref().is_valid(schema)
}
#[inline]
pub fn validate(schema: &Value) -> Result<(), ValidationError<'_>> {
validator().as_ref().validate(schema)
}
}
}
pub mod draft6 {
use super::{Draft, ValidationError, ValidationOptions, Validator, Value};
pub fn new(schema: &Value) -> Result<Validator, ValidationError<'static>> {
options().build(schema)
}
#[must_use]
pub fn is_valid(schema: &Value, instance: &Value) -> bool {
new(schema).expect("Invalid schema").is_valid(instance)
}
pub fn validate<'i>(schema: &Value, instance: &'i Value) -> Result<(), ValidationError<'i>> {
new(schema).expect("Invalid schema").validate(instance)
}
#[must_use]
pub fn options<'i>() -> ValidationOptions<'i> {
crate::options().with_draft(Draft::Draft6)
}
pub mod meta {
use crate::{meta::MetaValidator, ValidationError};
use serde_json::Value;
#[must_use]
pub fn validator() -> MetaValidator<'static> {
crate::meta::validator_for_draft(super::Draft::Draft6)
}
#[must_use]
#[inline]
pub fn is_valid(schema: &Value) -> bool {
validator().as_ref().is_valid(schema)
}
#[inline]
pub fn validate(schema: &Value) -> Result<(), ValidationError<'_>> {
validator().as_ref().validate(schema)
}
}
}
pub mod draft7 {
use super::{Draft, ValidationError, ValidationOptions, Validator, Value};
pub fn new(schema: &Value) -> Result<Validator, ValidationError<'static>> {
options().build(schema)
}
#[must_use]
pub fn is_valid(schema: &Value, instance: &Value) -> bool {
new(schema).expect("Invalid schema").is_valid(instance)
}
pub fn validate<'i>(schema: &Value, instance: &'i Value) -> Result<(), ValidationError<'i>> {
new(schema).expect("Invalid schema").validate(instance)
}
#[must_use]
pub fn options<'i>() -> ValidationOptions<'i> {
crate::options().with_draft(Draft::Draft7)
}
pub mod meta {
use crate::{meta::MetaValidator, ValidationError};
use serde_json::Value;
#[must_use]
pub fn validator() -> MetaValidator<'static> {
crate::meta::validator_for_draft(super::Draft::Draft7)
}
#[must_use]
#[inline]
pub fn is_valid(schema: &Value) -> bool {
validator().as_ref().is_valid(schema)
}
#[inline]
pub fn validate(schema: &Value) -> Result<(), ValidationError<'_>> {
validator().as_ref().validate(schema)
}
}
}
pub mod draft201909 {
use super::{Draft, ValidationError, ValidationOptions, Validator, Value};
pub fn new(schema: &Value) -> Result<Validator, ValidationError<'static>> {
options().build(schema)
}
#[must_use]
pub fn is_valid(schema: &Value, instance: &Value) -> bool {
new(schema).expect("Invalid schema").is_valid(instance)
}
pub fn validate<'i>(schema: &Value, instance: &'i Value) -> Result<(), ValidationError<'i>> {
new(schema).expect("Invalid schema").validate(instance)
}
#[must_use]
pub fn options<'i>() -> ValidationOptions<'i> {
crate::options().with_draft(Draft::Draft201909)
}
pub mod meta {
use crate::{meta::MetaValidator, ValidationError};
use serde_json::Value;
#[must_use]
pub fn validator() -> MetaValidator<'static> {
crate::meta::validator_for_draft(super::Draft::Draft201909)
}
#[must_use]
#[inline]
pub fn is_valid(schema: &Value) -> bool {
validator().as_ref().is_valid(schema)
}
#[inline]
pub fn validate(schema: &Value) -> Result<(), ValidationError<'_>> {
validator().as_ref().validate(schema)
}
}
}
pub mod draft202012 {
use super::{Draft, ValidationError, ValidationOptions, Validator, Value};
pub fn new(schema: &Value) -> Result<Validator, ValidationError<'static>> {
options().build(schema)
}
#[must_use]
pub fn is_valid(schema: &Value, instance: &Value) -> bool {
new(schema).expect("Invalid schema").is_valid(instance)
}
pub fn validate<'i>(schema: &Value, instance: &'i Value) -> Result<(), ValidationError<'i>> {
new(schema).expect("Invalid schema").validate(instance)
}
#[must_use]
pub fn options<'i>() -> ValidationOptions<'i> {
crate::options().with_draft(Draft::Draft202012)
}
pub mod meta {
use crate::{meta::MetaValidator, ValidationError};
use serde_json::Value;
#[must_use]
pub fn validator() -> MetaValidator<'static> {
crate::meta::validator_for_draft(super::Draft::Draft202012)
}
#[must_use]
#[inline]
pub fn is_valid(schema: &Value) -> bool {
validator().as_ref().is_valid(schema)
}
#[inline]
pub fn validate(schema: &Value) -> Result<(), ValidationError<'_>> {
validator().as_ref().validate(schema)
}
}
}
#[cfg(test)]
pub(crate) mod tests_util {
use super::Validator;
use crate::ValidationError;
use serde_json::Value;
#[track_caller]
pub(crate) fn is_not_valid_with(validator: &Validator, instance: &Value) {
assert!(
!validator.is_valid(instance),
"{instance} should not be valid (via is_valid)",
);
assert!(
validator.validate(instance).is_err(),
"{instance} should not be valid (via validate)",
);
assert!(
validator.iter_errors(instance).next().is_some(),
"{instance} should not be valid (via validate)",
);
let evaluation = validator.evaluate(instance);
assert!(
!evaluation.flag().valid,
"{instance} should not be valid (via evaluate)",
);
}
#[track_caller]
pub(crate) fn is_not_valid(schema: &Value, instance: &Value) {
let validator = crate::options()
.should_validate_formats(true)
.build(schema)
.expect("Invalid schema");
is_not_valid_with(&validator, instance);
}
pub(crate) fn expect_errors(schema: &Value, instance: &Value, errors: &[&str]) {
let mut actual = crate::validator_for(schema)
.expect("Should be a valid schema")
.iter_errors(instance)
.map(|e| e.to_string())
.collect::<Vec<String>>();
actual.sort();
assert_eq!(actual, errors);
}
#[track_caller]
pub(crate) fn is_valid_with(validator: &Validator, instance: &Value) {
if let Some(first) = validator.iter_errors(instance).next() {
panic!(
"{} should be valid (via validate). Error: {} at {}",
instance,
first,
first.instance_path()
);
}
assert!(
validator.is_valid(instance),
"{instance} should be valid (via is_valid)",
);
assert!(
validator.validate(instance).is_ok(),
"{instance} should be valid (via is_valid)",
);
let evaluation = validator.evaluate(instance);
assert!(
evaluation.flag().valid,
"{instance} should be valid (via evaluate)",
);
}
#[track_caller]
pub(crate) fn is_valid(schema: &Value, instance: &Value) {
let validator = crate::options()
.should_validate_formats(true)
.build(schema)
.expect("Invalid schema");
is_valid_with(&validator, instance);
}
#[track_caller]
pub(crate) fn validate(schema: &Value, instance: &Value) -> ValidationError<'static> {
let validator = crate::options()
.should_validate_formats(true)
.build(schema)
.expect("Invalid schema");
let err = validator
.validate(instance)
.expect_err("Should be an error")
.to_owned();
err
}
#[track_caller]
pub(crate) fn assert_schema_location(schema: &Value, instance: &Value, expected: &str) {
let error = validate(schema, instance);
assert_eq!(error.schema_path().as_str(), expected);
}
#[track_caller]
pub(crate) fn assert_evaluation_path(schema: &Value, instance: &Value, expected: &str) {
let error = validate(schema, instance);
assert_eq!(error.evaluation_path().as_str(), expected);
}
#[track_caller]
pub(crate) fn assert_locations(schema: &Value, instance: &Value, expected: &[&str]) {
let validator = crate::validator_for(schema).unwrap();
let mut errors: Vec<_> = validator
.iter_errors(instance)
.map(|error| error.schema_path().as_str().to_string())
.collect();
errors.sort();
for (error, location) in errors.into_iter().zip(expected) {
assert_eq!(error, *location);
}
}
#[track_caller]
pub(crate) fn assert_keyword_location(
validator: &Validator,
instance: &Value,
instance_pointer: &str,
keyword_pointer: &str,
) {
fn pointer_from_schema_location(location: &str) -> &str {
location
.split_once('#')
.map_or(location, |(_, fragment)| fragment)
}
let evaluation = validator.evaluate(instance);
let serialized =
serde_json::to_value(evaluation.list()).expect("List output should be serializable");
let details = serialized
.get("details")
.and_then(|value| value.as_array())
.expect("List output must contain details");
let mut available = Vec::new();
for entry in details {
let Some(instance_location) = entry
.get("instanceLocation")
.and_then(|value| value.as_str())
else {
continue;
};
if instance_location != instance_pointer {
continue;
}
let schema_location = entry
.get("schemaLocation")
.and_then(|value| value.as_str())
.unwrap_or("");
let pointer = pointer_from_schema_location(schema_location);
if pointer == keyword_pointer {
return;
}
available.push(pointer.to_string());
}
panic!(
"No annotation for instance pointer `{instance_pointer}` with keyword location `{keyword_pointer}`. Available keyword locations for pointer: {available:?}"
);
}
#[track_caller]
pub(crate) fn is_valid_with_draft4(schema: &Value, instance: &Value) {
let validator = crate::options()
.with_draft(crate::Draft::Draft4)
.should_validate_formats(true)
.build(schema)
.expect("Invalid schema");
is_valid_with(&validator, instance);
}
#[track_caller]
pub(crate) fn is_not_valid_with_draft4(schema: &Value, instance: &Value) {
let validator = crate::options()
.with_draft(crate::Draft::Draft4)
.should_validate_formats(true)
.build(schema)
.expect("Invalid schema");
is_not_valid_with(&validator, instance);
}
}
#[cfg(test)]
mod tests {
use crate::{validator_for, Registry, ValidationError};
use super::Draft;
use serde_json::json;
use test_case::test_case;
#[test_case(crate::is_valid ; "autodetect")]
#[test_case(crate::draft4::is_valid ; "draft4")]
#[test_case(crate::draft6::is_valid ; "draft6")]
#[test_case(crate::draft7::is_valid ; "draft7")]
#[test_case(crate::draft201909::is_valid ; "draft201909")]
#[test_case(crate::draft202012::is_valid ; "draft202012")]
fn test_is_valid(is_valid_fn: fn(&serde_json::Value, &serde_json::Value) -> bool) {
let schema = json!({
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer", "minimum": 0}
},
"required": ["name"]
});
let valid_instance = json!({
"name": "John Doe",
"age": 30
});
let invalid_instance = json!({
"age": -5
});
assert!(is_valid_fn(&schema, &valid_instance));
assert!(!is_valid_fn(&schema, &invalid_instance));
}
#[test_case(crate::validate ; "autodetect")]
#[test_case(crate::draft4::validate ; "draft4")]
#[test_case(crate::draft6::validate ; "draft6")]
#[test_case(crate::draft7::validate ; "draft7")]
#[test_case(crate::draft201909::validate ; "draft201909")]
#[test_case(crate::draft202012::validate ; "draft202012")]
fn test_validate(
validate_fn: for<'i> fn(
&serde_json::Value,
&'i serde_json::Value,
) -> Result<(), ValidationError<'i>>,
) {
let schema = json!({
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer", "minimum": 0}
},
"required": ["name"]
});
let valid_instance = json!({
"name": "John Doe",
"age": 30
});
let invalid_instance = json!({
"age": -5
});
assert!(validate_fn(&schema, &valid_instance).is_ok());
assert!(validate_fn(&schema, &invalid_instance).is_err());
}
#[test]
fn test_evaluate() {
let schema = json!({
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer", "minimum": 0}
},
"required": ["name"]
});
let valid_instance = json!({
"name": "John Doe",
"age": 30
});
let invalid_instance = json!({
"age": -5
});
let valid_eval = crate::evaluate(&schema, &valid_instance);
assert!(valid_eval.flag().valid);
let invalid_eval = crate::evaluate(&schema, &invalid_instance);
assert!(!invalid_eval.flag().valid);
let errors: Vec<_> = invalid_eval.iter_errors().collect();
assert!(!errors.is_empty());
}
#[test_case(crate::meta::validate, crate::meta::is_valid ; "autodetect")]
#[test_case(crate::draft4::meta::validate, crate::draft4::meta::is_valid ; "draft4")]
#[test_case(crate::draft6::meta::validate, crate::draft6::meta::is_valid ; "draft6")]
#[test_case(crate::draft7::meta::validate, crate::draft7::meta::is_valid ; "draft7")]
#[test_case(crate::draft201909::meta::validate, crate::draft201909::meta::is_valid ; "draft201909")]
#[test_case(crate::draft202012::meta::validate, crate::draft202012::meta::is_valid ; "draft202012")]
fn test_meta_validation(
validate_fn: fn(&serde_json::Value) -> Result<(), ValidationError>,
is_valid_fn: fn(&serde_json::Value) -> bool,
) {
let valid = json!({
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer", "minimum": 0}
},
"required": ["name"]
});
let invalid = json!({
"type": "invalid_type",
"minimum": "not_a_number",
"required": true });
assert!(validate_fn(&valid).is_ok());
assert!(validate_fn(&invalid).is_err());
assert!(is_valid_fn(&valid));
assert!(!is_valid_fn(&invalid));
}
#[test]
fn test_exclusive_minimum_across_drafts() {
let draft4_schema = json!({
"$schema": "http://json-schema.org/draft-04/schema#",
"minimum": 5,
"exclusiveMinimum": true
});
assert!(crate::meta::is_valid(&draft4_schema));
assert!(crate::meta::validate(&draft4_schema).is_ok());
let invalid_draft4 = json!({
"$schema": "http://json-schema.org/draft-04/schema#",
"exclusiveMinimum": 5
});
assert!(!crate::meta::is_valid(&invalid_draft4));
assert!(crate::meta::validate(&invalid_draft4).is_err());
let drafts = [
"http://json-schema.org/draft-06/schema#",
"http://json-schema.org/draft-07/schema#",
"https://json-schema.org/draft/2019-09/schema",
"https://json-schema.org/draft/2020-12/schema",
];
for uri in drafts {
let valid_schema = json!({
"$schema": uri,
"exclusiveMinimum": 5
});
assert!(
crate::meta::is_valid(&valid_schema),
"Schema should be valid for {uri}"
);
assert!(
crate::meta::validate(&valid_schema).is_ok(),
"Schema validation should succeed for {uri}",
);
let invalid_schema = json!({
"$schema": uri,
"minimum": 5,
"exclusiveMinimum": true
});
assert!(
!crate::meta::is_valid(&invalid_schema),
"Schema should be invalid for {uri}",
);
assert!(
crate::meta::validate(&invalid_schema).is_err(),
"Schema validation should fail for {uri}",
);
}
}
#[test_case(
"http://json-schema.org/draft-04/schema#",
true,
5,
true ; "draft4 valid"
)]
#[test_case(
"http://json-schema.org/draft-04/schema#",
5,
true,
false ; "draft4 invalid"
)]
#[test_case(
"http://json-schema.org/draft-06/schema#",
5,
true,
false ; "draft6 invalid"
)]
#[test_case(
"http://json-schema.org/draft-07/schema#",
5,
true,
false ; "draft7 invalid"
)]
#[test_case(
"https://json-schema.org/draft/2019-09/schema",
5,
true,
false ; "draft2019-09 invalid"
)]
#[test_case(
"https://json-schema.org/draft/2020-12/schema",
5,
true,
false ; "draft2020-12 invalid"
)]
fn test_exclusive_minimum_detection(
schema_uri: &str,
exclusive_minimum: impl Into<serde_json::Value>,
minimum: impl Into<serde_json::Value>,
expected: bool,
) {
let schema = json!({
"$schema": schema_uri,
"minimum": minimum.into(),
"exclusiveMinimum": exclusive_minimum.into()
});
let is_valid_result = crate::meta::is_valid(&schema);
assert_eq!(is_valid_result, expected);
let validate_result = crate::meta::validate(&schema);
assert_eq!(validate_result.is_ok(), expected);
}
#[test]
fn test_invalid_schema_uri() {
let schema = json!({
"$schema": "invalid-uri",
"type": "string"
});
let result = crate::options().without_schema_validation().build(&schema);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error.to_string().contains("Unknown meta-schema"));
assert!(error.to_string().contains("invalid-uri"));
}
#[test]
fn test_invalid_schema_keyword() {
let schema = json!({
"$schema": "htt://json-schema.org/draft-07/schema",
"type": "string"
});
let result = crate::options().without_schema_validation().build(&schema);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error.to_string().contains("Unknown meta-schema"));
assert!(error
.to_string()
.contains("htt://json-schema.org/draft-07/schema"));
}
#[test_case(Draft::Draft4)]
#[test_case(Draft::Draft6)]
#[test_case(Draft::Draft7)]
fn meta_schemas(draft: Draft) {
for schema in [json!({"enum": [0, 0.0]}), json!({"enum": []})] {
assert!(crate::options().with_draft(draft).build(&schema).is_ok());
}
}
#[test]
fn incomplete_escape_in_pattern() {
let schema = json!({"pattern": "\\u"});
assert!(crate::validator_for(&schema).is_err());
}
#[test]
fn validation_error_propagation() {
fn foo() -> Result<(), Box<dyn std::error::Error>> {
let schema = json!({});
let validator = validator_for(&schema)?;
let _ = validator.is_valid(&json!({}));
Ok(())
}
let _ = foo();
}
#[test]
fn test_meta_validation_with_unknown_schema() {
let schema = json!({
"$schema": "json-schema:///custom",
"type": "string"
});
assert!(crate::meta::validate(&schema).is_err());
let result = crate::validator_for(&schema);
assert!(result.is_err());
}
#[test]
#[cfg(all(not(target_arch = "wasm32"), feature = "resolve-file"))]
fn test_meta_validation_respects_metaschema_draft() {
use std::io::Write;
let mut temp_file = tempfile::NamedTempFile::new().expect("Failed to create temp file");
let meta_schema_draft7 = json!({
"$id": "http://example.com/meta/draft7",
"$schema": "http://json-schema.org/draft-07/schema",
"type": ["object", "boolean"],
"properties": {
"$schema": { "type": "string" },
"type": {},
"properties": { "type": "object" }
},
"additionalProperties": false
});
write!(temp_file, "{meta_schema_draft7}").expect("Failed to write to temp file");
let uri = crate::retriever::path_to_uri(temp_file.path());
let schema_using_draft7_meta = json!({
"$schema": uri,
"type": "object",
"properties": {
"name": { "type": "string" }
},
"unevaluatedProperties": false
});
let schema_valid_for_draft7_meta = json!({
"$schema": uri,
"type": "object",
"properties": {
"name": { "type": "string" }
}
});
assert!(crate::meta::is_valid(&meta_schema_draft7));
assert!(!crate::meta::is_valid(&schema_using_draft7_meta));
assert!(crate::meta::is_valid(&schema_valid_for_draft7_meta));
}
#[test]
#[cfg(all(not(target_arch = "wasm32"), feature = "resolve-file"))]
fn test_meta_schema_chain_resolution() {
use std::io::Write;
let mut intermediate_file =
tempfile::NamedTempFile::new().expect("Failed to create temp file");
let intermediate_meta = json!({
"$id": "http://example.com/meta/intermediate",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object"
});
write!(intermediate_file, "{intermediate_meta}").expect("Failed to write to temp file");
let intermediate_uri = crate::retriever::path_to_uri(intermediate_file.path());
let mut custom_file = tempfile::NamedTempFile::new().expect("Failed to create temp file");
let custom_meta = json!({
"$id": "http://example.com/meta/custom",
"$schema": intermediate_uri,
"type": "object"
});
write!(custom_file, "{custom_meta}").expect("Failed to write to temp file");
let custom_uri = crate::retriever::path_to_uri(custom_file.path());
let schema = json!({
"$schema": custom_uri,
"type": "string"
});
assert!(crate::meta::is_valid(&schema));
}
#[test]
#[cfg(all(not(target_arch = "wasm32"), feature = "resolve-file"))]
fn test_circular_meta_schema_reference() {
use std::io::Write;
let mut meta_a_file = tempfile::NamedTempFile::new().expect("Failed to create temp file");
let meta_a_uri = crate::retriever::path_to_uri(meta_a_file.path());
let mut meta_b_file = tempfile::NamedTempFile::new().expect("Failed to create temp file");
let meta_b_uri = crate::retriever::path_to_uri(meta_b_file.path());
let meta_a = json!({
"$id": "http://example.com/meta/a",
"$schema": &meta_b_uri,
"type": "object"
});
write!(meta_a_file, "{meta_a}").expect("Failed to write to temp file");
let meta_b = json!({
"$id": "http://example.com/meta/b",
"$schema": &meta_a_uri,
"type": "object"
});
write!(meta_b_file, "{meta_b}").expect("Failed to write to temp file");
let schema = json!({
"$schema": meta_a_uri.clone(),
"type": "string"
});
let result = crate::meta::options().validate(&schema);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Circular meta-schema reference"));
}
#[test]
fn simple_schema_with_unknown_draft() {
let meta_schema = json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "http://custom.example.com/schema",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/core": true,
"https://json-schema.org/draft/2020-12/vocab/applicator": true,
"https://json-schema.org/draft/2020-12/vocab/validation": true,
}
});
let schema = json!({
"$schema": "http://custom.example.com/schema",
"type": "object",
"properties": {
"name": { "type": "string" }
}
});
let registry = Registry::new()
.add("http://custom.example.com/schema", meta_schema)
.expect("Should accept meta-schema")
.prepare()
.expect("Should create registry");
let validator = crate::options()
.without_schema_validation()
.with_registry(®istry)
.build(&schema)
.expect("Should build validator");
assert!(validator.is_valid(&json!({"name": "test"})));
assert!(!validator.is_valid(&json!({"name": 123})));
assert!(!validator.is_valid(&json!("not an object")));
}
#[test]
fn custom_meta_schema_support() {
let meta_schema = json!({
"$id": "http://example.com/meta/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Core schema definition",
"type": "object",
"allOf": [
{
"$ref": "#/$defs/editable"
},
{
"$ref": "#/$defs/core"
}
],
"properties": {
"properties": {
"type": "object",
"patternProperties": {
".*": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"array",
"boolean",
"integer",
"number",
"object",
"string",
"null"
]
}
}
}
},
"propertyNames": {
"type": "string",
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
}
}
},
"unevaluatedProperties": false,
"required": [
"properties"
],
"$defs": {
"core": {
"type": "object",
"properties": {
"$id": {
"type": "string"
},
"$schema": {
"type": "string"
},
"type": {
"const": "object"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"additionalProperties": {
"type": "boolean",
"const": false
}
},
"required": [
"$id",
"$schema",
"type"
]
},
"editable": {
"type": "object",
"properties": {
"creationDate": {
"type": "string",
"format": "date-time"
},
"updateDate": {
"type": "string",
"format": "date-time"
}
},
"required": [
"creationDate"
]
}
}
});
let element_schema = json!({
"$schema": "http://example.com/meta/schema",
"$id": "http://example.com/schemas/element",
"title": "Element",
"description": "An element",
"creationDate": "2024-12-31T12:31:53+01:00",
"properties": {
"value": {
"type": "string"
}
},
"type": "object"
});
let registry = Registry::new()
.add("http://example.com/meta/schema", meta_schema)
.expect("Should accept meta-schema")
.prepare()
.expect("Should create registry");
let validator = crate::options()
.without_schema_validation()
.with_registry(®istry)
.build(&element_schema)
.expect("Should successfully build validator with custom meta-schema");
let valid_instance = json!({
"value": "test string"
});
assert!(validator.is_valid(&valid_instance));
let invalid_instance = json!({
"value": 123
});
assert!(!validator.is_valid(&invalid_instance));
}
#[test]
fn custom_meta_schema_with_fragment_finds_vocabularies() {
let custom_meta = json!({
"$id": "http://example.com/custom-with-unevaluated",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/core": true,
"https://json-schema.org/draft/2020-12/vocab/applicator": true,
"https://json-schema.org/draft/2020-12/vocab/validation": true,
"https://json-schema.org/draft/2020-12/vocab/unevaluated": true
}
});
let registry = Registry::new()
.add("http://example.com/custom-with-unevaluated", custom_meta)
.expect("Should accept meta-schema")
.prepare()
.expect("Should create registry");
let schema = json!({
"$id": "http://example.com/subject",
"$schema": "http://example.com/custom-with-unevaluated#",
"type": "object",
"properties": {
"foo": { "type": "string" }
},
"unevaluatedProperties": false
});
let validator = crate::options()
.without_schema_validation()
.with_registry(®istry)
.build(&schema)
.expect("Should build validator");
assert!(validator.is_valid(&json!({"foo": "bar"})));
assert!(!validator.is_valid(&json!({"foo": "bar", "extra": "value"})));
}
#[test]
fn strict_meta_schema_catches_typos() {
let strict_meta = json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/strict",
"$dynamicAnchor": "meta",
"$ref": "https://json-schema.org/draft/2020-12/schema",
"unevaluatedProperties": false
});
let registry = Registry::new()
.add("https://json-schema.org/draft/2020-12/strict", strict_meta)
.expect("Should accept strict meta-schema")
.prepare()
.expect("Should create registry");
let valid_schema = json!({
"$schema": "https://json-schema.org/draft/2020-12/strict",
"type": "object",
"properties": {
"name": {"type": "string", "minLength": 1}
}
});
assert!(crate::meta::options()
.with_registry(®istry)
.is_valid(&valid_schema));
let invalid_schema_top_level = json!({
"$schema": "https://json-schema.org/draft/2020-12/strict",
"typ": "string" });
assert!(!crate::meta::options()
.with_registry(®istry)
.is_valid(&invalid_schema_top_level));
let invalid_schema_nested = json!({
"$schema": "https://json-schema.org/draft/2020-12/strict",
"type": "object",
"properties": {
"name": {"type": "string", "minSize": 1} }
});
assert!(!crate::meta::options()
.with_registry(®istry)
.is_valid(&invalid_schema_nested));
}
#[test]
fn custom_meta_schema_preserves_underlying_draft_behavior() {
let custom_meta_draft7 = json!({
"$id": "http://example.com/meta/draft7-custom",
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"customKeyword": {"type": "string"}
}
});
let registry = Registry::new()
.add("http://example.com/meta/draft7-custom", custom_meta_draft7)
.expect("Should accept meta-schema")
.prepare()
.expect("Should create registry");
let schema = json!({
"$id": "http://example.com/subject",
"$schema": "http://example.com/meta/draft7-custom",
"$ref": "#/$defs/positiveNumber",
"maximum": 5,
"$defs": {
"positiveNumber": {
"type": "number",
"minimum": 0
}
}
});
let validator = crate::options()
.without_schema_validation()
.with_registry(®istry)
.build(&schema)
.expect("Should build validator");
assert!(validator.is_valid(&json!(10)));
}
mod meta_options_tests {
use super::*;
use crate::Registry;
#[test]
fn test_meta_options_with_registry_valid_schema() {
let custom_meta = json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"$schema": { "type": "string" },
"type": { "type": "string" },
"maxLength": { "type": "integer" }
},
"additionalProperties": false
});
let registry = Registry::new()
.add("http://example.com/meta", custom_meta)
.unwrap()
.prepare()
.unwrap();
let schema = json!({
"$schema": "http://example.com/meta",
"type": "string",
"maxLength": 10
});
assert!(crate::meta::options()
.with_registry(®istry)
.is_valid(&schema));
assert!(crate::meta::options()
.with_registry(®istry)
.validate(&schema)
.is_ok());
}
#[test]
fn test_meta_options_with_registry_invalid_schema() {
let custom_meta = json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"type": { "type": "string" }
},
"additionalProperties": false
});
let registry = Registry::new()
.add("http://example.com/meta", custom_meta)
.unwrap()
.prepare()
.unwrap();
let schema = json!({
"$schema": "http://example.com/meta",
"type": "string",
"maxLength": 10 });
assert!(!crate::meta::options()
.with_registry(®istry)
.is_valid(&schema));
assert!(crate::meta::options()
.with_registry(®istry)
.validate(&schema)
.is_err());
}
#[test]
fn test_meta_options_with_registry_chain() {
let custom_meta = json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object"
});
let registry = Registry::new()
.add("http://example.com/custom", custom_meta)
.unwrap()
.prepare()
.unwrap();
let schema = json!({
"$schema": "http://example.com/custom",
"type": "string"
});
assert!(crate::meta::options()
.with_registry(®istry)
.is_valid(&schema));
}
#[test]
fn test_meta_options_with_registry_multi_level_chain() {
let meta_level_1 = json!({
"$id": "http://example.com/meta/level1",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"customProp": { "type": "boolean" }
}
});
let meta_level_2 = json!({
"$id": "http://example.com/meta/level2",
"$schema": "http://example.com/meta/level1",
"type": "object",
"customProp": true
});
let registry = Registry::new()
.extend([
("http://example.com/meta/level1", meta_level_1),
("http://example.com/meta/level2", meta_level_2),
])
.unwrap()
.prepare()
.unwrap();
let schema = json!({
"$schema": "http://example.com/meta/level2",
"type": "string",
"customProp": true
});
assert!(crate::meta::options()
.with_registry(®istry)
.is_valid(&schema));
}
#[test]
fn test_meta_options_with_registry_multi_document_meta_schema() {
let shared_constraints = json!({
"$id": "http://example.com/meta/shared",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"maxLength": { "type": "integer", "minimum": 0 }
}
});
let root_meta = json!({
"$id": "http://example.com/meta/root",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"$schema": { "type": "string" },
"type": { "type": "string" }
},
"allOf": [
{ "$ref": "http://example.com/meta/shared" }
]
});
let registry = Registry::new()
.extend([
("http://example.com/meta/root", root_meta),
("http://example.com/meta/shared", shared_constraints),
])
.unwrap()
.prepare()
.unwrap();
let schema = json!({
"$schema": "http://example.com/meta/root",
"type": "string",
"maxLength": 5
});
let result = crate::meta::options()
.with_registry(®istry)
.validate(&schema);
assert!(
result.is_ok(),
"meta validation failed even though registry contains all meta-schemas: {}",
result.unwrap_err()
);
assert!(crate::meta::options()
.with_registry(®istry)
.is_valid(&schema));
}
#[test]
fn test_meta_options_without_registry_unknown_metaschema() {
let schema = json!({
"$schema": "http://0.0.0.0/nonexistent",
"type": "string"
});
let result = crate::meta::options().validate(&schema);
assert!(result.is_err());
}
#[test]
#[should_panic(expected = "Failed to resolve meta-schema")]
fn test_meta_options_is_valid_panics_on_missing_metaschema() {
let schema = json!({
"$schema": "http://0.0.0.0/nonexistent",
"type": "string"
});
let _ = crate::meta::options().is_valid(&schema);
}
#[test]
fn test_meta_options_with_registry_missing_metaschema() {
let custom_meta = json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object"
});
let registry = Registry::new()
.add("http://example.com/meta1", custom_meta)
.unwrap()
.prepare()
.unwrap();
let schema = json!({
"$schema": "http://example.com/meta2",
"type": "string"
});
let result = crate::meta::options()
.with_registry(®istry)
.validate(&schema);
assert!(result.is_err());
}
#[test]
fn test_meta_options_circular_reference_detection() {
let meta1 = json!({
"$id": "http://example.com/meta1",
"$schema": "http://example.com/meta2",
"type": "object"
});
let meta2 = json!({
"$id": "http://example.com/meta2",
"$schema": "http://example.com/meta1",
"type": "object"
});
let registry = Registry::new()
.extend([
("http://example.com/meta1", meta1),
("http://example.com/meta2", meta2),
])
.unwrap()
.prepare()
.unwrap();
let schema = json!({
"$schema": "http://example.com/meta1",
"type": "string"
});
let result = crate::meta::options()
.with_registry(®istry)
.validate(&schema);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("Circular"));
}
#[test]
fn test_meta_options_standard_drafts_without_registry() {
let schemas = vec![
json!({ "$schema": "http://json-schema.org/draft-04/schema#", "type": "string" }),
json!({ "$schema": "http://json-schema.org/draft-06/schema#", "type": "string" }),
json!({ "$schema": "http://json-schema.org/draft-07/schema#", "type": "string" }),
json!({ "$schema": "https://json-schema.org/draft/2019-09/schema", "type": "string" }),
json!({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "string" }),
];
for schema in schemas {
assert!(
crate::meta::options().is_valid(&schema),
"Failed for schema: {schema}"
);
}
}
#[test]
fn test_meta_options_validate_returns_specific_errors() {
let custom_meta = json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["type"]
});
let registry = Registry::new()
.add("http://example.com/meta", custom_meta)
.unwrap()
.prepare()
.unwrap();
let schema = json!({
"$schema": "http://example.com/meta",
"properties": {
"name": { "type": "string" }
}
});
let result = crate::meta::options()
.with_registry(®istry)
.validate(&schema);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("required") || err.to_string().contains("type"));
}
#[test]
fn test_meta_options_builds_validator_with_resolved_draft() {
let custom_meta = json!({
"$id": "http://example.com/meta/draft7-based",
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"$schema": { "type": "string" },
"type": { "type": "string" },
"minLength": { "type": "integer" }
},
"additionalProperties": false
});
let registry = Registry::new()
.add("http://example.com/meta/draft7-based", custom_meta)
.unwrap()
.prepare()
.unwrap();
let schema = json!({
"$schema": "http://example.com/meta/draft7-based",
"type": "string",
"minLength": 5
});
let result = crate::meta::options()
.with_registry(®istry)
.validate(&schema);
assert!(result.is_ok());
}
#[test]
fn test_meta_options_validator_uses_correct_draft() {
let custom_meta_draft6 = json!({
"$id": "http://example.com/meta/draft6-based",
"$schema": "http://json-schema.org/draft-06/schema#",
"type": "object",
"properties": {
"$schema": { "type": "string" },
"type": { "type": "string" },
"exclusiveMinimum": { "type": "number" }
},
"additionalProperties": false
});
let registry = Registry::new()
.add("http://example.com/meta/draft6-based", custom_meta_draft6)
.unwrap()
.prepare()
.unwrap();
let schema_valid_for_draft6 = json!({
"$schema": "http://example.com/meta/draft6-based",
"type": "number",
"exclusiveMinimum": 0
});
let result = crate::meta::options()
.with_registry(®istry)
.validate(&schema_valid_for_draft6);
assert!(result.is_ok());
}
#[test]
fn test_meta_options_without_schema_validation_in_built_validator() {
let custom_meta = json!({
"$id": "http://example.com/meta/custom",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"$schema": { "type": "string" },
"type": { "type": "string" }
},
"additionalProperties": false
});
let registry = Registry::new()
.add("http://example.com/meta/custom", custom_meta)
.unwrap()
.prepare()
.unwrap();
let schema = json!({
"$schema": "http://example.com/meta/custom",
"type": "string"
});
let result = crate::meta::options()
.with_registry(®istry)
.validate(&schema);
assert!(result.is_ok());
}
#[test]
fn test_meta_validation_uses_resolved_draft_from_chain() {
let custom_meta = json!({
"$id": "http://example.com/meta/draft4-based",
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {
"$schema": { "type": "string" },
"type": { "type": "string" },
"enum": { "type": "array" },
"const": { "type": "string" }
},
"additionalProperties": false
});
let registry = Registry::new()
.add("http://example.com/meta/draft4-based", custom_meta)
.unwrap()
.prepare()
.unwrap();
let schema = json!({
"$schema": "http://example.com/meta/draft4-based",
"type": "string",
"const": "foo"
});
let result = crate::meta::options()
.with_registry(®istry)
.validate(&schema);
assert!(result.is_ok());
}
#[test]
fn test_meta_validation_multi_level_chain_uses_resolved_draft() {
let meta_level_1 = json!({
"$id": "http://example.com/meta/level1",
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {
"customKeyword": { "type": "boolean" }
}
});
let meta_level_2 = json!({
"$id": "http://example.com/meta/level2",
"$schema": "http://example.com/meta/level1",
"type": "object",
"properties": {
"$schema": { "type": "string" },
"type": { "type": "string" },
"minimum": { "type": "number" },
"exclusiveMinimum": { "type": "boolean" }
},
"customKeyword": true,
"additionalProperties": false
});
let registry = Registry::new()
.extend([
("http://example.com/meta/level1", meta_level_1),
("http://example.com/meta/level2", meta_level_2),
])
.unwrap()
.prepare()
.unwrap();
let schema = json!({
"$schema": "http://example.com/meta/level2",
"type": "number",
"minimum": 5,
"exclusiveMinimum": true
});
let result = crate::meta::options()
.with_registry(®istry)
.validate(&schema);
assert!(result.is_ok());
}
}
#[test]
fn test_meta_validator_for_valid_schema() {
let schema = json!({
"type": "string",
"maxLength": 5
});
let validator = crate::meta::validator_for(&schema).expect("Valid meta-schema");
assert!(validator.is_valid(&schema));
}
#[test]
fn test_meta_validator_for_invalid_schema() {
let schema = json!({
"type": "invalid_type"
});
let validator = crate::meta::validator_for(&schema).expect("Valid meta-schema");
assert!(!validator.is_valid(&schema));
}
#[test]
fn test_meta_validator_for_evaluate_api() {
let schema = json!({
"type": "string",
"maxLength": 5
});
let validator = crate::meta::validator_for(&schema).expect("Valid meta-schema");
let evaluation = validator.evaluate(&schema);
let flag = evaluation.flag();
assert!(flag.valid);
}
#[test]
fn test_meta_validator_for_evaluate_api_invalid() {
let schema = json!({
"type": "invalid_type",
"minimum": "not a number"
});
let validator = crate::meta::validator_for(&schema).expect("Valid meta-schema");
let evaluation = validator.evaluate(&schema);
let flag = evaluation.flag();
assert!(!flag.valid);
}
#[test]
fn test_meta_validator_for_all_drafts() {
let schemas = vec![
json!({ "$schema": "http://json-schema.org/draft-04/schema#", "type": "string" }),
json!({ "$schema": "http://json-schema.org/draft-06/schema#", "type": "string" }),
json!({ "$schema": "http://json-schema.org/draft-07/schema#", "type": "string" }),
json!({ "$schema": "https://json-schema.org/draft/2019-09/schema", "type": "string" }),
json!({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "string" }),
];
for schema in schemas {
let validator = crate::meta::validator_for(&schema).unwrap();
assert!(validator.is_valid(&schema));
}
}
#[test]
fn test_meta_validator_for_iter_errors() {
let schema = json!({
"type": "invalid_type",
"minimum": "not a number"
});
let validator = crate::meta::validator_for(&schema).expect("Valid meta-schema");
let errors: Vec<_> = validator.iter_errors(&schema).collect();
assert!(!errors.is_empty());
}
}
#[cfg(all(test, feature = "resolve-async", not(target_family = "wasm")))]
mod async_tests {
use std::{collections::HashMap, sync::Arc};
use serde_json::json;
use crate::{AsyncRetrieve, Draft, Uri};
#[derive(Clone)]
struct TestRetriever {
schemas: HashMap<String, serde_json::Value>,
}
impl TestRetriever {
fn new() -> Self {
let mut schemas = HashMap::new();
schemas.insert(
"https://example.com/user.json".to_string(),
json!({
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer", "minimum": 0}
},
"required": ["name"]
}),
);
Self { schemas }
}
}
#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
impl AsyncRetrieve for TestRetriever {
async fn retrieve(
&self,
uri: &Uri<String>,
) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
self.schemas
.get(uri.as_str())
.cloned()
.ok_or_else(|| "Schema not found".into())
}
}
#[tokio::test]
async fn test_async_validator_for() {
let schema = json!({
"$ref": "https://example.com/user.json"
});
let validator = crate::async_options()
.with_retriever(TestRetriever::new())
.build(&schema)
.await
.unwrap();
assert!(validator.is_valid(&json!({
"name": "John Doe",
"age": 30
})));
assert!(!validator.is_valid(&json!({
"age": -5
})));
assert!(!validator.is_valid(&json!({
"name": 123,
"age": 30
})));
}
#[tokio::test]
async fn test_async_options_with_draft() {
let schema = json!({
"$ref": "https://example.com/user.json"
});
let validator = crate::async_options()
.with_draft(Draft::Draft202012)
.with_retriever(TestRetriever::new())
.build(&schema)
.await
.unwrap();
assert!(validator.is_valid(&json!({
"name": "John Doe",
"age": 30
})));
}
#[tokio::test]
async fn test_async_retrieval_failure() {
let schema = json!({
"$ref": "https://example.com/nonexistent.json"
});
let result = crate::async_options()
.with_retriever(TestRetriever::new())
.build(&schema)
.await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Schema not found"));
}
#[tokio::test]
async fn test_async_nested_references() {
let mut retriever = TestRetriever::new();
retriever.schemas.insert(
"https://example.com/nested.json".to_string(),
json!({
"type": "object",
"properties": {
"user": { "$ref": "https://example.com/user.json" }
}
}),
);
let schema = json!({
"$ref": "https://example.com/nested.json"
});
let validator = crate::async_options()
.with_retriever(retriever)
.build(&schema)
.await
.unwrap();
assert!(validator.is_valid(&json!({
"user": {
"name": "John Doe",
"age": 30
}
})));
assert!(!validator.is_valid(&json!({
"user": {
"age": -5
}
})));
}
#[tokio::test]
async fn test_async_with_registry_uses_async_retriever_for_inline_only_refs() {
let registry = crate::Registry::new().prepare().unwrap();
let schema = json!({
"$ref": "https://example.com/user.json"
});
let validator = crate::async_options()
.with_registry(®istry)
.with_retriever(TestRetriever::new())
.build(&schema)
.await
.unwrap();
assert!(validator.is_valid(&json!({
"name": "John Doe",
"age": 30
})));
assert!(!validator.is_valid(&json!({
"age": -5
})));
}
#[tokio::test]
async fn test_async_validator_for_basic() {
let schema = json!({"type": "integer"});
let validator = crate::async_validator_for(&schema).await.unwrap();
assert!(validator.is_valid(&json!(42)));
assert!(!validator.is_valid(&json!("abc")));
}
#[tokio::test]
async fn test_async_build_future_is_send() {
let schema = Arc::new(json!({
"$ref": "https://example.com/user.json"
}));
let retriever = TestRetriever::new();
let handle = tokio::spawn({
let schema = Arc::clone(&schema);
let retriever = retriever.clone();
async move {
crate::async_options()
.with_retriever(retriever)
.build(&schema)
.await
}
});
let validator = handle.await.unwrap().unwrap();
assert!(validator.is_valid(&json!({
"name": "John Doe",
"age": 30
})));
}
}