Skip to main content

ferrin_schema/
validation.rs

1//! Dynamic JSON Schema validation.
2//!
3//! [`ValidationIssue`] / [`ValidationIssues`] are always available so custom
4//! validators can report structured problems. The [`Validator`] that checks a
5//! value against a raw JSON Schema requires the `json-schema-validation`
6//! feature.
7
8use std::fmt;
9
10#[cfg(feature = "json-schema-validation")]
11use serde_json::Value;
12
13/// One validation problem.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct ValidationIssue {
16    /// JSON pointer to the offending value (empty for the root).
17    pub path: String,
18    /// Human-readable message.
19    pub message: String,
20}
21
22/// A set of validation problems, usable as an error cause.
23#[derive(Debug, Clone, PartialEq, Eq, Default)]
24pub struct ValidationIssues {
25    /// The problems, in schema evaluation order.
26    pub issues: Vec<ValidationIssue>,
27}
28
29impl ValidationIssues {
30    /// Creates a set from issues.
31    #[must_use]
32    pub fn new(issues: Vec<ValidationIssue>) -> Self {
33        Self { issues }
34    }
35
36    /// Creates a set with a single root-level message.
37    #[must_use]
38    pub fn message(message: impl Into<String>) -> Self {
39        Self {
40            issues: vec![ValidationIssue {
41                path: String::new(),
42                message: message.into(),
43            }],
44        }
45    }
46
47    /// Returns `true` when there are no issues.
48    #[must_use]
49    pub fn is_empty(&self) -> bool {
50        self.issues.is_empty()
51    }
52}
53
54impl fmt::Display for ValidationIssues {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        match self.issues.as_slice() {
57            [] => f.write_str("validation failed"),
58            [single] if single.path.is_empty() => f.write_str(&single.message),
59            [single] => write!(f, "{}: {}", single.path, single.message),
60            many => {
61                write!(f, "{} validation issues:", many.len())?;
62                for issue in many {
63                    if issue.path.is_empty() {
64                        write!(f, " [{}]", issue.message)?;
65                    } else {
66                        write!(f, " [{}: {}]", issue.path, issue.message)?;
67                    }
68                }
69                Ok(())
70            }
71        }
72    }
73}
74
75impl std::error::Error for ValidationIssues {}
76
77/// A compiled JSON Schema validator using the declared dialect.
78#[cfg(feature = "json-schema-validation")]
79#[derive(Debug)]
80pub struct Validator {
81    inner: jsonschema::Validator,
82}
83
84#[cfg(feature = "json-schema-validation")]
85impl Validator {
86    /// Compiles `schema` (draft-07 unless the schema declares another draft).
87    ///
88    /// # Errors
89    ///
90    /// Returns [`SchemaError::InvalidSchema`](crate::SchemaError::InvalidSchema)
91    /// when the schema is not a valid JSON Schema.
92    pub fn compile(schema: &Value) -> Result<Self, crate::SchemaError> {
93        let options = if schema.get("$schema").is_some() {
94            jsonschema::options()
95        } else {
96            jsonschema::draft7::options()
97        };
98        let inner = options
99            .build(schema)
100            .map_err(|error| crate::SchemaError::InvalidSchema {
101                message: error.to_string(),
102            })?;
103        Ok(Self { inner })
104    }
105
106    /// Returns `true` when `value` satisfies the schema.
107    #[must_use]
108    pub fn is_valid(&self, value: &Value) -> bool {
109        self.inner.is_valid(value)
110    }
111
112    /// Collects every violation of `value` against the schema.
113    #[must_use]
114    pub fn issues(&self, value: &Value) -> ValidationIssues {
115        let issues = self
116            .inner
117            .iter_errors(value)
118            .map(|error| ValidationIssue {
119                path: error.instance_path().to_string(),
120                message: error.to_string(),
121            })
122            .collect();
123        ValidationIssues::new(issues)
124    }
125
126    /// Validates `value`, returning all issues on failure.
127    ///
128    /// # Errors
129    ///
130    /// Returns the collected [`ValidationIssues`] when `value` violates the
131    /// schema.
132    pub fn validate(&self, value: &Value) -> Result<(), ValidationIssues> {
133        let issues = self.issues(value);
134        if issues.is_empty() {
135            Ok(())
136        } else {
137            Err(issues)
138        }
139    }
140}