use crate::{
paths::{LazyLocation, Location},
types::{JsonType, JsonTypeSet},
validator::LazyEvaluationPath,
};
use referencing::Uri;
use serde_json::{Map, Number, Value};
use std::{
borrow::Cow,
error,
fmt::{self, Formatter, Write},
iter::{empty, once},
slice,
string::FromUtf8Error,
sync::Arc,
vec,
};
#[derive(Debug)]
pub struct ValidationError<'a> {
repr: Box<ValidationErrorRepr<'a>>,
}
struct ValidationErrorRepr<'a> {
instance: Cow<'a, Value>,
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() {
writeln!(f, "{:02}: {error}", idx + 1)?;
}
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()
}
}
pub(crate) fn no_error<'a>() -> ErrorIterator<'a> {
ErrorIterator::from_iterator(empty())
}
pub(crate) fn error(instance: ValidationError) -> ErrorIterator {
ErrorIterator::from_iterator(once(instance))
}
#[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 },
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::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: Cow<'a, Value>,
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
}
#[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
}
#[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,
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: &'a Value,
kind: ValidationErrorKind,
instance_path: Location,
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
) -> Self {
Self::new(
Cow::Borrowed(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(
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 additional_items(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: &'a Value,
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: &'a Value,
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: &'a Value,
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: &'a Value,
error: fancy_regex::Error,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::BacktrackLimitExceeded { error },
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn constant_array(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: &'a Value,
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: &'a Value,
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: &'a Value,
) -> 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: &'a Value,
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: &'a Value,
expected_value: &Map<String, Value>,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::Constant {
expected_value: Value::Object(expected_value.clone()),
},
instance_path,
schema_path,
tracker,
)
}
pub(crate) fn constant_string(
schema_path: Location,
tracker: impl Into<LazyEvaluationPath>,
instance_path: Location,
instance: &'a Value,
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: &'a Value,
) -> 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: &'a Value,
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: &'a Value,
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: &'a Value,
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: &'a Value,
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: &'a Value,
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: &'a Value,
) -> 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: &'a Value,
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(
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: &'a Value,
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: &'a Value,
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: &'a Value,
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: &'a Value,
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: &'a Value,
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: &'a Value,
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: &'a Value,
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: &'a Value,
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: &'a Value,
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: &'a Value,
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: &'a Value,
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: &'a Value,
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: &'a Value,
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: &'a Value,
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: &'a Value,
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: &'a Value,
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: &'a Value,
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: &'a Value,
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: &'a Value,
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: &'a Value,
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: &'a Value,
) -> ValidationError<'a> {
Self::borrowed(
instance,
ValidationErrorKind::UniqueItems,
instance_path,
schema_path,
tracker,
)
}
pub fn custom(message: impl Into<String>) -> ValidationError<'static> {
ValidationError::new(
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(
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(
Cow::Borrowed(instance),
kind,
instance_path.into(),
schema_path.clone(),
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(
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: &'a Value,
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(
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)"
})
}
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, .. } => 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 } => {
write!(f, "Additional items are not allowed ({limit} items)")
}
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 } => {
write!(
f,
"Unevaluated items are not allowed ({} items)",
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, .. } => f.write_str(message),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use referencing::{Registry, Resource};
use serde_json::json;
use test_case::test_case;
fn owned_error(instance: Value, kind: ValidationErrorKind) -> ValidationError<'static> {
ValidationError::new(
Cow::Owned(instance),
kind,
Location::new(),
Location::new(),
Location::new(),
)
}
#[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(),
&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::empty()
.insert(JsonType::String)
.insert(JsonType::Number);
let err = ValidationError::multiple_type_error(
Location::new(),
Location::new(),
Location::new(),
&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!("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(
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(
Cow::Owned(instance),
kind,
Location::new(),
Location::new(),
Location::new(),
);
assert_eq!(error.masked_with(placeholder).to_string(), expected);
}
#[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"
);
}
}