#![allow(clippy::unnecessary_wraps)]
#![cfg_attr(not(feature = "macros"), allow(unreachable_pub))]
#[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;
pub mod error;
mod evaluation;
pub(crate) use jsonschema_value::{
cmp, numeric, unique, Array, Json, LazyInstance, Node, NodeIdentity, Object, SerdeJson,
};
pub mod json {
#[cfg(feature = "conformance")]
pub use jsonschema_value::conformance;
pub use jsonschema_value::{
cmp, unique, Array, Json, JsonNumber, Node, NodeIdentity, Object, SerdeJson,
};
#[cfg(feature = "pyo3")]
pub use jsonschema_value::{
inspect, narrow_array, narrow_object, object_values, stock_enum_value, Pyo3,
};
#[cfg(feature = "jsonb")]
pub use jsonschema_value::{jsonb, Jsonb, JsonbNode};
#[cfg(feature = "magnus")]
pub use jsonschema_value::{
magnus_child, magnus_invalidate_members_cache, magnus_is_object, magnus_object_values,
magnus_probe_root, magnus_string_node, magnus_take_pending_error, Magnus,
MagnusPendingErrorScope, PendingError, RbNode,
};
}
mod http;
mod keywords;
#[cfg(all(feature = "macros", not(target_family = "wasm")))]
mod meta_codegen;
mod node;
mod options;
pub mod output;
pub mod paths;
pub(crate) mod properties;
pub(crate) mod regex;
mod retriever;
pub mod types {
pub use jsonschema_value::types::{JsonType, JsonTypeSet, JsonTypeSetIterator};
}
mod validator;
pub use canonical::CanonicalizationError;
pub use error::{
ErrorIterator, MaskedValidationError, ValidationError, ValidationErrorParts, ValidationErrors,
};
pub use evaluation::{
AnnotationEntry, ErrorEntry, Evaluation, FlagOutput, HierarchicalOutput, ListOutput,
};
pub use http::HttpOptions;
#[doc(inline)]
#[cfg(feature = "macros")]
pub use jsonschema_macros::validator;
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
}
pub fn canonicalize(value: &Value) -> Result<canonical::CanonicalSchema, CanonicalizationError> {
canonical::options().canonicalize(value)
}
#[must_use]
pub fn options<'i>() -> ValidationOptions<'i> {
Validator::options()
}
#[must_use]
pub fn options_for<'i, F: Json>() -> ValidationOptions<'i, std::sync::Arc<dyn Retrieve>, F> {
ValidationOptions::default()
}
#[cfg(feature = "resolve-async")]
#[must_use]
pub fn async_options_for<'i, F: Json>(
) -> ValidationOptions<'i, std::sync::Arc<dyn AsyncRetrieve>, F> {
ValidationOptions::default()
}
#[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, Json, Node, Object, Registry, Validator};
use ahash::AHashSet;
use referencing::Retrieve;
use serde_json::Value;
use std::{
any::{Any, TypeId},
borrow::Cow,
sync::{OnceLock, RwLock},
};
pub use validator_handle::MetaValidator;
#[cfg(all(feature = "macros", feature = "pyo3", not(target_family = "wasm")))]
pub mod pyo3 {
use crate::Draft;
use ::pyo3::{types::PyAny, Borrowed};
pub use crate::meta_codegen::pyo3::{is_valid_fn, validate_fn, IsValidFn, ValidateFn};
#[must_use]
pub fn draft_of(schema: Borrowed<'_, '_, PyAny>) -> Draft {
super::meta_cache::<crate::json::Pyo3>().draft_of(&schema)
}
}
#[cfg(all(feature = "macros", feature = "magnus", not(target_family = "wasm")))]
pub mod magnus {
use crate::{json::RbNode, Draft};
pub use crate::meta_codegen::magnus::{is_valid_fn, validate_fn, IsValidFn, ValidateFn};
#[must_use]
pub fn draft_of(schema: RbNode<'_>) -> Draft {
super::meta_cache::<crate::json::Magnus>().draft_of(&schema)
}
}
#[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.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.validate(schema)
}
}
mod validator_handle {
use crate::{ValidationError, Validator};
use serde_json::Value;
use std::{marker::PhantomData, ops::Deref};
pub struct MetaValidator<'a>(MetaValidatorInner<'a>);
enum MetaValidatorInner<'a> {
#[cfg(all(not(feature = "macros"), not(target_family = "wasm")))]
Borrowed(&'a Validator),
Owned(Box<Validator>, PhantomData<&'a Validator>),
#[cfg(all(feature = "macros", not(target_family = "wasm")))]
Generated {
draft: crate::Draft,
is_valid: fn(&Value) -> bool,
validate: crate::meta_codegen::ValidateFn,
iter_errors: crate::meta_codegen::IterErrorsFn,
},
}
#[cfg_attr(
any(target_family = "wasm", feature = "macros"),
allow(clippy::elidable_lifetime_names)
)]
impl<'a> MetaValidator<'a> {
#[cfg(all(not(feature = "macros"), 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))
}
#[cfg(all(feature = "macros", not(target_family = "wasm")))]
pub(crate) fn generated(draft: crate::Draft) -> Self {
Self(MetaValidatorInner::Generated {
draft,
is_valid: crate::meta_codegen::is_valid_fn(draft),
validate: crate::meta_codegen::validate_fn(draft),
iter_errors: crate::meta_codegen::iter_errors_fn(draft),
})
}
#[must_use]
pub fn is_valid(&self, schema: &Value) -> bool {
match &self.0 {
#[cfg(all(feature = "macros", not(target_family = "wasm")))]
MetaValidatorInner::Generated { is_valid, .. } => is_valid(schema),
_ => self.as_ref().is_valid(schema),
}
}
pub fn validate<'i>(&self, schema: &'i Value) -> Result<(), ValidationError<'i>> {
match &self.0 {
#[cfg(all(feature = "macros", not(target_family = "wasm")))]
MetaValidatorInner::Generated { validate, .. } => validate(schema),
_ => self.as_ref().validate(schema),
}
}
#[must_use]
pub fn iter_errors<'i>(&'i self, schema: &'i Value) -> crate::ErrorIterator<'i> {
match &self.0 {
#[cfg(all(feature = "macros", not(target_family = "wasm")))]
MetaValidatorInner::Generated { iter_errors, .. } => iter_errors(schema),
_ => self.as_ref().iter_errors(schema),
}
}
}
impl AsRef<Validator> for MetaValidator<'_> {
fn as_ref(&self) -> &Validator {
match &self.0 {
#[cfg(all(not(feature = "macros"), not(target_family = "wasm")))]
MetaValidatorInner::Borrowed(validator) => validator,
MetaValidatorInner::Owned(validator, _) => validator,
#[cfg(all(feature = "macros", not(target_family = "wasm")))]
MetaValidatorInner::Generated { draft, .. } => {
crate::meta::validators::runtime_validator_for_draft(*draft)
}
}
}
}
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)
}
#[cfg(all(feature = "macros", not(target_family = "wasm")))]
pub(crate) fn runtime_validator_for_draft(draft: crate::Draft) -> &'static Validator {
use crate::Draft;
match draft {
Draft::Draft4 => &DRAFT4_META_VALIDATOR,
Draft::Draft6 => &DRAFT6_META_VALIDATOR,
Draft::Draft7 => &DRAFT7_META_VALIDATOR,
Draft::Draft201909 => &DRAFT201909_META_VALIDATOR,
_ => &DRAFT202012_META_VALIDATOR,
}
}
}
pub(crate) fn validator_for_draft(draft: Draft) -> MetaValidator<'static> {
#[cfg(all(feature = "macros", not(target_family = "wasm")))]
{
MetaValidator::generated(draft)
}
#[cfg(all(not(feature = "macros"), 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.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.validate(schema)
}
pub fn validator_for(
schema: &Value,
) -> Result<MetaValidator<'static>, ValidationError<'static>> {
try_meta_validator_for(schema, None)
}
pub fn validate_for<F: Json>(schema: F::Node<'_>) -> Result<(), ValidationError<'_>> {
let cache = meta_cache::<F>();
match cache.draft_of(&schema) {
Draft::Unknown => custom_meta_validator(cache, &schema)?.validate(schema),
draft => cache.validator(draft).validate(schema),
}
}
pub fn is_valid_for<F: Json>(schema: F::Node<'_>) -> Result<bool, ValidationError<'static>> {
let cache = meta_cache::<F>();
Ok(match cache.draft_of(&schema) {
Draft::Unknown => custom_meta_validator(cache, &schema)?.is_valid(schema),
draft => cache.validator(draft).is_valid(schema),
})
}
fn custom_meta_validator<F: Json>(
cache: &MetaCache<F>,
schema: &F::Node<'_>,
) -> Result<Validator<F>, ValidationError<'static>> {
let uri = cache
.meta_schema_uri(schema)
.expect("`$schema` must exist when draft is Unknown")
.into_owned();
let (custom_meta_schema, resolved_draft) = resolve_meta_schema_chain(&uri)?;
crate::options_for::<F>()
.with_draft(resolved_draft)
.without_schema_validation()
.build(&custom_meta_schema)
}
struct MetaCache<F: Json> {
schema_key: F::PreparedKey,
validators: [OnceLock<Validator<F>>; 5],
}
impl<F: Json> MetaCache<F> {
fn meta_schema_uri<'a>(&self, schema: &F::Node<'a>) -> Option<Cow<'a, str>> {
schema.as_object()?.get(&self.schema_key)?.as_string()
}
fn draft_of(&self, schema: &F::Node<'_>) -> Draft {
self.meta_schema_uri(schema)
.map_or_else(Draft::default, |uri| Draft::from_schema_uri(&uri))
}
fn validator(&self, draft: Draft) -> &Validator<F> {
let (index, meta_schema) = match draft {
Draft::Draft4 => (0, &referencing::meta::DRAFT4),
Draft::Draft6 => (1, &referencing::meta::DRAFT6),
Draft::Draft7 => (2, &referencing::meta::DRAFT7),
Draft::Draft201909 => (3, &referencing::meta::DRAFT201909),
_ => (4, &referencing::meta::DRAFT202012),
};
self.validators[index].get_or_init(|| {
crate::options_for::<F>()
.without_schema_validation()
.build(meta_schema)
.expect("Meta-schema should be valid")
})
}
}
fn meta_cache<F: Json>() -> &'static MetaCache<F> {
static CACHE: RwLock<Vec<(TypeId, &'static (dyn Any + Send + Sync))>> =
RwLock::new(Vec::new());
fn find<F: Json>(
entries: &[(TypeId, &'static (dyn Any + Send + Sync))],
) -> Option<&'static MetaCache<F>> {
let type_id = TypeId::of::<F>();
entries.iter().find(|(id, _)| *id == type_id).map(|(_, e)| {
e.downcast_ref()
.expect("Entries are found by representation type")
})
}
if let Some(cache) = find::<F>(&CACHE.read().expect("Meta-validator cache is poisoned")) {
return cache;
}
let mut entries = CACHE.write().expect("Meta-validator cache is poisoned");
if let Some(cache) = find::<F>(&entries) {
return cache;
}
let cache: &'static MetaCache<F> = Box::leak(Box::new(MetaCache {
schema_key: F::prepare_key("$schema"),
validators: Default::default(),
}));
entries.push((TypeId::of::<F>(), cache));
cache
}
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().is_valid(schema)
}
#[inline]
pub fn validate(schema: &Value) -> Result<(), ValidationError<'_>> {
validator().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().is_valid(schema)
}
#[inline]
pub fn validate(schema: &Value) -> Result<(), ValidationError<'_>> {
validator().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().is_valid(schema)
}
#[inline]
pub fn validate(schema: &Value) -> Result<(), ValidationError<'_>> {
validator().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().is_valid(schema)
}
#[inline]
pub fn validate(schema: &Value) -> Result<(), ValidationError<'_>> {
validator().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().is_valid(schema)
}
#[inline]
pub fn validate(schema: &Value) -> Result<(), ValidationError<'_>> {
validator().validate(schema)
}
}
}
#[cfg(feature = "macros")]
#[doc(hidden)]
pub mod __private {
#[cfg(feature = "pyo3")]
pub mod pyo3 {
pub use pyo3::{intern, types::PyString, Borrowed, Bound, PyAny, PyResult};
}
#[cfg(feature = "pyo3")]
#[doc(hidden)]
#[macro_export]
macro_rules! __pyo3_backend {
($($generated:tt)*) => { $($generated)* };
}
#[cfg(not(feature = "pyo3"))]
#[doc(hidden)]
#[macro_export]
macro_rules! __pyo3_backend {
($($generated:tt)*) => {
::core::compile_error!("`backend = Pyo3` needs the `pyo3` feature of `jsonschema`");
};
}
pub use crate::__pyo3_backend as pyo3_backend;
#[cfg(feature = "magnus")]
pub mod magnus {
pub use ::magnus::{rb_sys::AsRawValue, Error, Value};
}
#[cfg(feature = "magnus")]
#[doc(hidden)]
#[macro_export]
macro_rules! __magnus_backend {
($($generated:tt)*) => { $($generated)* };
}
#[cfg(not(feature = "magnus"))]
#[doc(hidden)]
#[macro_export]
macro_rules! __magnus_backend {
($($generated:tt)*) => {
::core::compile_error!("`backend = Magnus` needs the `magnus` feature of `jsonschema`");
};
}
pub use crate::__magnus_backend as magnus_backend;
pub use ::serde_json;
pub mod fancy_regex {
pub use fancy_regex::{Regex, RegexBuilder};
}
pub mod regex {
pub use jsonschema_regex::contains_ecma_whitespace;
pub use regex::{Regex, RegexBuilder};
}
pub mod unique_items {
pub use crate::unique::is_unique;
}
pub mod cmp {
pub use crate::cmp::{equal, equal_numbers};
}
pub mod custom {
use crate::{
json::{Json, Node},
paths::Location,
};
#[must_use]
pub fn location(pointer: &str) -> Location {
Location::from_escaped(pointer)
}
pub fn validate<'i, F: Json>(
keyword: &dyn crate::Keyword<'i, F>,
instance: &F::Node<'i>,
instance_path: Location,
schema_path: &str,
keyword_name: &str,
) -> Option<crate::ValidationError<'i>> {
match keyword.validate(instance.clone()) {
Ok(()) => None,
Err(error) => Some(error.with_generated_context(
instance.to_value(),
instance_path,
Location::from_escaped(schema_path),
keyword_name,
)),
}
}
pub fn collect_errors<'i, F: Json>(
keyword: &dyn crate::Keyword<'i, F>,
instance: &F::Node<'i>,
instance_path: &Location,
schema_path: &str,
keyword_name: &str,
errors: &mut Vec<crate::ValidationError<'i>>,
) {
let mut found = keyword.iter_errors(instance.clone()).peekable();
if found.peek().is_none() {
return;
}
let value = instance.to_value();
for error in found {
errors.push(error.with_generated_context(
value.clone(),
instance_path.clone(),
Location::from_escaped(schema_path),
keyword_name,
));
}
}
}
pub mod types {
#[must_use]
pub fn is_integer(n: &serde_json::Number) -> bool {
jsonschema_value::types::number_is_integer(n)
}
#[must_use]
pub fn is_integer_draft4(n: &serde_json::Number) -> bool {
crate::keywords::legacy::type_draft_4::is_integer(n)
}
}
pub mod format {
#[cfg(feature = "idna")]
pub use crate::keywords::format::is_valid_idn_hostname;
pub use crate::keywords::format::{
is_valid_date, is_valid_datetime, is_valid_duration, is_valid_hostname,
is_valid_hostname_rfc1034, is_valid_ipv4, is_valid_ipv6, is_valid_iri,
is_valid_iri_reference, is_valid_json_pointer, is_valid_regex,
is_valid_relative_json_pointer, is_valid_time, is_valid_uri, is_valid_uri_reference,
is_valid_uri_template, is_valid_uuid,
};
#[cfg(not(feature = "idna"))]
#[must_use]
pub fn is_valid_idn_hostname(_: &str) -> bool {
true
}
pub type Cache = std::collections::HashMap<Box<str>, bool, ahash::RandomState>;
#[must_use]
pub fn is_valid_email_with_options(
value: &str,
options: Option<&crate::EmailOptions>,
) -> bool {
crate::keywords::format::is_valid_email(value, options.map(|opts| &opts.inner))
}
#[cfg(feature = "idna")]
#[must_use]
pub fn is_valid_idn_email_with_options(
value: &str,
options: Option<&crate::EmailOptions>,
) -> bool {
crate::keywords::format::is_valid_idn_email(value, options.map(|opts| &opts.inner))
}
#[cfg(not(feature = "idna"))]
#[must_use]
pub fn is_valid_idn_email_with_options(_: &str, _: Option<&crate::EmailOptions>) -> bool {
true
}
}
pub mod content {
pub use crate::{
content_encoding::{
from_base16, from_base32, from_base32hex, from_base64, from_base64url, is_base16,
is_base32, is_base32hex, is_base64, is_base64url,
},
content_media_type::is_json,
};
}
pub mod numeric {
pub fn ge<T>(value: &serde_json::Number, limit: T) -> bool
where
T: Copy + num_traits::ToPrimitive,
u64: num_cmp::NumCmp<T>,
i64: num_cmp::NumCmp<T>,
f64: num_cmp::NumCmp<T>,
{
crate::numeric::ge(value, limit)
}
pub fn le<T>(value: &serde_json::Number, limit: T) -> bool
where
T: Copy + num_traits::ToPrimitive,
u64: num_cmp::NumCmp<T>,
i64: num_cmp::NumCmp<T>,
f64: num_cmp::NumCmp<T>,
{
crate::numeric::le(value, limit)
}
pub fn eq<T>(value: &serde_json::Number, limit: T) -> bool
where
T: Copy + num_traits::ToPrimitive,
u64: num_cmp::NumCmp<T>,
i64: num_cmp::NumCmp<T>,
f64: num_cmp::NumCmp<T>,
{
crate::numeric::eq(value, limit)
}
pub fn gt<T>(value: &serde_json::Number, limit: T) -> bool
where
T: Copy + num_traits::ToPrimitive,
u64: num_cmp::NumCmp<T>,
i64: num_cmp::NumCmp<T>,
f64: num_cmp::NumCmp<T>,
{
crate::numeric::gt(value, limit)
}
pub fn lt<T>(value: &serde_json::Number, limit: T) -> bool
where
T: Copy + num_traits::ToPrimitive,
u64: num_cmp::NumCmp<T>,
i64: num_cmp::NumCmp<T>,
f64: num_cmp::NumCmp<T>,
{
crate::numeric::lt(value, limit)
}
#[must_use]
pub fn is_multiple_of_integer(value: &serde_json::Number, multiple: f64) -> bool {
crate::numeric::is_multiple_of_integer(value, multiple)
}
#[must_use]
pub fn is_multiple_of_float(value: &serde_json::Number, multiple: f64) -> bool {
crate::numeric::is_multiple_of_float(value, multiple)
}
#[cfg(feature = "arbitrary-precision")]
#[must_use]
pub fn check_compiled_bound(
value: &serde_json::Number,
op: u8,
limit_literal: &'static str,
) -> bool {
use jsonschema_value::numeric_check::{
check_bound, compile_bound, BoundOp, CompiledBound,
};
#[inline]
fn literal_key(literal: &'static str) -> (usize, usize) {
(literal.as_ptr() as usize, literal.len())
}
type BoundCache =
std::cell::RefCell<ahash::AHashMap<((usize, usize), u8), CompiledBound>>;
std::thread_local! {
static CACHE: BoundCache = std::cell::RefCell::new(ahash::AHashMap::default());
}
let op_tag = op;
let Some(op) = BoundOp::from_u8(op_tag) else {
unreachable!("codegen emits only bound op tags 0..=3")
};
let key = (literal_key(limit_literal), op_tag);
CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
let compiled = cache.entry(key).or_insert_with(|| {
let limit = serde_json::from_str::<serde_json::Number>(limit_literal)
.expect("Codegen emitted an invalid numeric literal");
compile_bound(op, &limit)
});
check_bound(compiled, value)
})
}
#[cfg(feature = "arbitrary-precision")]
#[must_use]
pub fn check_compiled_multiple_of(
value: &serde_json::Number,
limit_literal: &'static str,
) -> bool {
use jsonschema_value::numeric_check::{
check_multiple_of, compile_multiple_of, CompiledMultipleOf,
};
#[inline]
fn literal_key(literal: &'static str) -> (usize, usize) {
(literal.as_ptr() as usize, literal.len())
}
std::thread_local! {
static CACHE: std::cell::RefCell<ahash::AHashMap<(usize, usize), CompiledMultipleOf>> =
std::cell::RefCell::new(ahash::AHashMap::default());
}
let key = literal_key(limit_literal);
CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
let compiled = cache.entry(key).or_insert_with(|| {
let limit = serde_json::from_str::<serde_json::Number>(limit_literal)
.expect("Codegen emitted an invalid numeric literal");
compile_multiple_of(&limit)
});
check_multiple_of(compiled, value)
})
}
}
pub mod error {
use serde_json::Value;
use crate::{
paths::Location,
types::{JsonType, JsonTypeSet},
validator::LazyEvaluationPath,
ErrorIterator, LazyInstance, ValidationError,
};
#[inline]
#[must_use]
pub fn iterator_from(errors: Vec<ValidationError<'_>>) -> ErrorIterator<'_> {
ErrorIterator::from_iterator(errors.into_iter())
}
#[inline]
pub fn content_encoding<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
encoding: &str,
) -> ValidationError<'i> {
ValidationError::content_encoding(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
encoding,
)
}
#[inline]
pub fn content_media_type<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
media_type: &str,
) -> ValidationError<'i> {
ValidationError::content_media_type(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
media_type,
)
}
#[inline]
pub fn min_length<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
limit: u64,
) -> ValidationError<'i> {
ValidationError::min_length(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
limit,
)
}
#[inline]
pub fn max_length<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
limit: u64,
) -> ValidationError<'i> {
ValidationError::max_length(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
limit,
)
}
#[inline]
pub fn pattern<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
pattern: &str,
) -> ValidationError<'i> {
ValidationError::pattern(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
pattern.to_string(),
)
}
#[inline]
pub fn format<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
format: &str,
) -> ValidationError<'i> {
ValidationError::format(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
format,
)
}
#[inline]
pub fn single_type<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
ty: JsonType,
) -> ValidationError<'i> {
ValidationError::single_type_error(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
ty,
)
}
#[inline]
pub fn multiple_types<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
types: JsonTypeSet,
) -> ValidationError<'i> {
ValidationError::multiple_type_error(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
types,
)
}
#[inline]
pub fn minimum<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
limit: Value,
) -> ValidationError<'i> {
ValidationError::minimum(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
limit,
)
}
#[inline]
pub fn maximum<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
limit: Value,
) -> ValidationError<'i> {
ValidationError::maximum(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
limit,
)
}
#[inline]
pub fn exclusive_minimum<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
limit: Value,
) -> ValidationError<'i> {
ValidationError::exclusive_minimum(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
limit,
)
}
#[inline]
pub fn exclusive_maximum<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
limit: Value,
) -> ValidationError<'i> {
ValidationError::exclusive_maximum(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
limit,
)
}
#[cfg(not(feature = "arbitrary-precision"))]
#[inline]
pub fn multiple_of<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
multiple_of: f64,
) -> ValidationError<'i> {
ValidationError::multiple_of(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
multiple_of,
)
}
#[cfg(feature = "arbitrary-precision")]
#[inline]
pub fn multiple_of<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
multiple_of: Value,
) -> ValidationError<'i> {
ValidationError::multiple_of(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
multiple_of,
)
}
#[inline]
pub fn min_items<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
limit: u64,
) -> ValidationError<'i> {
ValidationError::min_items(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
limit,
)
}
#[inline]
pub fn max_items<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
limit: u64,
) -> ValidationError<'i> {
ValidationError::max_items(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
limit,
)
}
#[inline]
pub fn additional_items<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
limit: usize,
) -> ValidationError<'i> {
ValidationError::additional_items(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
limit,
)
}
#[inline]
pub fn unique_items<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
) -> ValidationError<'i> {
ValidationError::unique_items(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
)
}
#[inline]
pub fn contains<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
) -> ValidationError<'i> {
ValidationError::contains(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
)
}
#[inline]
pub fn min_properties<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
limit: u64,
) -> ValidationError<'i> {
ValidationError::min_properties(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
limit,
)
}
#[inline]
pub fn max_properties<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
limit: u64,
) -> ValidationError<'i> {
ValidationError::max_properties(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
limit,
)
}
#[inline]
pub fn required<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
property: &str,
) -> ValidationError<'i> {
ValidationError::required(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
Value::String(property.to_owned()),
)
}
#[inline]
pub fn additional_properties<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
unexpected: Vec<String>,
) -> ValidationError<'i> {
ValidationError::additional_properties(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
unexpected,
)
}
#[inline]
pub fn constant<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
expected: Value,
) -> ValidationError<'i> {
ValidationError::new(
instance.into(),
crate::error::ValidationErrorKind::Constant {
expected_value: expected,
},
instance_path,
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
)
}
#[inline]
pub fn enumeration<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
options: &Value,
) -> ValidationError<'i> {
ValidationError::enumeration(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
options,
)
}
#[inline]
pub fn false_schema<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
) -> ValidationError<'i> {
ValidationError::false_schema(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
)
}
#[inline]
pub fn not<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
schema: Value,
) -> ValidationError<'i> {
ValidationError::not(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
schema,
)
}
#[inline]
pub fn any_of<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
context: Vec<Vec<ValidationError<'i>>>,
) -> ValidationError<'i> {
ValidationError::any_of(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
context,
)
}
#[inline]
pub fn one_of_not_valid<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
context: Vec<Vec<ValidationError<'i>>>,
) -> ValidationError<'i> {
ValidationError::one_of_not_valid(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
context,
)
}
#[inline]
pub fn one_of_multiple_valid<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
context: Vec<Vec<ValidationError<'i>>>,
) -> ValidationError<'i> {
ValidationError::one_of_multiple_valid(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
context,
)
}
#[inline]
pub fn unevaluated_properties<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
unexpected: Vec<String>,
) -> ValidationError<'i> {
ValidationError::unevaluated_properties(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
unexpected,
)
}
#[inline]
pub fn unevaluated_items<'i>(
schema_path: &str,
instance_path: Location,
instance: impl Into<LazyInstance<'i>>,
unexpected: Vec<String>,
) -> ValidationError<'i> {
ValidationError::unevaluated_items(
Location::from_escaped(schema_path),
LazyEvaluationPath::SameAsSchemaPath,
instance_path,
instance.into(),
unexpected,
)
}
}
}
#[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)",
);
}
pub(crate) fn outcomes_with_only_vocabulary(
vocabulary: &str,
schema: &Value,
instances: &[Value],
) -> Vec<(bool, Vec<(String, String)>)> {
let meta_id = format!("json-schema:///meta/only-{vocabulary}");
let meta = serde_json::json!({
"$id": meta_id,
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/core": true,
format!("https://json-schema.org/draft/2020-12/vocab/{vocabulary}"): true
}
});
let registry = crate::Registry::new()
.add(meta_id.as_str(), &meta)
.expect("meta-schema registers")
.prepare()
.expect("registry prepares");
let mut schema = schema.clone();
schema["$schema"] = Value::String(meta_id);
let validator = crate::options()
.with_registry(®istry)
.build(&schema)
.expect("schema compiles");
instances
.iter()
.map(|instance| {
let errors = validator
.iter_errors(instance)
.map(|error| (error.schema_path().as_str().to_string(), error.to_string()))
.collect();
(validator.is_valid(instance), errors)
})
.collect()
}
#[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_absolute_keyword_locations(
schema: &Value,
instance: &Value,
expected: &[(&str, &str)],
) {
let validator = crate::validator_for(schema).expect("Invalid schema");
let actual: Vec<(String, String)> = validator
.iter_errors(instance)
.map(|error| {
(
error.kind().keyword().to_string(),
error
.absolute_keyword_location()
.expect("Absolute keyword location")
.to_string(),
)
})
.collect();
let expected: Vec<(String, String)> = expected
.iter()
.map(|(keyword, location)| ((*keyword).to_string(), (*location).to_string()))
.collect();
assert_eq!(actual, expected);
}
type ErrorLocations = (String, String, String, Option<String>);
fn error_locations(error: &ValidationError<'_>) -> ErrorLocations {
(
error.to_string(),
error.instance_path().as_str().to_owned(),
error.schema_path().as_str().to_owned(),
error.absolute_keyword_location().map(ToString::to_string),
)
}
#[track_caller]
pub(crate) fn assert_error_locations(
schema: &Value,
instance: &Value,
expected: &[(&str, &str, &str)],
) {
const BASE: &str = "https://example.com/s.json";
let mut root = schema.clone();
root["$id"] = Value::from(BASE);
let behind_ref =
serde_json::json!({"$id": BASE, "$defs": {"t": schema}, "$ref": "#/$defs/t"});
for (schema, prefix) in [(root, ""), (behind_ref, "/$defs/t")] {
let validator = crate::validator_for(&schema).expect("Invalid schema");
let expected: Vec<ErrorLocations> = expected
.iter()
.map(|(message, instance_path, schema_path)| {
(
(*message).to_owned(),
(*instance_path).to_owned(),
format!("{prefix}{schema_path}"),
Some(format!("{BASE}#{prefix}{schema_path}")),
)
})
.collect();
let actual: Vec<ErrorLocations> = validator
.iter_errors(instance)
.map(|error| error_locations(&error))
.collect();
assert_eq!(actual, expected, "iter_errors for {schema}");
let first = validator
.validate(instance)
.expect_err("Should be an error");
assert_eq!(
Some(error_locations(&first)),
expected.first().cloned(),
"validate for {schema}"
);
}
}
#[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);
}
#[track_caller]
pub(crate) fn assert_compile_error_with(
options: &crate::ValidationOptions,
subschema: &Value,
message: &str,
location: &str,
) {
let error = options
.build(&serde_json::json!({"$ref": "#/x", "x": subschema}))
.expect_err("Should fail to compile");
let location = format!("/x{location}");
assert_eq!(
(
error.to_string(),
error.instance_path().as_str(),
error.schema_path().as_str()
),
(message.to_string(), location.as_str(), location.as_str())
);
}
#[track_caller]
pub(crate) fn assert_compile_error(subschema: &Value, message: &str, location: &str) {
assert_compile_error_with(&crate::options(), subschema, message, location);
}
}
#[cfg(test)]
mod tests {
use crate::{validator_for, Registry, SerdeJson, ValidationError};
use super::Draft;
use serde_json::{json, Value};
use test_case::test_case;
#[test_case(&json!({"type": "object", "properties": {"sku": {"type": "string", "pattern": "([unclosed"}}}), "/properties/sku/pattern")]
#[test_case(&json!({"patternProperties": {"([unclosed": {"type": "string"}}}), "/patternProperties/([unclosed")]
fn build_error_instance_path(schema: &Value, expected: &str) {
let error = validator_for(schema).expect_err("Should fail to compile");
assert_eq!(error.instance_path().as_str(), expected);
}
#[test_case(Draft::Draft4, &json!({"$schema": "https://json-schema.org/draft/2020-12/schema", "prefixItems": [{"type": "string"}]}), false ; "2020-12 under draft 4")]
#[test_case(Draft::Draft4, &json!({"$schema": "https://json-schema.org/draft/2019-09/schema", "items": [{"type": "string"}]}), false ; "2019-09 under draft 4")]
#[test_case(Draft::Draft7, &json!({"$schema": "https://json-schema.org/draft/2020-12/schema", "prefixItems": [{"type": "string"}]}), false ; "2020-12 under draft 7")]
#[test_case(Draft::Draft4, &json!({"$schema": "http://json-schema.org/draft-07/schema#", "items": [{"type": "string"}]}), false ; "draft 7 under draft 4")]
#[test_case(Draft::Draft201909, &json!({"$schema": "https://json-schema.org/draft/2020-12/schema", "prefixItems": [{"type": "string"}]}), false ; "2020-12 under 2019-09")]
#[test_case(Draft::Draft202012, &json!({"$schema": "http://json-schema.org/draft-04/schema#", "prefixItems": [{"type": "string"}]}), true ; "draft 4 under 2020-12")]
fn nested_schema_switches_draft(root: Draft, pair: &Value, expected: bool) {
let validator = crate::options()
.with_draft(root)
.build(&json!({"properties": {"pair": pair}}))
.expect("Should build validator");
assert_eq!(validator.is_valid(&json!({"pair": [1]})), expected);
}
#[test_case(
&json!({"properties": {"tree": {"$id": "https://example.com/tree", "type": "object", "properties": {"value": {"type": "string"}}}}}),
&json!({"tree": {"value": 1}}),
&["https://example.com/tree#/properties/value/type"];
"draft 2020-12"
)]
#[test_case(
&json!({"properties": {"tree": {"$id": "https://example.com/tree", "type": "object"}}}),
&json!({"tree": 1}),
&["https://example.com/tree#/type"];
"keyword at resource root"
)]
#[test_case(
&json!({"$schema": "https://json-schema.org/draft/2019-09/schema", "properties": {"tree": {"$id": "https://example.com/tree", "properties": {"value": {"type": "string"}}}}}),
&json!({"tree": {"value": 1}}),
&["https://example.com/tree#/properties/value/type"];
"draft 2019-09"
)]
#[test_case(
&json!({"$schema": "http://json-schema.org/draft-07/schema#", "properties": {"tree": {"$id": "https://example.com/tree", "properties": {"value": {"type": "string"}}}}}),
&json!({"tree": {"value": 1}}),
&["https://example.com/tree#/properties/value/type"];
"draft 7"
)]
#[test_case(
&json!({"$schema": "http://json-schema.org/draft-06/schema#", "properties": {"tree": {"$id": "https://example.com/tree", "properties": {"value": {"type": "string"}}}}}),
&json!({"tree": {"value": 1}}),
&["https://example.com/tree#/properties/value/type"];
"draft 6"
)]
#[test_case(
&json!({"$schema": "http://json-schema.org/draft-04/schema#", "properties": {"tree": {"id": "https://example.com/tree", "properties": {"value": {"type": "string"}}}}}),
&json!({"tree": {"value": 1}}),
&["https://example.com/tree#/properties/value/type"];
"draft 4"
)]
#[test_case(
&json!({"$id": "https://example.com/root.json", "properties": {"tree": {"$id": "tree.json", "properties": {"value": {"type": "string"}}}}}),
&json!({"tree": {"value": 1}}),
&["https://example.com/tree.json#/properties/value/type"];
"relative id"
)]
#[test_case(
&json!({
"$id": "https://example.com/root.json",
"properties": {
"outer": {
"$id": "outer/",
"properties": {
"inner": {"$id": "inner.json", "properties": {"value": {"type": "string"}}},
"count": {"type": "integer"}
}
},
"name": {"type": "string"}
}
}),
&json!({"outer": {"inner": {"value": 1}, "count": "x"}, "name": 1}),
&[
"https://example.com/outer/#/properties/count/type",
"https://example.com/outer/inner.json#/properties/value/type",
"https://example.com/root.json#/properties/name/type",
];
"id nested in id"
)]
#[test_case(
&json!({"$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://example.com/root", "properties": {"tree": {"$id": "#tree", "properties": {"value": {"type": "string"}}}}}),
&json!({"tree": {"value": 1}}),
&["https://example.com/root#/properties/tree/properties/value/type"];
"draft 7 fragment-only id names no resource"
)]
#[test_case(
&json!({"$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://example.com/root", "properties": {"tree": {"$id": "https://example.com/tree", "$ref": "#/definitions/leaf"}}, "definitions": {"leaf": {"type": "string"}}}),
&json!({"tree": 1}),
&["https://example.com/root#/definitions/leaf/type"];
"draft 7 id beside ref names no resource"
)]
fn absolute_keyword_location_in_nested_resource(
schema: &Value,
instance: &Value,
expected: &[&str],
) {
let validator = validator_for(schema).expect("Should build validator");
let mut locations: Vec<String> = validator
.iter_errors(instance)
.map(|error| {
error
.absolute_keyword_location()
.expect("Absolute keyword location")
.to_string()
})
.collect();
locations.sort();
assert_eq!(locations, expected);
let evaluation = validator.evaluate(instance);
let mut locations: Vec<String> = evaluation
.iter_errors()
.map(|entry| {
entry
.absolute_keyword_location
.expect("Absolute keyword location")
.to_string()
})
.collect();
locations.sort();
assert_eq!(locations, expected);
}
#[test_case(
&json!({"properties": {"tree": {"$id": "https://example.com/tree", "properties": {"value": false}}}}),
&json!({"tree": {"value": 1}}),
&[("https://example.com/tree#/properties/value", Some("https://example.com/tree#/properties/value"))],
&[Some("https://example.com/tree#/properties/value")];
"false subschema in nested resource"
)]
#[test_case(
&json!({"properties": {"tree": {"$id": "https://example.com/tree", "unevaluatedProperties": false}}}),
&json!({"tree": {"a": 1, "b": 2}}),
&[
("https://example.com/tree#/unevaluatedProperties", Some("https://example.com/tree#/unevaluatedProperties")),
("https://example.com/tree#/unevaluatedProperties", Some("https://example.com/tree#/unevaluatedProperties")),
("https://example.com/tree#/unevaluatedProperties", Some("https://example.com/tree#/unevaluatedProperties")),
],
&[Some("https://example.com/tree#/unevaluatedProperties")];
"unevaluatedProperties false in nested resource"
)]
#[test_case(
&json!({"$id": "https://example.com/root", "$ref": "#/$defs/never", "$defs": {"never": false}}),
&json!(1),
&[("https://example.com/root#/$defs/never", Some("https://example.com/root#/$defs/never"))],
&[Some("https://example.com/root#/$defs/never")];
"ref to false in same resource"
)]
#[test_case(
&json!({"$id": "https://example.com/root", "$ref": "other#/$defs/never", "$defs": {"other": {"$id": "other", "$defs": {"never": false}}}}),
&json!(1),
&[("https://example.com/other#/$defs/never", Some("https://example.com/other#/$defs/never"))],
&[Some("https://example.com/other#/$defs/never")];
"ref to false in other resource"
)]
#[test_case(
&json!({"properties": {"value": false}}),
&json!({"value": 1}),
&[("/properties/value", None)],
&[None];
"false subschema without id"
)]
fn false_schema_error_locations(
schema: &Value,
instance: &Value,
evaluate: &[(&str, Option<&str>)],
iter_errors: &[Option<&str>],
) {
let validator = validator_for(schema).expect("Should build validator");
let evaluation = validator.evaluate(instance);
let locations: Vec<(&str, Option<String>)> = evaluation
.iter_errors()
.map(|entry| {
(
entry.schema_location,
entry.absolute_keyword_location.map(ToString::to_string),
)
})
.collect();
let expected: Vec<(&str, Option<String>)> = evaluate
.iter()
.map(|(schema_location, absolute)| (*schema_location, absolute.map(str::to_string)))
.collect();
assert_eq!(locations, expected);
let locations: Vec<Option<String>> = validator
.iter_errors(instance)
.map(|error| error.absolute_keyword_location().map(ToString::to_string))
.collect();
let expected: Vec<Option<String>> = iter_errors
.iter()
.map(|absolute| absolute.map(str::to_string))
.collect();
assert_eq!(locations, expected);
}
#[test_case(
&json!({"$id": "https://example.com/tree", "properties": {"a": {"type": "string"}, "b": {"type": "string"}}, "required": ["a", "b"]}),
&json!({}),
&["", "/properties", "https://example.com/tree#", "https://example.com/tree#/properties", "https://example.com/tree#/required"];
"required"
)]
#[test_case(
&json!({"$id": "https://example.com/tree", "minLength": 1, "maxLength": 2}),
&json!("abc"),
&["", "/properties", "https://example.com/tree#", "https://example.com/tree#/minLength", "https://example.com/tree#/maxLength"];
"maxLength beside minLength"
)]
#[test_case(
&json!({"$id": "https://example.com/tree", "type": "array", "minItems": 2, "maxItems": 0, "items": {"type": "string"}}),
&json!([1]),
&["", "/properties", "https://example.com/tree#", "https://example.com/tree#/type", "https://example.com/tree#/minItems", "https://example.com/tree#/maxItems", "https://example.com/tree#/items", "https://example.com/tree#/items", "https://example.com/tree#/items/type"];
"array bounds beside items"
)]
#[test_case(
&json!({"$id": "https://example.com/tree", "type": "array", "items": {"type": "string"}}),
&json!(1),
&["", "/properties", "https://example.com/tree#", "https://example.com/tree#/type", "https://example.com/tree#/items"];
"type beside items"
)]
#[test_case(
&json!({"$id": "https://example.com/tree", "patternProperties": {"^a": {}}, "additionalProperties": false}),
&json!({"a": 1, "b": 1}),
&["", "/properties", "https://example.com/tree#", "https://example.com/tree#/additionalProperties", "https://example.com/tree#/patternProperties/%5Ea", "https://example.com/tree#/patternProperties"];
"patternProperties beside additionalProperties"
)]
fn list_schema_locations_in_nested_resource(tree: &Value, instance: &Value, expected: &[&str]) {
let validator =
validator_for(&json!({"properties": {"tree": tree}})).expect("Should build validator");
let evaluation = validator.evaluate(&json!({"tree": instance}));
let list = serde_json::to_value(evaluation.list()).expect("List output");
let locations: Vec<&str> = list["details"]
.as_array()
.expect("List output details")
.iter()
.map(|entry| entry["schemaLocation"].as_str().expect("Schema location"))
.collect();
assert_eq!(locations, expected);
}
#[test]
fn output_locations_in_nested_resource() {
let validator = validator_for(&json!({
"properties": {
"tree": {"$id": "https://example.com/tree", "properties": {"value": {"type": "string"}}}
}
}))
.expect("Should build validator");
let evaluation = validator.evaluate(&json!({"tree": {"value": 1}}));
let list = serde_json::to_value(evaluation.list()).expect("List output");
let locations: Vec<&str> = list["details"]
.as_array()
.expect("List output details")
.iter()
.map(|entry| entry["schemaLocation"].as_str().expect("Schema location"))
.collect();
assert_eq!(
locations,
[
"",
"/properties",
"https://example.com/tree#",
"https://example.com/tree#/properties",
"https://example.com/tree#/properties/value",
"https://example.com/tree#/properties/value/type",
]
);
let evaluation = validator.evaluate(&json!({"tree": {"value": "leaf"}}));
let annotations: Vec<Option<String>> = evaluation
.iter_annotations()
.map(|entry| entry.absolute_keyword_location.map(ToString::to_string))
.collect();
assert_eq!(
annotations,
[
None,
Some("https://example.com/tree#/properties".to_string())
]
);
}
#[test_case(
&json!({"$id": "https://example.com/root", "patternProperties": {"^a": {}}, "additionalProperties": false}),
&json!({"a": 1}),
&[("https://example.com/root#/patternProperties", Some("https://example.com/root#/patternProperties"))];
"patternProperties beside additionalProperties at resource root"
)]
#[test_case(
&json!({"properties": {"tree": {"$id": "https://example.com/tree", "patternProperties": {"^a": {}}, "additionalProperties": false}}}),
&json!({"tree": {"a": 1}}),
&[
("/properties", None),
("https://example.com/tree#/patternProperties", Some("https://example.com/tree#/patternProperties")),
];
"patternProperties beside additionalProperties in nested resource"
)]
#[test_case(
&json!({"properties": {"tree": {"$id": "https://example.com/tree", "unknown": 1}}}),
&json!({"tree": 1}),
&[
("/properties", None),
("https://example.com/tree#", Some("https://example.com/tree#")),
];
"unknown keyword at nested resource root"
)]
fn annotation_locations(schema: &Value, instance: &Value, expected: &[(&str, Option<&str>)]) {
let validator = validator_for(schema).expect("Should build validator");
let evaluation = validator.evaluate(instance);
let locations: Vec<(&str, Option<String>)> = evaluation
.iter_annotations()
.map(|entry| {
(
entry.schema_location,
entry.absolute_keyword_location.map(ToString::to_string),
)
})
.collect();
let expected: Vec<(&str, Option<String>)> = expected
.iter()
.map(|(schema_location, absolute)| (*schema_location, absolute.map(str::to_string)))
.collect();
assert_eq!(locations, expected);
}
fn property_with_space() -> crate::Validator {
validator_for(&json!({
"$id": "https://example.com/root",
"properties": {"s p": {"type": "string"}}
}))
.expect("Should build validator")
}
#[test]
fn list_output_encodes_schema_locations() {
let validator = property_with_space();
let evaluation = validator.evaluate(&json!({"s p": 1}));
assert_eq!(
serde_json::to_value(evaluation.list()).expect("List output"),
json!({
"valid": false,
"details": [
{
"valid": false,
"evaluationPath": "",
"schemaLocation": "https://example.com/root#",
"instanceLocation": ""
},
{
"valid": false,
"evaluationPath": "/properties",
"schemaLocation": "https://example.com/root#/properties",
"instanceLocation": "",
"droppedAnnotations": ["s p"]
},
{
"valid": false,
"evaluationPath": "/properties/s p",
"schemaLocation": "https://example.com/root#/properties/s%20p",
"instanceLocation": "/s p"
},
{
"valid": false,
"evaluationPath": "/properties/s p/type",
"schemaLocation": "https://example.com/root#/properties/s%20p/type",
"instanceLocation": "/s p",
"errors": {"type": "1 is not of type \"string\""}
}
]
})
);
}
#[test]
fn hierarchical_output_encodes_schema_locations() {
let validator = property_with_space();
let evaluation = validator.evaluate(&json!({"s p": 1}));
assert_eq!(
serde_json::to_value(evaluation.hierarchical()).expect("Hierarchical output"),
json!({
"valid": false,
"evaluationPath": "",
"schemaLocation": "https://example.com/root#",
"instanceLocation": "",
"details": [
{
"valid": false,
"evaluationPath": "/properties",
"schemaLocation": "https://example.com/root#/properties",
"instanceLocation": "",
"droppedAnnotations": ["s p"],
"details": [
{
"valid": false,
"evaluationPath": "/properties/s p",
"schemaLocation": "https://example.com/root#/properties/s%20p",
"instanceLocation": "/s p",
"details": [
{
"valid": false,
"evaluationPath": "/properties/s p/type",
"schemaLocation": "https://example.com/root#/properties/s%20p/type",
"instanceLocation": "/s p",
"errors": {"type": "1 is not of type \"string\""}
}
]
}
]
}
]
})
);
}
#[test]
fn annotation_locations_are_encoded() {
let validator = validator_for(&json!({
"$id": "https://example.com/root",
"properties": {"s p": {"properties": {"a b": true}}}
}))
.expect("Should build validator");
let evaluation = validator.evaluate(&json!({"s p": {"a b": 1}}));
let locations: Vec<(&str, Option<String>, &str)> = evaluation
.iter_annotations()
.map(|entry| {
(
entry.schema_location,
entry.absolute_keyword_location.map(ToString::to_string),
entry.instance_location.as_str(),
)
})
.collect();
assert_eq!(
locations,
[
(
"https://example.com/root#/properties",
Some("https://example.com/root#/properties".to_string()),
""
),
(
"https://example.com/root#/properties/s%20p/properties",
Some("https://example.com/root#/properties/s%20p/properties".to_string()),
"/s p"
),
]
);
assert_eq!(
serde_json::to_value(evaluation.list()).expect("List output"),
json!({
"valid": true,
"details": [
{
"valid": true,
"evaluationPath": "",
"schemaLocation": "https://example.com/root#",
"instanceLocation": ""
},
{
"valid": true,
"evaluationPath": "/properties",
"schemaLocation": "https://example.com/root#/properties",
"instanceLocation": "",
"annotations": ["s p"]
},
{
"valid": true,
"evaluationPath": "/properties/s p",
"schemaLocation": "https://example.com/root#/properties/s%20p",
"instanceLocation": "/s p"
},
{
"valid": true,
"evaluationPath": "/properties/s p/properties",
"schemaLocation": "https://example.com/root#/properties/s%20p/properties",
"instanceLocation": "/s p",
"annotations": ["a b"]
},
{
"valid": true,
"evaluationPath": "/properties/s p/properties/a b",
"schemaLocation": "https://example.com/root#/properties/s%20p/properties/a%20b",
"instanceLocation": "/s p/a b"
}
]
})
);
}
#[test_case("s p", "s p", "s%20p")]
#[test_case("a%b", "a%b", "a%25b")]
#[test_case("q\"t", "q\"t", "q%22t")]
#[test_case("c^d", "c^d", "c%5Ed")]
#[test_case("[x]", "[x]", "%5Bx%5D")]
#[test_case("{y}", "{y}", "%7By%7D")]
#[test_case("p|q", "p|q", "p%7Cq")]
#[test_case("b\\s", "b\\s", "b%5Cs")]
#[test_case("<>", "<>", "%3C%3E")]
#[test_case("`b", "`b", "%60b")]
#[test_case("#h", "#h", "%23h")]
#[test_case("é", "é", "%C3%A9")]
#[test_case("t~e", "t~0e", "t~0e")]
#[test_case("s/l", "s~1l", "s~1l")]
fn error_locations_agree_on_encoding(name: &str, pointer: &str, encoded: &str) {
let validator = validator_for(&json!({
"$id": "https://example.com/root",
"properties": {name: {"type": "string"}}
}))
.expect("Should build validator");
let instance = json!({name: 1});
let expected = vec![(
format!("/properties/{pointer}/type"),
format!("https://example.com/root#/properties/{encoded}/type"),
format!("/{pointer}"),
)];
let from_iter_errors: Vec<(String, String, String)> = validator
.iter_errors(&instance)
.map(|error| {
(
error.schema_path().to_string(),
error
.absolute_keyword_location()
.expect("Absolute keyword location")
.to_string(),
error.instance_path().to_string(),
)
})
.collect();
assert_eq!(from_iter_errors, expected);
let evaluation = validator.evaluate(&instance);
let from_entries: Vec<(String, Option<String>, String)> = evaluation
.iter_errors()
.map(|entry| {
(
entry.schema_location.to_string(),
entry.absolute_keyword_location.map(ToString::to_string),
entry.instance_location.to_string(),
)
})
.collect();
assert_eq!(
from_entries,
[(
expected[0].1.clone(),
Some(expected[0].1.clone()),
expected[0].2.clone()
)]
);
let list = serde_json::to_value(evaluation.list()).expect("List output");
let from_list: Vec<(String, String, String)> = list["details"]
.as_array()
.expect("List output details")
.iter()
.filter(|entry| entry.get("errors").is_some())
.map(|entry| {
let field = |key: &str| entry[key].as_str().expect("Location").to_string();
(
field("evaluationPath"),
field("schemaLocation"),
field("instanceLocation"),
)
})
.collect();
assert_eq!(from_list, expected);
}
#[test]
fn ref_target_location_is_encoded() {
let validator = validator_for(&json!({
"$id": "https://example.com/root",
"$ref": "#/$defs/s%20p",
"$defs": {"s p": {"type": "string"}}
}))
.expect("Should build validator");
let evaluation = validator.evaluate(&json!(1));
let list = serde_json::to_value(evaluation.list()).expect("List output");
let locations: Vec<(&str, &str)> = list["details"]
.as_array()
.expect("List output details")
.iter()
.map(|entry| {
(
entry["evaluationPath"].as_str().expect("Evaluation path"),
entry["schemaLocation"].as_str().expect("Schema location"),
)
})
.collect();
assert_eq!(
locations,
[
("", "https://example.com/root#"),
("/$ref", "https://example.com/root#/$defs/s%20p"),
("/$ref/type", "https://example.com/root#/$defs/s%20p/type"),
]
);
let absolute: Vec<Option<String>> = validator
.iter_errors(&json!(1))
.map(|error| error.absolute_keyword_location().map(ToString::to_string))
.collect();
assert_eq!(
absolute,
[Some(
"https://example.com/root#/$defs/s%20p/type".to_string()
)]
);
}
const DRAFT4: &str = "http://json-schema.org/draft-04/schema#";
const DRAFT6: &str = "http://json-schema.org/draft-06/schema#";
const DRAFT7: &str = "http://json-schema.org/draft-07/schema#";
const DRAFT202012: &str = "https://json-schema.org/draft/2020-12/schema";
fn with_embedded_resource(root: &str, resource: &Value) -> Value {
json!({"$schema": root, "properties": {"a": resource}})
}
#[test_case(DRAFT202012, &json!({"$id": "https://example.com/a", "$schema": DRAFT4, "items": [{"type": "string"}]}), &json!(["x"]), &json!([1]) ; "draft 4 tuple items under 2020-12")]
#[test_case(DRAFT202012, &json!({"$id": "https://example.com/a", "$schema": DRAFT4, "minimum": 5, "exclusiveMinimum": true}), &json!(6), &json!(5) ; "draft 4 boolean exclusiveMinimum under 2020-12")]
#[test_case(DRAFT6, &json!({"$id": "https://example.com/a", "$schema": DRAFT4, "minimum": 5, "exclusiveMinimum": true}), &json!(6), &json!(5) ; "draft 4 boolean exclusiveMinimum under draft 6")]
#[test_case(DRAFT4, &json!({"$id": "https://example.com/a", "$schema": DRAFT202012, "exclusiveMinimum": 5}), &json!(6), &json!(5) ; "2020-12 numeric exclusiveMinimum under draft 4")]
fn embedded_resource_follows_own_meta_schema(
root: &str,
resource: &Value,
valid: &Value,
invalid: &Value,
) {
let validator =
validator_for(&with_embedded_resource(root, resource)).expect("Should build validator");
assert_eq!(
(
validator.is_valid(&json!({"a": valid})),
validator.is_valid(&json!({"a": invalid}))
),
(true, false)
);
}
#[test_case(DRAFT7, &json!({"$id": "https://example.com/a", "$schema": DRAFT202012, "minContains": -1}), "/properties/a/minContains" ; "2020-12 under draft 7")]
#[test_case(DRAFT202012, &json!({"$id": "https://example.com/a", "$schema": DRAFT4, "minimum": 1, "exclusiveMinimum": 5}), "/properties/a/exclusiveMinimum" ; "draft 4 under 2020-12")]
#[test_case(DRAFT202012, &json!({"$id": "https://example.com/a", "$schema": DRAFT4, "properties": {"b": {"$id": "https://example.com/b", "$schema": DRAFT202012, "minContains": -1}}}), "/properties/a/properties/b/minContains" ; "2020-12 under draft 4 under 2020-12")]
fn embedded_resource_rejected_by_own_meta_schema(root: &str, resource: &Value, expected: &str) {
let error = validator_for(&with_embedded_resource(root, resource))
.expect_err("Should reject the embedded resource");
assert_eq!(error.instance_path().as_str(), expected);
}
#[test]
fn nested_schema_without_id_follows_root_meta_schema() {
let error = validator_for(&with_embedded_resource(
DRAFT202012,
&json!({"$schema": DRAFT4, "items": [{"type": "string"}]}),
))
.expect_err("Should reject array-form `items` under 2020-12");
assert_eq!(error.instance_path().as_str(), "/properties/a/items");
}
#[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_case(&json!({"type": "object", "required": ["name"]}), true ; "valid")]
#[test_case(&json!({"type": "invalid_type", "required": true}), false ; "invalid")]
#[test_case(&json!(true), true ; "boolean schema")]
#[test_case(&json!({"$schema": "http://json-schema.org/draft-04/schema#", "minimum": 5, "exclusiveMinimum": true}), true ; "draft4 boolean exclusive minimum")]
#[test_case(&json!({"$schema": "http://json-schema.org/draft-04/schema#", "exclusiveMinimum": 5}), false ; "draft4 numeric exclusive minimum")]
#[test_case(&json!({"$schema": "https://json-schema.org/draft/2020-12/schema", "exclusiveMinimum": 5}), true ; "draft2020-12 numeric exclusive minimum")]
fn test_meta_validate_for(schema: &serde_json::Value, expected: bool) {
let result = crate::meta::validate_for::<SerdeJson>(schema);
assert_eq!(result.is_ok(), expected);
assert_eq!(result.is_ok(), crate::meta::validate(schema).is_ok());
assert_eq!(
crate::meta::is_valid_for::<SerdeJson>(schema).expect("Meta-schema is resolvable"),
expected
);
}
#[test]
fn test_meta_validate_for_unresolvable_meta_schema() {
let schema = json!({"$schema": "htt://json-schema.org/draft-07/schema"});
let error = crate::meta::validate_for::<SerdeJson>(&schema)
.expect_err("Unresolvable meta-schema should fail");
assert!(matches!(
error.kind(),
crate::error::ValidationErrorKind::Referencing(_)
));
let error = crate::meta::is_valid_for::<SerdeJson>(&schema)
.expect_err("Unresolvable meta-schema should fail");
assert!(matches!(
error.kind(),
crate::error::ValidationErrorKind::Referencing(_)
));
}
#[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"})));
}
fn dialect_registry(vocabularies: &Value) -> Registry<'static> {
Registry::new()
.add(
"https://example.com/dialect",
json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/dialect",
"$vocabulary": vocabularies,
}),
)
.expect("Should accept meta-schema")
.prepare()
.expect("Should create registry")
}
fn format_assertion_dialect(required: bool) -> Value {
json!({
"https://json-schema.org/draft/2020-12/vocab/core": true,
"https://json-schema.org/draft/2020-12/vocab/validation": true,
"https://json-schema.org/draft/2020-12/vocab/format-assertion": required,
})
}
#[test_case(true)]
#[test_case(false)]
fn format_assertion_vocabulary_asserts_without_opt_in(required: bool) {
let registry = dialect_registry(&format_assertion_dialect(required));
let validator = crate::options()
.with_registry(®istry)
.build(&json!({
"$schema": "https://example.com/dialect",
"type": "string",
"format": "date-time"
}))
.expect("Should build validator");
assert!(validator.is_valid(&json!("2026-08-22T12:00:00Z")));
assert!(!validator.is_valid(&json!("not-a-date")));
}
#[test_case(false)]
#[test_case(true)]
fn format_assertion_vocabulary_rejects_unknown_format(ignore_unknown_formats: bool) {
let registry = dialect_registry(&format_assertion_dialect(true));
let error = crate::options()
.with_registry(®istry)
.should_ignore_unknown_formats(ignore_unknown_formats)
.build(&json!({
"$schema": "https://example.com/dialect",
"format": "totally-made-up"
}))
.expect_err("Should reject unknown format");
assert_eq!(
error.to_string(),
"Unknown format: 'totally-made-up'. The meta-schema asserts formats, so unrecognized ones cannot be ignored. Register a check for it or disable format validation"
);
}
#[test]
fn format_assertion_vocabulary_honors_explicit_opt_out() {
let registry = dialect_registry(&format_assertion_dialect(true));
let validator = crate::options()
.with_registry(®istry)
.should_validate_formats(false)
.build(&json!({
"$schema": "https://example.com/dialect",
"format": "date-time"
}))
.expect("Should build validator");
assert!(validator.is_valid(&json!("not-a-date")));
}
#[test_case(true, false ; "required")]
#[test_case(false, true ; "optional")]
fn draft201909_format_vocabulary_asserts_when_required(required: bool, expected: bool) {
let registry = Registry::new()
.add(
"https://example.com/dialect",
json!({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://example.com/dialect",
"$vocabulary": {
"https://json-schema.org/draft/2019-09/vocab/core": true,
"https://json-schema.org/draft/2019-09/vocab/validation": true,
"https://json-schema.org/draft/2019-09/vocab/format": required,
},
}),
)
.expect("Should accept meta-schema")
.prepare()
.expect("Should create registry");
let validator = crate::options()
.with_registry(®istry)
.build(&json!({
"$schema": "https://example.com/dialect",
"format": "ipv4"
}))
.expect("Should build validator");
assert_eq!(validator.is_valid(&json!("not-an-ipv4")), expected);
}
#[test]
fn dialect_without_id_declares_vocabularies() {
let registry = Registry::new()
.add(
"https://example.com/dialect",
json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$vocabulary": format_assertion_dialect(true),
}),
)
.expect("Should accept meta-schema")
.prepare()
.expect("Should create registry");
let validator = crate::options()
.with_registry(®istry)
.build(&json!({
"$schema": "https://example.com/dialect",
"format": "ipv4"
}))
.expect("Should build validator");
assert!(!validator.is_valid(&json!("not-an-ipv4")));
}
#[test]
fn nested_dialect_declares_vocabularies() {
let registry = dialect_registry(&format_assertion_dialect(true));
let validator = crate::options()
.with_draft(Draft::Draft4)
.with_registry(®istry)
.build(&json!({
"properties": {
"address": {"$schema": "https://example.com/dialect", "format": "ipv4"}
}
}))
.expect("Should build validator");
assert!(!validator.is_valid(&json!({"address": "not-an-ipv4"})));
}
#[test_case("https://json-schema.org/draft/2020-12/meta/format-assertion" ; "2020-12 format-assertion")]
#[test_case("https://json-schema.org/draft/2019-09/meta/format" ; "2019-09 format")]
fn bundled_format_meta_schema_asserts(meta_schema: &str) {
let validator = crate::options()
.build(&json!({
"$schema": meta_schema,
"format": "ipv4"
}))
.expect("Should build validator");
assert!(validator.is_valid(&json!("127.0.0.1")));
assert!(!validator.is_valid(&json!("not-an-ipv4")));
}
#[test]
fn format_annotation_vocabulary_does_not_assert() {
let registry = dialect_registry(&json!({
"https://json-schema.org/draft/2020-12/vocab/core": true,
"https://json-schema.org/draft/2020-12/vocab/format-annotation": true,
}));
let validator = crate::options()
.with_registry(®istry)
.build(&json!({
"$schema": "https://example.com/dialect",
"format": "date-time"
}))
.expect("Should build validator");
assert!(validator.is_valid(&json!("not-a-date")));
}
const UNKNOWN_VOCABULARY_ERROR: &str = "Unknown vocabulary: 'https://example.com/vocab/made-up' is required by the meta-schema. Adjust configuration to declare support for it";
#[test_case("https://example.com/vocab/made-up", true, false, Some(UNKNOWN_VOCABULARY_ERROR) ; "required undeclared")]
#[test_case("https://example.com/vocab/made-up", false, false, None ; "optional undeclared")]
#[test_case("https://example.com/vocab/made-up", true, true, None ; "required declared")]
#[test_case("https://Example.COM/vocab/made-up", true, true, None ; "declared uppercase host")]
#[test_case("https://example.com/vocab/./made-up", true, true, None ; "declared dot segment")]
#[test_case("https://example.com/vocab/made%2Dup", true, true, None ; "declared percent-encoded unreserved")]
fn unknown_vocabulary(uri: &str, required: bool, declared: bool, expected: Option<&str>) {
let registry = dialect_registry(&json!({
"https://json-schema.org/draft/2020-12/vocab/core": true,
uri: required,
}));
let mut options = crate::options().with_registry(®istry);
if declared {
options = options.with_vocabulary(uri);
}
let error = options
.clone()
.build(&json!({"$schema": "https://example.com/dialect"}))
.err()
.map(|error| error.to_string());
assert_eq!(error.as_deref(), expected);
}
#[test_case("$ref", "https://example.com/inner" ; "ref to resource")]
#[test_case("$ref", "https://example.com/inner#/$defs/leaf" ; "ref into resource")]
#[test_case("$dynamicRef", "https://example.com/inner" ; "dynamic ref to resource")]
fn required_unknown_vocabulary_is_rejected_behind_reference(keyword: &str, target: &str) {
let registry = Registry::new()
.add(
"https://example.com/dialect",
json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/dialect",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/core": true,
"https://example.com/vocab/made-up": true,
},
}),
)
.expect("Should accept meta-schema")
.add(
"https://example.com/inner",
json!({
"$schema": "https://example.com/dialect",
"$id": "https://example.com/inner",
"type": "string",
"$defs": {"leaf": {"type": "string"}},
}),
)
.expect("Should accept inner schema")
.prepare()
.expect("Should create registry");
let error = crate::options()
.with_registry(®istry)
.build(&json!({keyword: target}))
.expect_err("Should reject required unknown vocabulary");
assert_eq!(error.to_string(), UNKNOWN_VOCABULARY_ERROR);
}
#[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());
}
#[test_case(
"http://json-schema.org/draft-04/schema#",
"http://json-schema.org/draft-04/schema#/definitions/positiveInteger/minimum",
"/definitions/positiveInteger/minimum";
"draft 4"
)]
#[test_case(
"http://json-schema.org/draft-06/schema#",
"http://json-schema.org/draft-06/schema#/definitions/nonNegativeInteger/minimum",
"/definitions/nonNegativeInteger/minimum";
"draft 6"
)]
#[test_case(
"http://json-schema.org/draft-07/schema#",
"http://json-schema.org/draft-07/schema#/definitions/nonNegativeInteger/minimum",
"/definitions/nonNegativeInteger/minimum";
"draft 7"
)]
#[test_case(
"https://json-schema.org/draft/2019-09/schema",
"https://json-schema.org/draft/2019-09/meta/validation#/$defs/nonNegativeInteger/minimum",
"/$defs/nonNegativeInteger/minimum";
"draft 2019-09"
)]
#[test_case(
"https://json-schema.org/draft/2020-12/schema",
"https://json-schema.org/draft/2020-12/meta/validation#/$defs/nonNegativeInteger/minimum",
"/$defs/nonNegativeInteger/minimum";
"draft 2020-12"
)]
fn test_meta_validator_for_reports_negative_length(
dialect: &str,
schema_location: &str,
schema_path: &str,
) {
const NEGATIVE: &str = "-1 is less than the minimum of 0";
let schema = json!({"$schema": dialect, "minLength": -1});
let validator = crate::meta::validator_for(&schema).expect("Valid meta-schema");
let evaluation = validator.evaluate(&schema);
let evaluated: Vec<_> = evaluation
.iter_errors()
.map(|entry| {
(
entry.instance_location.as_str().to_string(),
entry.schema_location.to_string(),
entry.error.to_string(),
)
})
.collect();
let errors: Vec<_> = validator
.iter_errors(&schema)
.map(|error| {
(
error.to_string(),
error.instance_path().to_string(),
error.schema_path().to_string(),
)
})
.collect();
assert_eq!(
(evaluation.flag().valid, evaluated, errors),
(
false,
vec![(
"/minLength".to_string(),
schema_location.to_string(),
NEGATIVE.to_string()
)],
vec![(
NEGATIVE.to_string(),
"/minLength".to_string(),
schema_path.to_string()
)]
)
);
}
}
#[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
})));
}
}