use crate::{
paths::{LazyLocation, Location},
types::{JsonType, JsonTypeSet},
validator::LazyEvaluationPath,
LazyInstance,
};
use referencing::Uri;
use serde_json::{Number, Value};
use std::{
borrow::Cow,
error,
fmt::{self, Formatter, Write},
slice,
string::FromUtf8Error,
sync::Arc,
vec,
};
#[derive(Debug)]
pub struct ValidationError<'a> {
repr: Box<ValidationErrorRepr<'a>>,
}
struct ValidationErrorRepr<'a> {
instance: LazyInstance<'a>,
kind: ValidationErrorKind,
instance_path: Location,
schema_path: Location,
tracker: LazyEvaluationPath,
absolute_keyword_location: Option<Arc<Uri<String>>>,
}
impl fmt::Debug for ValidationErrorRepr<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("ValidationErrorRepr")
.field("instance", &self.instance)
.field("kind", &self.kind)
.field("instance_path", &self.instance_path)
.field("schema_path", &self.schema_path)
.field("absolute_keyword_location", &self.absolute_keyword_location)
.finish_non_exhaustive()
}
}
#[doc(hidden)]
pub trait ValidationErrorIterator<'a>: Iterator<Item = ValidationError<'a>> + Send + Sync {}
impl<'a, T> ValidationErrorIterator<'a> for T where
T: Iterator<Item = ValidationError<'a>> + Send + Sync
{
}
pub struct ErrorIterator<'a> {
iter: Box<dyn ValidationErrorIterator<'a> + 'a>,
}
impl<'a> ErrorIterator<'a> {
#[inline]
pub(crate) fn from_iterator<T>(iterator: T) -> Self
where
T: ValidationErrorIterator<'a> + 'a,
{
Self {
iter: Box::new(iterator),
}
}
#[inline]
#[must_use]
pub fn into_errors(self) -> ValidationErrors<'a> {
ValidationErrors {
errors: self.collect(),
}
}
}
pub struct ValidationErrors<'a> {
errors: Vec<ValidationError<'a>>,
}
impl<'a> ValidationErrors<'a> {
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.errors.len()
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.errors.is_empty()
}
#[inline]
#[must_use]
pub fn as_slice(&self) -> &[ValidationError<'a>] {
&self.errors
}
#[inline]
pub fn iter(&self) -> slice::Iter<'_, ValidationError<'a>> {
self.errors.iter()
}
#[inline]
pub fn iter_mut(&mut self) -> slice::IterMut<'_, ValidationError<'a>> {
self.errors.iter_mut()
}
}
impl fmt::Display for ValidationErrors<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if self.errors.is_empty() {
f.write_str("Validation succeeded")
} else {
writeln!(f, "Validation errors:")?;
for (idx, error) in self.errors.iter().enumerate() {
if error.instance_path().is_empty() {
writeln!(f, "{:02}: {error}", idx + 1)?;
} else {
writeln!(f, "{:02}: {}: {error}", idx + 1, error.instance_path())?;
}
}
Ok(())
}
}
}
impl fmt::Debug for ValidationErrors<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("ValidationErrors")
.field("errors", &self.errors)
.finish()
}
}
impl error::Error for ValidationErrors<'_> {}
impl<'a> Iterator for ErrorIterator<'a> {
type Item = ValidationError<'a>;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.iter.as_mut().next()
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.iter.size_hint()
}
}
impl<'a> IntoIterator for ValidationErrors<'a> {
type Item = ValidationError<'a>;
type IntoIter = vec::IntoIter<ValidationError<'a>>;
fn into_iter(self) -> Self::IntoIter {
self.errors.into_iter()
}
}
impl<'a, 'b> IntoIterator for &'b ValidationErrors<'a> {
type Item = &'b ValidationError<'a>;
type IntoIter = slice::Iter<'b, ValidationError<'a>>;
fn into_iter(self) -> Self::IntoIter {
self.errors.iter()
}
}
impl<'a, 'b> IntoIterator for &'b mut ValidationErrors<'a> {
type Item = &'b mut ValidationError<'a>;
type IntoIter = slice::IterMut<'b, ValidationError<'a>>;
fn into_iter(self) -> Self::IntoIter {
self.errors.iter_mut()
}
}
#[derive(Debug)]
#[allow(missing_docs)]
pub enum ValidationErrorKind {
AdditionalItems { limit: usize },
AdditionalProperties { unexpected: Vec<String> },
AnyOf {
context: Vec<Vec<ValidationError<'static>>>,
},
BacktrackLimitExceeded { error: fancy_regex::Error },
RegexEngineFailure { message: String },
Constant { expected_value: Value },
Contains,
ContentEncoding { content_encoding: String },
ContentMediaType { content_media_type: String },
Custom { keyword: String, message: String },
Enum { options: Value },
ExclusiveMaximum { limit: Value },
ExclusiveMinimum { limit: Value },
FalseSchema,
Format { format: String },
FromUtf8 { error: FromUtf8Error },
MaxItems { limit: u64 },
Maximum { limit: Value },
MaxLength { limit: u64 },
MaxProperties { limit: u64 },
MinItems { limit: u64 },
Minimum { limit: Value },
MinLength { limit: u64 },
MinProperties { limit: u64 },
MultipleOf {
#[cfg(feature = "arbitrary-precision")]
multiple_of: Value,
#[cfg(not(feature = "arbitrary-precision"))]
multiple_of: f64,
},
Not { schema: Value },
OneOfMultipleValid {
context: Vec<Vec<ValidationError<'static>>>,
},
OneOfNotValid {
context: Vec<Vec<ValidationError<'static>>>,
},
Pattern { pattern: String },
PropertyNames {
error: Box<ValidationError<'static>>,
},
Required { property: Value },
Type { kind: TypeKind },
UnevaluatedItems { unexpected: Vec<String> },
UnevaluatedProperties { unexpected: Vec<String> },
UniqueItems,
Referencing(referencing::Error),
}
impl ValidationErrorKind {
#[must_use]
pub fn keyword(&self) -> &str {
match self {
ValidationErrorKind::AdditionalItems { .. } => "additionalItems",
ValidationErrorKind::AdditionalProperties { .. } => "additionalProperties",
ValidationErrorKind::AnyOf { .. } => "anyOf",
ValidationErrorKind::BacktrackLimitExceeded { .. }
| ValidationErrorKind::RegexEngineFailure { .. }
| ValidationErrorKind::Pattern { .. } => "pattern",
ValidationErrorKind::Constant { .. } => "const",
ValidationErrorKind::Contains => "contains",
ValidationErrorKind::ContentEncoding { .. } | ValidationErrorKind::FromUtf8 { .. } => {
"contentEncoding"
}
ValidationErrorKind::ContentMediaType { .. } => "contentMediaType",
ValidationErrorKind::Custom { keyword, .. } => keyword,
ValidationErrorKind::Enum { .. } => "enum",
ValidationErrorKind::ExclusiveMaximum { .. } => "exclusiveMaximum",
ValidationErrorKind::ExclusiveMinimum { .. } => "exclusiveMinimum",
ValidationErrorKind::FalseSchema => "falseSchema",
ValidationErrorKind::Format { .. } => "format",
ValidationErrorKind::MaxItems { .. } => "maxItems",
ValidationErrorKind::Maximum { .. } => "maximum",
ValidationErrorKind::MaxLength { .. } => "maxLength",
ValidationErrorKind::MaxProperties { .. } => "maxProperties",
ValidationErrorKind::MinItems { .. } => "minItems",
ValidationErrorKind::Minimum { .. } => "minimum",
ValidationErrorKind::MinLength { .. } => "minLength",
ValidationErrorKind::MinProperties { .. } => "minProperties",
ValidationErrorKind::MultipleOf { .. } => "multipleOf",
ValidationErrorKind::Not { .. } => "not",
ValidationErrorKind::OneOfMultipleValid { .. }
| ValidationErrorKind::OneOfNotValid { .. } => "oneOf",
ValidationErrorKind::PropertyNames { .. } => "propertyNames",
ValidationErrorKind::Required { .. } => "required",
ValidationErrorKind::Type { .. } => "type",
ValidationErrorKind::UnevaluatedItems { .. } => "unevaluatedItems",
ValidationErrorKind::UnevaluatedProperties { .. } => "unevaluatedProperties",
ValidationErrorKind::UniqueItems => "uniqueItems",
ValidationErrorKind::Referencing(_) => "$ref",
}
}
}
#[derive(Debug)]
#[allow(missing_docs)]
pub enum TypeKind {
Single(JsonType),
Multiple(JsonTypeSet),
}
#[derive(Debug)]
#[non_exhaustive]
pub struct ValidationErrorParts<'a> {
pub instance: Cow<'a, Value>,
pub kind: ValidationErrorKind,
pub instance_path: Location,
pub schema_path: Location,
pub evaluation_path: Location,
pub absolute_keyword_location: Option<Arc<Uri<String>>>,
}
impl<'a> ValidationError<'a> {
#[inline]
#[must_use]
pub(crate) fn new(
instance: LazyInstance<'a>,
kind: ValidationErrorKind,
instance_path: Location,
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
) -> Self {
Self {
repr: Box::new(ValidationErrorRepr {
instance,
kind,
instance_path,
schema_path,
tracker: tracker.into(),
absolute_keyword_location: None,
}),
}
}
#[inline]
#[must_use]
pub fn instance(&self) -> &Cow<'a, Value> {
self.repr.instance.get()
}
#[inline]
#[must_use]
pub fn kind(&self) -> &ValidationErrorKind {
&self.repr.kind
}
#[inline]
#[must_use]
pub fn instance_path(&self) -> &Location {
&self.repr.instance_path
}
#[inline]
#[must_use]
pub fn schema_path(&self) -> &Location {
&self.repr.schema_path
}
#[inline]
#[must_use]
pub fn evaluation_path(&self) -> &Location {
self.repr.tracker.resolve(&self.repr.schema_path)
}
#[inline]
#[must_use]
pub fn absolute_keyword_location(&self) -> Option<&Uri<String>> {
self.repr.absolute_keyword_location.as_deref()
}
#[inline]
pub(crate) fn with_absolute_keyword_location(mut self, uri: Option<Arc<Uri<String>>>) -> Self {
if self.repr.absolute_keyword_location.is_none() {
self.repr.absolute_keyword_location = uri;
}
self
}
pub(crate) fn with_instance_path_prefix(mut self, prefix: &Location) -> Self {
self.repr.instance_path = prefix.join_raw_suffix(self.repr.instance_path.as_str());
self
}
pub(crate) fn set_absolute_keyword_location(&mut self, uri: &Arc<Uri<String>>) {
if self.repr.absolute_keyword_location.is_none() {
self.repr.absolute_keyword_location = Some(Arc::clone(uri));
}
}
#[inline]
#[must_use]
pub fn into_parts(self) -> ValidationErrorParts<'a> {
let repr = *self.repr;
let evaluation_path = repr.tracker.into_owned(repr.schema_path.clone());
ValidationErrorParts {
instance: repr.instance.into_cow(),
kind: repr.kind,
instance_path: repr.instance_path,
schema_path: repr.schema_path,
evaluation_path,
absolute_keyword_location: repr.absolute_keyword_location,
}
}
#[inline]
fn borrowed(
instance: LazyInstance<'a>,
kind: ValidationErrorKind,
instance_path: Location,
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
) -> Self {
Self::new(instance, kind, instance_path, schema_path, tracker)
}
#[must_use]
pub fn masked<'b>(&'b self) -> MaskedValidationError<'a, 'b, 'static> {
self.masked_with("value")
}
pub fn masked_with<'b, 'c>(
&'b self,
placeholder: impl Into<Cow<'c, str>>,
) -> MaskedValidationError<'a, 'b, 'c> {
MaskedValidationError {
error: self,
placeholder: placeholder.into(),
}
}
#[must_use]
pub fn to_owned(self) -> ValidationError<'static> {
let parts = self.into_parts();
ValidationError::new(
LazyInstance::Ready(Cow::Owned(parts.instance.into_owned())),
parts.kind,
parts.instance_path,
parts.schema_path,
parts.evaluation_path,
)
.with_absolute_keyword_location(parts.absolute_keyword_location)
}
pub(crate) fn into_build_error(self) -> ValidationError<'static> {
let parts = self.into_parts();
ValidationError::new(
LazyInstance::Ready(Cow::Owned(parts.instance.into_owned())),
parts.kind,
parts.schema_path.clone(),
parts.schema_path,
parts.evaluation_path,
)
.with_absolute_keyword_location(parts.absolute_keyword_location)
}
pub(crate) fn additional_items(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
limit: usize,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::AdditionalItems { limit },
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn additional_properties(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
unexpected: Vec<String>,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::AdditionalProperties { unexpected },
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn any_of(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
context: Vec<Vec<ValidationError<'a>>>,
) -> ValidationError<'a> {
let context = context
.into_iter()
.map(|errors| errors.into_iter().map(ValidationError::to_owned).collect())
.collect::<Vec<_>>();
Self::borrowed(
instance,
ValidationErrorKind::AnyOf { context },
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn backtrack_limit(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
error: fancy_regex::Error,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::BacktrackLimitExceeded { error },
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn regex_engine_failure(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
message: String,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::RegexEngineFailure { message },
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn constant_array(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
expected_value: &[Value],
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::Constant {
expected_value: Value::Array(expected_value.to_vec()),
},
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn constant_boolean(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
expected_value: bool,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::Constant {
expected_value: Value::Bool(expected_value),
},
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn constant_null(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::Constant {
expected_value: Value::Null,
},
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn constant_number(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
expected_value: &Number,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::Constant {
expected_value: Value::Number(expected_value.clone()),
},
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn constant_object(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
expected_value: &Value,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::Constant {
expected_value: expected_value.clone(),
},
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn constant_string(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
expected_value: &str,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::Constant {
expected_value: Value::String(expected_value.to_string()),
},
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn contains(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::Contains,
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn content_encoding(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
encoding: &str,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::ContentEncoding {
content_encoding: encoding.to_string(),
},
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn content_media_type(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
media_type: &str,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::ContentMediaType {
content_media_type: media_type.to_string(),
},
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn enumeration(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
options: &Value,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::Enum {
options: options.clone(),
},
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn exclusive_maximum(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
limit: Value,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::ExclusiveMaximum { limit },
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn exclusive_minimum(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
limit: Value,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::ExclusiveMinimum { limit },
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn false_schema(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::FalseSchema,
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn format(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
format: impl Into<String>,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::Format {
format: format.into(),
},
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn from_utf8(error: FromUtf8Error) -> ValidationError<'a> {
ValidationError::new(
LazyInstance::Ready(Cow::Owned(Value::Null)),
ValidationErrorKind::FromUtf8 { error },
Location::new(),
Location::new(),
Location::new(),
)
}
pub(crate) fn max_items(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
limit: u64,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::MaxItems { limit },
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn maximum(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
limit: Value,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::Maximum { limit },
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn max_length(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
limit: u64,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::MaxLength { limit },
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn max_properties(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
limit: u64,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::MaxProperties { limit },
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn min_items(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
limit: u64,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::MinItems { limit },
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn minimum(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
limit: Value,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::Minimum { limit },
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn min_length(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
limit: u64,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::MinLength { limit },
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn min_properties(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
limit: u64,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::MinProperties { limit },
instance_path,
schema_path,
tracker,
)
}
#[cfg(feature = "arbitrary-precision")]
pub(crate) fn multiple_of(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
multiple_of: Value,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::MultipleOf { multiple_of },
instance_path,
schema_path,
tracker,
)
}
#[cfg(not(feature = "arbitrary-precision"))]
pub(crate) fn multiple_of(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
multiple_of: f64,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::MultipleOf { multiple_of },
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn not(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
schema: Value,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::Not { schema },
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn one_of_multiple_valid(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
context: Vec<Vec<ValidationError<'a>>>,
) -> ValidationError<'a> {
let context = context
.into_iter()
.map(|errors| errors.into_iter().map(ValidationError::to_owned).collect())
.collect::<Vec<_>>();
Self::borrowed(
instance,
ValidationErrorKind::OneOfMultipleValid { context },
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn one_of_not_valid(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
context: Vec<Vec<ValidationError<'a>>>,
) -> ValidationError<'a> {
let context = context
.into_iter()
.map(|errors| errors.into_iter().map(ValidationError::to_owned).collect())
.collect::<Vec<_>>();
Self::borrowed(
instance,
ValidationErrorKind::OneOfNotValid { context },
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn pattern(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
pattern: String,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::Pattern { pattern },
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn property_names(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
error: ValidationError<'a>,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::PropertyNames {
error: Box::new(error.to_owned()),
},
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn required(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
property: Value,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::Required { property },
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn single_type_error(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
type_name: JsonType,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::Type {
kind: TypeKind::Single(type_name),
},
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn multiple_type_error(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
types: JsonTypeSet,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::Type {
kind: TypeKind::Multiple(types),
},
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn unevaluated_items(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
unexpected: Vec<String>,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::UnevaluatedItems { unexpected },
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn unevaluated_properties(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
unexpected: Vec<String>,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::UnevaluatedProperties { unexpected },
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn unique_items(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::UniqueItems,
instance_path,
schema_path,
tracker,
)
}
pub fn custom(message: impl Into<String>) -> ValidationError<'static> {
ValidationError::new(
LazyInstance::Ready(Cow::Owned(Value::Null)),
ValidationErrorKind::Custom {
keyword: String::new(),
message: message.into(),
},
Location::new(),
Location::new(),
Location::new(),
)
}
pub fn schema(message: impl Into<String>) -> ValidationError<'static> {
ValidationError::new(
LazyInstance::Ready(Cow::Owned(Value::Null)),
ValidationErrorKind::Custom {
keyword: String::new(),
message: message.into(),
},
Location::new(),
Location::new(),
Location::new(),
)
}
pub(crate) fn with_context<'i>(
self,
instance: &'i Value,
instance_path: &LazyLocation,
schema_path: &Location,
keyword: &str,
) -> ValidationError<'i> {
let kind = match self.repr.kind {
ValidationErrorKind::Custom { message, .. } => ValidationErrorKind::Custom {
keyword: keyword.to_string(),
message,
},
other => other,
};
ValidationError::new(
LazyInstance::Ready(Cow::Borrowed(instance)),
kind,
instance_path.into(),
schema_path.clone(),
LazyEvaluationPath::SameAsSchemaPath,
)
}
#[cfg(feature = "macros")]
pub(crate) fn with_generated_context<'i>(
self,
instance: Cow<'i, Value>,
instance_path: Location,
schema_path: Location,
keyword: &str,
) -> ValidationError<'i> {
let kind = match self.repr.kind {
ValidationErrorKind::Custom { message, .. } => ValidationErrorKind::Custom {
keyword: keyword.to_string(),
message,
},
other => other,
};
ValidationError::new(
LazyInstance::Ready(instance),
kind,
instance_path,
schema_path,
LazyEvaluationPath::SameAsSchemaPath,
)
}
pub(crate) fn with_schema_context<'s>(
self,
schema_value: &'s Value,
schema_path: Location,
keyword: &str,
) -> ValidationError<'s> {
let kind = match self.repr.kind {
ValidationErrorKind::Custom { message, .. } => ValidationErrorKind::Custom {
keyword: keyword.into(),
message,
},
other => other,
};
ValidationError::new(
LazyInstance::Ready(Cow::Borrowed(schema_value)),
kind,
Location::new(),
schema_path.clone(),
schema_path,
)
}
pub(crate) fn compile_error(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: LazyInstance<'a>,
message: impl Into<String>,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::Custom {
keyword: String::new(),
message: message.into(),
},
instance_path,
schema_path,
tracker,
)
}
}
impl error::Error for ValidationError<'_> {}
impl From<referencing::Error> for ValidationError<'_> {
#[inline]
fn from(err: referencing::Error) -> Self {
ValidationError::new(
LazyInstance::Ready(Cow::Owned(Value::Null)),
ValidationErrorKind::Referencing(err),
Location::new(),
Location::new(),
Location::new(),
)
}
}
impl From<FromUtf8Error> for ValidationError<'_> {
#[inline]
fn from(err: FromUtf8Error) -> Self {
ValidationError::from_utf8(err)
}
}
fn write_quoted_list(f: &mut Formatter<'_>, items: &[impl fmt::Display]) -> fmt::Result {
let mut iter = items.iter();
if let Some(item) = iter.next() {
f.write_char('\'')?;
write!(f, "{item}")?;
f.write_char('\'')?;
}
for item in iter {
f.write_str(", ")?;
f.write_char('\'')?;
write!(f, "{item}")?;
f.write_char('\'')?;
}
Ok(())
}
fn write_unexpected_suffix(f: &mut Formatter<'_>, len: usize) -> fmt::Result {
f.write_str(if len == 1 {
" was unexpected)"
} else {
" were unexpected)"
})
}
fn write_item_count(f: &mut Formatter<'_>, count: usize) -> fmt::Result {
write!(f, "{count} item{})", if count == 1 { "" } else { "s" })
}
const MAX_DISPLAYED_ENUM_VARIANTS: usize = 3;
fn write_enum_message(
f: &mut Formatter<'_>,
value: impl fmt::Display,
options: &Value,
) -> fmt::Result {
let array = options
.as_array()
.expect("Enum options must be a JSON array");
write!(f, "{value} is not one of ")?;
let total_count = array.len();
if total_count <= MAX_DISPLAYED_ENUM_VARIANTS {
for (i, option) in array.iter().enumerate() {
if i == 0 {
write!(f, "{option}")?;
} else if i == total_count - 1 {
write!(f, " or {option}")?;
} else {
write!(f, ", {option}")?;
}
}
} else {
let show_count = MAX_DISPLAYED_ENUM_VARIANTS - 1;
for (i, option) in array.iter().take(show_count).enumerate() {
if i == 0 {
write!(f, "{option}")?;
} else {
write!(f, ", {option}")?;
}
}
let remaining = total_count - show_count;
write!(f, " or {remaining} other candidates")?;
}
Ok(())
}
impl fmt::Display for ValidationError<'_> {
#[allow(clippy::too_many_lines)] fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self.kind() {
ValidationErrorKind::Referencing(error) => error.fmt(f),
ValidationErrorKind::BacktrackLimitExceeded { error } => error.fmt(f),
ValidationErrorKind::Format { format } => {
write!(f, r#"{} is not a "{}""#, self.instance(), format)
}
ValidationErrorKind::AdditionalItems { limit } => {
f.write_str("Additional items are not allowed (")?;
let array = self.instance().as_array().expect("Always valid");
let mut iter = array.iter().skip(*limit);
if let Some(item) = iter.next() {
write!(f, "{item}")?;
}
for item in iter {
f.write_str(", ")?;
write!(f, "{item}")?;
}
write_unexpected_suffix(f, array.len() - limit)
}
ValidationErrorKind::AdditionalProperties { unexpected } => {
f.write_str("Additional properties are not allowed (")?;
write_quoted_list(f, unexpected)?;
write_unexpected_suffix(f, unexpected.len())
}
ValidationErrorKind::AnyOf { context: _ } => write!(
f,
"{} is not valid under any of the schemas listed in the 'anyOf' keyword",
self.instance()
),
ValidationErrorKind::OneOfNotValid { context: _ } => write!(
f,
"{} is not valid under any of the schemas listed in the 'oneOf' keyword",
self.instance()
),
ValidationErrorKind::Contains => write!(
f,
"None of {} are valid under the given schema",
self.instance()
),
ValidationErrorKind::Constant { expected_value } => {
write!(f, "{expected_value} was expected")
}
ValidationErrorKind::ContentEncoding { content_encoding } => {
write!(
f,
r#"{} is not compliant with "{}" content encoding"#,
self.instance(),
content_encoding
)
}
ValidationErrorKind::ContentMediaType { content_media_type } => {
write!(
f,
r#"{} is not compliant with "{}" media type"#,
self.instance(),
content_media_type
)
}
ValidationErrorKind::FromUtf8 { error } => error.fmt(f),
ValidationErrorKind::Enum { options } => {
write_enum_message(f, self.instance(), options)
}
ValidationErrorKind::ExclusiveMaximum { limit } => write!(
f,
"{} is greater than or equal to the maximum of {}",
self.instance(),
limit
),
ValidationErrorKind::ExclusiveMinimum { limit } => write!(
f,
"{} is less than or equal to the minimum of {}",
self.instance(),
limit
),
ValidationErrorKind::FalseSchema => {
write!(f, "False schema does not allow {}", self.instance())
}
ValidationErrorKind::Maximum { limit } => write!(
f,
"{} is greater than the maximum of {}",
self.instance(),
limit
),
ValidationErrorKind::Minimum { limit } => {
write!(
f,
"{} is less than the minimum of {}",
self.instance(),
limit
)
}
ValidationErrorKind::MaxLength { limit } => write!(
f,
"{} is longer than {} character{}",
self.instance(),
limit,
if *limit == 1 { "" } else { "s" }
),
ValidationErrorKind::MinLength { limit } => write!(
f,
"{} is shorter than {} character{}",
self.instance(),
limit,
if *limit == 1 { "" } else { "s" }
),
ValidationErrorKind::MaxItems { limit } => write!(
f,
"{} has more than {} item{}",
self.instance(),
limit,
if *limit == 1 { "" } else { "s" }
),
ValidationErrorKind::MinItems { limit } => write!(
f,
"{} has less than {} item{}",
self.instance(),
limit,
if *limit == 1 { "" } else { "s" }
),
ValidationErrorKind::MaxProperties { limit } => write!(
f,
"{} has more than {} propert{}",
self.instance(),
limit,
if *limit == 1 { "y" } else { "ies" }
),
ValidationErrorKind::MinProperties { limit } => write!(
f,
"{} has less than {} propert{}",
self.instance(),
limit,
if *limit == 1 { "y" } else { "ies" }
),
ValidationErrorKind::Not { schema } => {
write!(f, "{} is not allowed for {}", schema, self.instance())
}
ValidationErrorKind::OneOfMultipleValid { .. } => write!(
f,
"{} is valid under more than one of the schemas listed in the 'oneOf' keyword",
self.instance()
),
ValidationErrorKind::Pattern { pattern } => {
write!(f, r#"{} does not match "{}""#, self.instance(), pattern)
}
ValidationErrorKind::PropertyNames { error } => error.fmt(f),
ValidationErrorKind::Required { property } => {
write!(f, "{property} is a required property")
}
ValidationErrorKind::MultipleOf { multiple_of } => {
write!(
f,
"{} is not a multiple of {}",
self.instance(),
multiple_of
)
}
ValidationErrorKind::UnevaluatedItems { unexpected } => {
f.write_str("Unevaluated items are not allowed (")?;
write_quoted_list(f, unexpected)?;
write_unexpected_suffix(f, unexpected.len())
}
ValidationErrorKind::UnevaluatedProperties { unexpected } => {
f.write_str("Unevaluated properties are not allowed (")?;
write_quoted_list(f, unexpected)?;
write_unexpected_suffix(f, unexpected.len())
}
ValidationErrorKind::UniqueItems => {
write!(f, "{} has non-unique elements", self.instance())
}
ValidationErrorKind::Type {
kind: TypeKind::Single(type_),
} => write!(f, r#"{} is not of type "{}""#, self.instance(), type_),
ValidationErrorKind::Type {
kind: TypeKind::Multiple(types),
} => {
write!(f, "{} is not of types ", self.instance())?;
let mut iter = types.iter();
if let Some(t) = iter.next() {
f.write_char('"')?;
write!(f, "{t}")?;
f.write_char('"')?;
}
for t in iter {
f.write_str(", ")?;
f.write_char('"')?;
write!(f, "{t}")?;
f.write_char('"')?;
}
Ok(())
}
ValidationErrorKind::Custom { message, .. }
| ValidationErrorKind::RegexEngineFailure { message } => f.write_str(message),
}
}
}
pub struct MaskedValidationError<'a, 'b, 'c> {
error: &'b ValidationError<'a>,
placeholder: Cow<'c, str>,
}
impl fmt::Display for MaskedValidationError<'_, '_, '_> {
#[allow(clippy::too_many_lines)]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self.error.kind() {
ValidationErrorKind::Referencing(error) => error.fmt(f),
ValidationErrorKind::BacktrackLimitExceeded { error } => error.fmt(f),
ValidationErrorKind::Format { format } => {
write!(f, r#"{} is not a "{format}""#, self.placeholder)
}
ValidationErrorKind::AdditionalItems { limit } => {
let extra = self
.error
.instance()
.as_array()
.map_or(0, |array| array.len().saturating_sub(*limit));
f.write_str("Additional items are not allowed (")?;
write_item_count(f, extra)
}
ValidationErrorKind::AdditionalProperties { unexpected } => {
f.write_str("Additional properties are not allowed (")?;
write_quoted_list(f, unexpected)?;
write_unexpected_suffix(f, unexpected.len())
}
ValidationErrorKind::AnyOf { .. } => write!(
f,
"{} is not valid under any of the schemas listed in the 'anyOf' keyword",
self.placeholder
),
ValidationErrorKind::OneOfNotValid { context: _ } => write!(
f,
"{} is not valid under any of the schemas listed in the 'oneOf' keyword",
self.placeholder
),
ValidationErrorKind::Contains => write!(
f,
"None of {} are valid under the given schema",
self.placeholder
),
ValidationErrorKind::Constant { expected_value } => {
write!(f, "{expected_value} was expected")
}
ValidationErrorKind::ContentEncoding { content_encoding } => {
write!(
f,
r#"{} is not compliant with "{}" content encoding"#,
self.placeholder, content_encoding
)
}
ValidationErrorKind::ContentMediaType { content_media_type } => {
write!(
f,
r#"{} is not compliant with "{}" media type"#,
self.placeholder, content_media_type
)
}
ValidationErrorKind::FromUtf8 { error } => error.fmt(f),
ValidationErrorKind::Enum { options } => {
write_enum_message(f, &self.placeholder, options)
}
ValidationErrorKind::ExclusiveMaximum { limit } => write!(
f,
"{} is greater than or equal to the maximum of {}",
self.placeholder, limit
),
ValidationErrorKind::ExclusiveMinimum { limit } => write!(
f,
"{} is less than or equal to the minimum of {}",
self.placeholder, limit
),
ValidationErrorKind::FalseSchema => {
write!(f, "False schema does not allow {}", self.placeholder)
}
ValidationErrorKind::Maximum { limit } => write!(
f,
"{} is greater than the maximum of {}",
self.placeholder, limit
),
ValidationErrorKind::Minimum { limit } => {
write!(
f,
"{} is less than the minimum of {}",
self.placeholder, limit
)
}
ValidationErrorKind::MaxLength { limit } => write!(
f,
"{} is longer than {} character{}",
self.placeholder,
limit,
if *limit == 1 { "" } else { "s" }
),
ValidationErrorKind::MinLength { limit } => write!(
f,
"{} is shorter than {} character{}",
self.placeholder,
limit,
if *limit == 1 { "" } else { "s" }
),
ValidationErrorKind::MaxItems { limit } => write!(
f,
"{} has more than {} item{}",
self.placeholder,
limit,
if *limit == 1 { "" } else { "s" }
),
ValidationErrorKind::MinItems { limit } => write!(
f,
"{} has less than {} item{}",
self.placeholder,
limit,
if *limit == 1 { "" } else { "s" }
),
ValidationErrorKind::MaxProperties { limit } => write!(
f,
"{} has more than {} propert{}",
self.placeholder,
limit,
if *limit == 1 { "y" } else { "ies" }
),
ValidationErrorKind::MinProperties { limit } => write!(
f,
"{} has less than {} propert{}",
self.placeholder,
limit,
if *limit == 1 { "y" } else { "ies" }
),
ValidationErrorKind::Not { schema } => {
write!(f, "{} is not allowed for {}", schema, self.placeholder)
}
ValidationErrorKind::OneOfMultipleValid { .. } => write!(
f,
"{} is valid under more than one of the schemas listed in the 'oneOf' keyword",
self.placeholder
),
ValidationErrorKind::Pattern { pattern } => {
write!(f, r#"{} does not match "{}""#, self.placeholder, pattern)
}
ValidationErrorKind::PropertyNames { error } => error.fmt(f),
ValidationErrorKind::Required { property } => {
write!(f, "{property} is a required property")
}
ValidationErrorKind::MultipleOf { multiple_of } => {
write!(
f,
"{} is not a multiple of {}",
self.placeholder, multiple_of
)
}
ValidationErrorKind::UnevaluatedItems { unexpected } => {
f.write_str("Unevaluated items are not allowed (")?;
write_item_count(f, unexpected.len())
}
ValidationErrorKind::UnevaluatedProperties { unexpected } => {
f.write_str("Unevaluated properties are not allowed (")?;
write_quoted_list(f, unexpected)?;
write_unexpected_suffix(f, unexpected.len())
}
ValidationErrorKind::UniqueItems => {
write!(f, "{} has non-unique elements", self.placeholder)
}
ValidationErrorKind::Type {
kind: TypeKind::Single(type_),
} => write!(f, r#"{} is not of type "{}""#, self.placeholder, type_),
ValidationErrorKind::Type {
kind: TypeKind::Multiple(types),
} => {
write!(f, "{} is not of types ", self.placeholder)?;
let mut iter = types.iter();
if let Some(t) = iter.next() {
f.write_char('"')?;
write!(f, "{t}")?;
f.write_char('"')?;
}
for t in iter {
f.write_str(", ")?;
f.write_char('"')?;
write!(f, "{t}")?;
f.write_char('"')?;
}
Ok(())
}
ValidationErrorKind::Custom { message, .. }
| ValidationErrorKind::RegexEngineFailure { message } => f.write_str(message),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use referencing::{Registry, Resource};
use serde_json::json;
use test_case::test_case;
const _: fn() = || {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<ValidationError<'static>>();
};
fn owned_error(instance: Value, kind: ValidationErrorKind) -> ValidationError<'static> {
ValidationError::new(
LazyInstance::Ready(Cow::Owned(instance)),
kind,
Location::new(),
Location::new(),
Location::new(),
)
}
#[test]
fn test_validation_errors_display_includes_paths() {
let schema = json!({
"type": "object",
"properties": {"age": {"type": "number"}},
"required": ["name"]
});
let validator = crate::validator_for(&schema).expect("valid schema");
let instance = json!({"age": "oops"});
let errors = validator.iter_errors(&instance).into_errors();
assert_eq!(
errors.to_string(),
"Validation errors:\n\
01: \"name\" is a required property\n\
02: /age: \"oops\" is not of type \"number\"\n"
);
}
#[test]
fn error_iterator_into_errors_collects_all_errors() {
let iterator = ErrorIterator::from_iterator(
vec![
owned_error(json!(1), ValidationErrorKind::Minimum { limit: json!(2) }),
owned_error(json!(3), ValidationErrorKind::Maximum { limit: json!(2) }),
]
.into_iter(),
);
let validation_errors = iterator.into_errors();
let collected: Vec<_> = validation_errors.into_iter().collect();
assert_eq!(collected.len(), 2);
assert_eq!(collected[0].to_string(), "1 is less than the minimum of 2");
assert_eq!(
collected[1].to_string(),
"3 is greater than the maximum of 2"
);
}
#[test]
fn validation_errors_display_reports_success() {
let errors = ValidationErrors { errors: Vec::new() };
assert_eq!(format!("{errors}"), "Validation succeeded");
}
#[test]
fn validation_errors_display_lists_messages() {
let errors = ValidationErrors {
errors: vec![
owned_error(json!(1), ValidationErrorKind::Minimum { limit: json!(2) }),
owned_error(json!(3), ValidationErrorKind::Maximum { limit: json!(2) }),
],
};
let rendered = format!("{errors}");
assert!(rendered.contains("Validation errors:"));
assert!(rendered.contains("01: 1 is less than the minimum of 2"));
assert!(rendered.contains("02: 3 is greater than the maximum of 2"));
}
#[test]
fn validation_errors_len_and_is_empty() {
let empty = ValidationErrors { errors: vec![] };
assert_eq!(empty.len(), 0);
assert!(empty.is_empty());
let errors = ValidationErrors {
errors: vec![owned_error(
json!(1),
ValidationErrorKind::Minimum { limit: json!(2) },
)],
};
assert_eq!(errors.len(), 1);
assert!(!errors.is_empty());
}
#[test]
fn validation_errors_as_slice() {
let errors = ValidationErrors {
errors: vec![
owned_error(json!(1), ValidationErrorKind::Minimum { limit: json!(2) }),
owned_error(json!(3), ValidationErrorKind::Maximum { limit: json!(2) }),
],
};
let slice = errors.as_slice();
assert_eq!(slice.len(), 2);
assert_eq!(slice[0].to_string(), "1 is less than the minimum of 2");
assert_eq!(slice[1].to_string(), "3 is greater than the maximum of 2");
}
#[test]
fn validation_errors_iter() {
let errors = ValidationErrors {
errors: vec![
owned_error(json!(1), ValidationErrorKind::Minimum { limit: json!(2) }),
owned_error(json!(3), ValidationErrorKind::Maximum { limit: json!(2) }),
],
};
let collected: Vec<_> = errors.iter().map(ValidationError::to_string).collect();
assert_eq!(collected.len(), 2);
assert_eq!(collected[0], "1 is less than the minimum of 2");
assert_eq!(collected[1], "3 is greater than the maximum of 2");
}
#[test]
#[allow(clippy::explicit_iter_loop)]
fn validation_errors_iter_mut() {
let mut errors = ValidationErrors {
errors: vec![owned_error(
json!(1),
ValidationErrorKind::Minimum { limit: json!(2) },
)],
};
for error in errors.iter_mut() {
let _ = error.to_string();
}
}
#[test]
fn validation_errors_into_iterator_by_ref() {
let errors = ValidationErrors {
errors: vec![owned_error(
json!(1),
ValidationErrorKind::Minimum { limit: json!(2) },
)],
};
let collected: Vec<_> = (&errors).into_iter().collect();
assert_eq!(collected.len(), 1);
assert_eq!(errors.len(), 1);
}
#[test]
fn validation_errors_into_iterator_by_mut_ref() {
let mut errors = ValidationErrors {
errors: vec![owned_error(
json!(1),
ValidationErrorKind::Minimum { limit: json!(2) },
)],
};
let collected: Vec<_> = (&mut errors).into_iter().collect();
assert_eq!(collected.len(), 1);
assert_eq!(errors.len(), 1);
}
#[test]
fn error_iterator_size_hint() {
let vec = vec![
owned_error(json!(1), ValidationErrorKind::Minimum { limit: json!(2) }),
owned_error(json!(3), ValidationErrorKind::Maximum { limit: json!(2) }),
];
let iterator = ErrorIterator::from_iterator(vec.into_iter());
let (lower, upper) = iterator.size_hint();
assert_eq!(lower, 2);
assert_eq!(upper, Some(2));
}
#[test]
fn validation_errors_debug() {
let errors = ValidationErrors {
errors: vec![owned_error(
json!(1),
ValidationErrorKind::Minimum { limit: json!(2) },
)],
};
let debug_output = format!("{errors:?}");
assert!(debug_output.contains("ValidationErrors"));
assert!(debug_output.contains("errors"));
}
#[test]
fn single_type_error() {
let instance = json!(42);
let err = ValidationError::single_type_error(
Location::new(),
Location::new(),
Location::new(),
LazyInstance::Ready(Cow::Borrowed(&instance)),
JsonType::String,
);
assert_eq!(err.to_string(), r#"42 is not of type "string""#);
}
#[test]
fn multiple_types_error() {
let instance = json!(42);
let types = JsonTypeSet::from(JsonType::String).insert(JsonType::Number);
let err = ValidationError::multiple_type_error(
Location::new(),
Location::new(),
Location::new(),
LazyInstance::Ready(Cow::Borrowed(&instance)),
types,
);
assert_eq!(err.to_string(), r#"42 is not of types "number", "string""#);
}
#[test_case(true, &json!({"foo": {"bar": 42}}), "/foo/bar")]
#[test_case(true, &json!({"foo": "a"}), "/foo")]
#[test_case(false, &json!({"foo": {"bar": 42}}), "/foo/bar")]
#[test_case(false, &json!({"foo": "a"}), "/foo")]
fn instance_path_properties(additional_properties: bool, instance: &Value, expected: &str) {
let schema = json!(
{
"additionalProperties": additional_properties,
"type":"object",
"properties":{
"foo":{
"type":"object",
"properties":{
"bar":{
"type":"string"
}
}
}
}
}
);
let validator = crate::validator_for(&schema).unwrap();
let mut result = validator.iter_errors(instance);
let error = result.next().expect("validation error");
assert!(result.next().is_none());
assert_eq!(error.instance_path().as_str(), expected);
}
#[test_case(true, &json!([1, {"foo": ["42"]}]), "/0")]
#[test_case(true, &json!(["a", {"foo": [42]}]), "/1/foo/0")]
#[test_case(false, &json!([1, {"foo": ["42"]}]), "/0")]
#[test_case(false, &json!(["a", {"foo": [42]}]), "/1/foo/0")]
fn instance_path_properties_and_arrays(
additional_items: bool,
instance: &Value,
expected: &str,
) {
let schema = json!(
{
"items": additional_items,
"type": "array",
"prefixItems": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"foo": {
"type": "array",
"prefixItems": [
{
"type": "string"
}
]
}
}
}
]
}
);
let validator = crate::validator_for(&schema).unwrap();
let mut result = validator.iter_errors(instance);
let error = result.next().expect("validation error");
assert!(result.next().is_none());
assert_eq!(error.instance_path().as_str(), expected);
}
#[test_case(true, &json!([[1, 2, 3], [4, "5", 6], [7, 8, 9]]), "/1/1")]
#[test_case(false, &json!([[1, 2, 3], [4, "5", 6], [7, 8, 9]]), "/1/1")]
#[test_case(true, &json!([[1, 2, 3], [4, 5, 6], 42]), "/2")]
#[test_case(false, &json!([[1, 2, 3], [4, 5, 6], 42]), "/2")]
fn instance_path_nested_arrays(additional_items: bool, instance: &Value, expected: &str) {
let schema = json!(
{
"additionalItems": additional_items,
"type": "array",
"items": {
"type": "array",
"items": {
"type": "integer"
}
}
}
);
let validator = crate::validator_for(&schema).unwrap();
let mut result = validator.iter_errors(instance);
let error = result.next().expect("validation error");
assert!(result.next().is_none());
assert_eq!(error.instance_path().as_str(), expected);
}
#[test_case(true, &json!([1, "a"]), "/1")]
#[test_case(false, &json!([1, "a"]), "/1")]
#[test_case(true, &json!(123), "")]
#[test_case(false, &json!(123), "")]
fn instance_path_arrays(additional_items: bool, instance: &Value, expected: &str) {
let schema = json!(
{
"additionalItems": additional_items,
"type": "array",
"items": {
"type": "integer"
}
}
);
let validator = crate::validator_for(&schema).unwrap();
let mut result = validator.iter_errors(instance);
let error = result.next().expect("validation error");
assert!(result.next().is_none());
assert_eq!(error.instance_path().as_str(), expected);
}
#[test_case(&json!({"anyOf": [{"type": "string"}, {"type": "boolean"}]}), &json!(1), "anyOf", "[REDACTED] is not valid under any of the schemas listed in the 'anyOf' keyword"; "any of")]
#[test_case(&json!({"oneOf": [{"type": "string"}, {"type": "boolean"}]}), &json!(1), "oneOf", "[REDACTED] is not valid under any of the schemas listed in the 'oneOf' keyword"; "one of none valid")]
#[test_case(&json!({"oneOf": [{"type": "integer"}, {"minimum": 0}]}), &json!(1), "oneOf", "[REDACTED] is valid under more than one of the schemas listed in the 'oneOf' keyword"; "one of multiple valid")]
#[test_case(&json!({"contains": {"type": "string"}}), &json!([1]), "contains", "None of [REDACTED] are valid under the given schema"; "contains")]
#[test_case(&json!({"const": "secret"}), &json!("x"), "const", "\"secret\" was expected"; "constant")]
#[test_case(&json!({"$schema": "http://json-schema.org/draft-07/schema#", "contentEncoding": "base64"}), &json!("not base64!"), "contentEncoding", "[REDACTED] is not compliant with \"base64\" content encoding"; "content encoding")]
#[test_case(&json!({"$schema": "http://json-schema.org/draft-07/schema#", "contentMediaType": "application/json"}), &json!("{"), "contentMediaType", "[REDACTED] is not compliant with \"application/json\" media type"; "content media type")]
#[test_case(&json!({"$schema": "http://json-schema.org/draft-07/schema#", "contentMediaType": "application/json", "contentEncoding": "base64"}), &json!("/w=="), "contentEncoding", "invalid utf-8 sequence of 1 bytes from index 0"; "decoded content is not utf-8")]
#[test_case(&json!({"enum": ["a", "b"]}), &json!("c"), "enum", "[REDACTED] is not one of \"a\" or \"b\""; "enumeration")]
#[test_case(&json!({"exclusiveMaximum": 1}), &json!(1), "exclusiveMaximum", "[REDACTED] is greater than or equal to the maximum of 1"; "exclusive maximum")]
#[test_case(&json!({"exclusiveMinimum": 1}), &json!(1), "exclusiveMinimum", "[REDACTED] is less than or equal to the minimum of 1"; "exclusive minimum")]
#[test_case(&json!(false), &json!(1), "falseSchema", "False schema does not allow [REDACTED]"; "false schema")]
#[test_case(&json!({"maximum": 1}), &json!(2), "maximum", "[REDACTED] is greater than the maximum of 1"; "maximum")]
#[test_case(&json!({"maxLength": 1}), &json!("ab"), "maxLength", "[REDACTED] is longer than 1 character"; "max length singular")]
#[test_case(&json!({"minLength": 1}), &json!(""), "minLength", "[REDACTED] is shorter than 1 character"; "min length singular")]
#[test_case(&json!({"minLength": 2}), &json!("a"), "minLength", "[REDACTED] is shorter than 2 characters"; "min length plural")]
#[test_case(&json!({"maxItems": 1}), &json!([1, 2]), "maxItems", "[REDACTED] has more than 1 item"; "max items singular")]
#[test_case(&json!({"maxItems": 0}), &json!([1]), "maxItems", "[REDACTED] has more than 0 items"; "max items plural")]
#[test_case(&json!({"minItems": 1}), &json!([]), "minItems", "[REDACTED] has less than 1 item"; "min items singular")]
#[test_case(&json!({"minItems": 2}), &json!([1]), "minItems", "[REDACTED] has less than 2 items"; "min items plural")]
#[test_case(&json!({"maxProperties": 1}), &json!({"a": 1, "b": 2}), "maxProperties", "[REDACTED] has more than 1 property"; "max properties singular")]
#[test_case(&json!({"maxProperties": 0}), &json!({"a": 1}), "maxProperties", "[REDACTED] has more than 0 properties"; "max properties plural")]
#[test_case(&json!({"minProperties": 1}), &json!({}), "minProperties", "[REDACTED] has less than 1 property"; "min properties singular")]
#[test_case(&json!({"minProperties": 2}), &json!({"a": 1}), "minProperties", "[REDACTED] has less than 2 properties"; "min properties plural")]
#[test_case(&json!({"not": {"type": "integer"}}), &json!(1), "not", "{\"type\":\"integer\"} is not allowed for [REDACTED]"; "not")]
#[test_case(&json!({"propertyNames": {"maxLength": 1}}), &json!({"ab": 1}), "propertyNames", "\"ab\" is longer than 1 character"; "property names")]
#[test_case(&json!({"required": ["a"]}), &json!({}), "required", "\"a\" is a required property"; "required")]
#[test_case(&json!({"multipleOf": 2}), &json!(3), "multipleOf", "[REDACTED] is not a multiple of 2"; "multiple of")]
#[test_case(&json!({"properties": {"a": {}}, "unevaluatedProperties": false}), &json!({"a": 1, "b": 2}), "unevaluatedProperties", "Unevaluated properties are not allowed ('b' was unexpected)"; "unevaluated properties")]
#[test_case(&json!({"type": ["string", "null"]}), &json!(1), "type", "[REDACTED] is not of types \"null\", \"string\""; "multiple types")]
#[test_case(&json!({"$schema": "http://json-schema.org/draft-04/schema#", "type": ["string", "null"]}), &json!(1), "type", "[REDACTED] is not of types \"null\", \"string\""; "multiple types draft 4")]
fn masked_validation_messages(schema: &Value, instance: &Value, keyword: &str, expected: &str) {
let validator = crate::validator_for(schema).expect("Invalid schema");
let error = validator.validate(instance).expect_err("Should fail");
assert_eq!(
(
error.kind().keyword(),
error.masked_with("[REDACTED]").to_string()
),
(keyword, expected.to_string())
);
}
#[test]
fn masked_unresolvable_reference() {
let error = crate::validator_for(&json!({"$ref": "#/missing"})).expect_err("Should fail");
assert_eq!(
(error.kind().keyword(), error.masked().to_string()),
("$ref", "Pointer '/missing' does not exist".to_string())
);
}
#[test]
fn masked_backtrack_limit_exceeded() {
let validator = crate::options()
.with_pattern_options(crate::PatternOptions::fancy_regex().backtrack_limit(1))
.build(&json!({"pattern": "(?<=ab)c"}))
.expect("Invalid schema");
let instance = json!("abc");
let error = validator.validate(&instance).expect_err("Should fail");
assert_eq!(
(error.kind().keyword(), error.masked().to_string()),
(
"pattern",
"Error executing regex: Max limit for backtracking count exceeded".to_string()
)
);
}
#[test_case(
json!("2023-13-45"),
ValidationErrorKind::Format {
format: "date".to_string(),
},
"value is not a \"date\""
)]
#[test_case(
json!("sensitive data"),
ValidationErrorKind::MaxLength { limit: 5 },
"value is longer than 5 characters"
)]
#[test_case(
json!({"secret": "data", "key": "value"}),
ValidationErrorKind::AdditionalProperties {
unexpected: vec!["secret".to_string(), "key".to_string()]
},
"Additional properties are not allowed ('secret', 'key' were unexpected)"
)]
#[test_case(
json!(123),
ValidationErrorKind::Minimum { limit: json!(456) },
"value is less than the minimum of 456"
)]
#[test_case(
json!("secret_key_123"),
ValidationErrorKind::Pattern {
pattern: "^[A-Z0-9]{32}$".to_string(),
},
"value does not match \"^[A-Z0-9]{32}$\""
)]
#[test_case(
json!([1, 2, 2, 3]),
ValidationErrorKind::UniqueItems,
"value has non-unique elements"
)]
#[test_case(
json!(123),
ValidationErrorKind::Type { kind: TypeKind::Single(JsonType::String) },
"value is not of type \"string\""
)]
fn test_masked_error_messages(instance: Value, kind: ValidationErrorKind, expected: &str) {
let error = ValidationError::new(
LazyInstance::Ready(Cow::Owned(instance)),
kind,
Location::new(),
Location::new(),
Location::new(),
);
assert_eq!(error.masked().to_string(), expected);
}
#[test_case(
json!("sensitive data"),
ValidationErrorKind::MaxLength { limit: 5 },
"[REDACTED]",
"[REDACTED] is longer than 5 characters"
)]
#[test_case(
json!({"password": "secret123"}),
ValidationErrorKind::Type {
kind: TypeKind::Single(JsonType::String)
},
"***",
"*** is not of type \"string\""
)]
fn test_custom_masked_error_messages(
instance: Value,
kind: ValidationErrorKind,
placeholder: &str,
expected: &str,
) {
let error = ValidationError::new(
LazyInstance::Ready(Cow::Owned(instance)),
kind,
Location::new(),
Location::new(),
Location::new(),
);
assert_eq!(error.masked_with(placeholder).to_string(), expected);
}
fn single_error_messages(
draft: referencing::Draft,
schema: &Value,
instance: &Value,
) -> (String, String, String) {
let validator = crate::options()
.with_draft(draft)
.build(schema)
.expect("schema compiles");
let mut errors = validator.iter_errors(instance);
let error = errors.next().expect("validation error");
assert!(errors.next().is_none());
(
error.to_string(),
error.masked().to_string(),
error.masked_with("***").to_string(),
)
}
#[test_case(referencing::Draft::Draft4, &json!([1, 2]), "Additional items are not allowed (2 was unexpected)", "Additional items are not allowed (1 item)"; "draft4 one extra")]
#[test_case(referencing::Draft::Draft4, &json!([1, 2, 3]), "Additional items are not allowed (2, 3 were unexpected)", "Additional items are not allowed (2 items)"; "draft4 two extra")]
#[test_case(referencing::Draft::Draft6, &json!([1, 2]), "Additional items are not allowed (2 was unexpected)", "Additional items are not allowed (1 item)"; "draft6 one extra")]
#[test_case(referencing::Draft::Draft6, &json!([1, 2, 3]), "Additional items are not allowed (2, 3 were unexpected)", "Additional items are not allowed (2 items)"; "draft6 two extra")]
#[test_case(referencing::Draft::Draft7, &json!([1, 2]), "Additional items are not allowed (2 was unexpected)", "Additional items are not allowed (1 item)"; "draft7 one extra")]
#[test_case(referencing::Draft::Draft7, &json!([1, 2, 3]), "Additional items are not allowed (2, 3 were unexpected)", "Additional items are not allowed (2 items)"; "draft7 two extra")]
#[test_case(referencing::Draft::Draft7, &json!([1, "a", null, 4]), "Additional items are not allowed (\"a\", null, 4 were unexpected)", "Additional items are not allowed (3 items)"; "draft7 three extra")]
#[test_case(referencing::Draft::Draft201909, &json!([1, 2]), "Additional items are not allowed (2 was unexpected)", "Additional items are not allowed (1 item)"; "draft2019 one extra")]
#[test_case(referencing::Draft::Draft201909, &json!([1, 2, 3]), "Additional items are not allowed (2, 3 were unexpected)", "Additional items are not allowed (2 items)"; "draft2019 two extra")]
fn masked_additional_items_counts_extra_items(
draft: referencing::Draft,
instance: &Value,
expected: &str,
expected_masked: &str,
) {
let schema = json!({"items": [{}], "additionalItems": false});
let (message, masked, masked_with) = single_error_messages(draft, &schema, instance);
assert_eq!(message, expected);
assert_eq!(masked, expected_masked);
assert_eq!(masked_with, expected_masked);
}
#[test_case(&json!([1, 2]), "Unevaluated items are not allowed ('2' was unexpected)", "Unevaluated items are not allowed (1 item)"; "one extra")]
#[test_case(&json!([1, 2, 3]), "Unevaluated items are not allowed ('2', '3' were unexpected)", "Unevaluated items are not allowed (2 items)"; "two extra")]
fn masked_unevaluated_items_pluralises_count(
instance: &Value,
expected: &str,
expected_masked: &str,
) {
let schema = json!({"prefixItems": [{}], "unevaluatedItems": false});
let (message, masked, masked_with) =
single_error_messages(referencing::Draft::Draft202012, &schema, instance);
assert_eq!(message, expected);
assert_eq!(masked, expected_masked);
assert_eq!(masked_with, expected_masked);
}
#[test]
fn test_absolute_keyword_location_absent_for_schema_without_base_uri() {
let schema = serde_json::json!({"type": "string"});
let instance = serde_json::json!(42);
let err = crate::validate(&schema, &instance).unwrap_err();
assert!(err.absolute_keyword_location().is_none());
}
#[test]
fn test_absolute_keyword_location_present_for_schema_with_base_uri() {
let schema = serde_json::json!({
"$id": "https://example.com/schema.json",
"type": "string"
});
let instance = serde_json::json!(42);
let validator = crate::options()
.build(&schema)
.expect("schema should compile");
let err = validator.validate(&instance).unwrap_err();
let absolute_location = err
.absolute_keyword_location()
.expect("should have absolute keyword location");
assert_eq!(
absolute_location.as_str(),
"https://example.com/schema.json#/type"
);
}
#[test]
fn test_absolute_keyword_location_from_iter_errors() {
let schema = serde_json::json!({
"$id": "https://example.com/schema.json",
"type": "string"
});
let instance = serde_json::json!(42);
let validator = crate::options()
.build(&schema)
.expect("schema should compile");
let errs: Vec<_> = validator.iter_errors(&instance).collect();
assert_eq!(errs.len(), 1);
let absolute_location = errs[0]
.absolute_keyword_location()
.expect("iter_errors should set absolute keyword location");
assert_eq!(
absolute_location.as_str(),
"https://example.com/schema.json#/type"
);
}
#[test]
fn test_absolute_keyword_location_false_schema() {
let schema = serde_json::json!(false);
let instance = serde_json::json!(42);
let validator = crate::options()
.with_base_uri("https://example.com/schema.json")
.build(&schema)
.expect("schema should compile");
let err = validator.validate(&instance).unwrap_err();
let absolute_location = err
.absolute_keyword_location()
.expect("false schema should have absolute keyword location");
assert_eq!(
absolute_location.as_str(),
"https://example.com/schema.json#"
);
}
#[test]
fn test_absolute_keyword_location_preserved_by_to_owned() {
let schema = serde_json::json!({
"$id": "https://example.com/schema.json",
"type": "string"
});
let instance = serde_json::json!(42);
let validator = crate::options()
.build(&schema)
.expect("schema should compile");
let err = validator.validate(&instance).unwrap_err();
let owned = err.to_owned();
let absolute_location = owned
.absolute_keyword_location()
.expect("to_owned should preserve absolute keyword location");
assert_eq!(
absolute_location.as_str(),
"https://example.com/schema.json#/type"
);
}
#[test]
fn test_absolute_keyword_location_external_ref() {
let external = serde_json::json!({
"$id": "https://example.com/string.json",
"type": "string"
});
let schema = serde_json::json!({
"$id": "https://example.com/root.json",
"$ref": "https://example.com/string.json"
});
let instance = serde_json::json!(42);
let registry = Registry::new()
.add(
"https://example.com/string.json",
Resource::from_contents(external),
)
.expect("external schema should be accepted")
.prepare()
.expect("registry should build");
let validator = crate::options()
.with_registry(®istry)
.build(&schema)
.expect("schema should compile");
let err = validator.validate(&instance).unwrap_err();
let absolute_location = err
.absolute_keyword_location()
.expect("external ref error should have absolute keyword location");
assert_eq!(
absolute_location.as_str(),
"https://example.com/string.json#/type"
);
}
}