Skip to main content

corvus_json_schema/
lib.rs

1//! A high-performance JSON Schema evaluator (draft 4, 6, 7, 2019-09 and 2020-12), ported from the Corvus.Text.Json V5
2//! runtime evaluator (`Corvus.Text.Json.RuntimeEvaluator`).
3//!
4//! A schema is compiled once into a node graph with pre-digested keyword data, then any number of
5//! `serde_json::Value` instances are evaluated against it: fast (fail-fast, no reporting) with
6//! [`Validator::is_valid`], or exhaustively with a [`JsonSchemaResultsCollector`] for Basic, Detailed or Verbose
7//! results and annotations, with the same rows (paths, messages, order) as the C# collector.
8//!
9//! ```
10//! use serde_json::json;
11//!
12//! let validator = corvus_json_schema::compile(&json!({
13//!     "type": "object",
14//!     "properties": { "id": { "type": "integer", "minimum": 1 } },
15//!     "required": ["id"]
16//! }))
17//! .unwrap();
18//! assert!(validator.is_valid(&json!({ "id": 3 })));
19//! assert!(!validator.is_valid(&json!({ "id": 0 })));
20//! ```
21//!
22//! # Regular expressions and their limits
23//!
24//! `pattern`, `patternProperties` and the `regex` format follow ECMA-262. Simple patterns are matched by matchers of
25//! the crate's own and by the `regex` crate, and everything else (lookarounds, backreferences, modifier groups) by
26//! the crate's own ECMA-262 engine, as each pattern is written.
27//!
28//! A schema and an instance may both come from outside, so a pattern is bounded in what it costs. One match against
29//! one string may take [`CompileOptions::pattern_match_limit`], one second unless the options say otherwise. A match
30//! that reaches it is abandoned and the validation has no answer: [`Validator::try_validate`] and the other `try_`
31//! functions return [`SchemaEvaluationError::PatternMatchLimit`]. A pattern longer than 1,048,576 bytes, nested more
32//! than 1,000 groups deep or of more than about 16,000 terms does not compile, and
33//! [`SchemaCompilationError::kind`] is then [`SchemaCompilationErrorKind::PatternTooLarge`], which is not the kind
34//! of a pattern that is not valid.
35//!
36//! ```
37//! use std::time::Duration;
38//!
39//! use corvus_json_schema::{CompileOptions, SchemaCompilationErrorKind, SchemaEvaluationError, compile, compile_with};
40//! use serde_json::json;
41//!
42//! // A group read back inside a repetition: on this string the match would run for longer than anyone waits.
43//! let options = CompileOptions { pattern_match_limit: Some(Duration::from_millis(50)), ..CompileOptions::default() };
44//! let validator = compile_with(&json!({ "pattern": r"^(?:(a+)\1*)+$" }), &options).unwrap();
45//! let instance = json!("a".repeat(60) + "b");
46//! match validator.try_validate(&instance) {
47//!     Err(SchemaEvaluationError::PatternMatchLimit(e)) => {
48//!         assert_eq!(e.pattern(), r"^(?:(a+)\1*)+$");
49//!         assert_eq!(e.limit(), Duration::from_millis(50));
50//!     }
51//!     other => panic!("{other:?}"),
52//! }
53//! // is_valid has no way to say so, and answers false.
54//! assert!(!validator.is_valid(&instance));
55//!
56//! // A pattern over a limit of size is not a pattern that is not valid.
57//! let error = compile(&json!({ "pattern": "a".repeat(20000) })).unwrap_err();
58//! assert!(matches!(error.kind(), SchemaCompilationErrorKind::PatternTooLarge(large) if large.limit() == "size"));
59//! let error = compile(&json!({ "pattern": "a**" })).unwrap_err();
60//! assert_eq!(error.kind(), &SchemaCompilationErrorKind::InvalidPattern);
61//! ```
62
63mod compiler;
64mod dialect;
65mod document;
66mod ecmaregex;
67mod eval;
68mod formats;
69mod instance;
70mod loader;
71#[rustfmt::skip]
72mod metaschemas;
73mod node;
74mod numbers;
75mod options;
76mod pattern;
77mod results;
78mod uri;
79
80use std::sync::Arc;
81
82use serde_json::Value;
83
84pub use dialect::Dialect;
85pub use document::{JsonDocument, JsonDocumentArray, JsonDocumentObject, JsonDocumentValue, JsonParseError};
86pub use instance::{ArrayView, Instance, Kind, ObjectView, View};
87pub use options::{
88    CompileOptions, DEFAULT_PATTERN_MATCH_LIMIT, DocumentResolver, FormatValidator, PatternMatchLimitError,
89    PatternTooLargeError, SchemaCompilationError, SchemaCompilationErrorKind, SchemaEvaluationDepthError,
90    SchemaEvaluationError,
91};
92pub use results::{
93    Annotation, JsonSchemaResultsCollector, ResultsLevel, SchemaResult, collect_annotations, encode_pointer_segment,
94    enumerate_annotations, schema_location_fragment,
95};
96
97/// The version of this crate.
98pub const VERSION: &str = env!("CARGO_PKG_VERSION");
99
100/// Why JSON text could not be validated, as [`Validator::validate_json`] and [`Validator::evaluate_json`] report it.
101/// [`Validator::try_validate_json`] and [`Validator::try_evaluate_json`] report a [`SchemaEvaluationError`], which
102/// also tells a match of a regular expression that reached its limit from the maximum depth.
103#[derive(Clone, Debug, PartialEq, Eq)]
104pub enum JsonValidationError {
105    /// The text is not valid JSON.
106    InvalidJson(JsonParseError),
107    /// The validation was abandoned at a limit: it recursed in place beyond the maximum depth, or a match of a
108    /// regular expression reached its limit. The name is from when the depth was the one limit, and the message
109    /// names both (see [`without_the_limit`] for why the two are one here).
110    DepthExceeded(SchemaEvaluationDepthError),
111}
112
113impl std::fmt::Display for JsonValidationError {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        match self {
116            JsonValidationError::InvalidJson(e) => write!(f, "invalid JSON: {e}"),
117            JsonValidationError::DepthExceeded(e) => e.fmt(f),
118        }
119    }
120}
121
122impl std::error::Error for JsonValidationError {
123    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
124        match self {
125            JsonValidationError::InvalidJson(e) => Some(e),
126            JsonValidationError::DepthExceeded(e) => Some(e),
127        }
128    }
129}
130
131/// What the functions whose error type was fixed before the limit of a match existed report for an evaluation that
132/// was abandoned. Their type, [`SchemaEvaluationDepthError`], has one value, so a match that reached its limit is
133/// reported as that value too: an error, and so not an answer, though not one that says which limit it was. The
134/// `try_` functions say which. This is the one place that decides it.
135fn without_the_limit(e: SchemaEvaluationError) -> SchemaEvaluationDepthError {
136    match e {
137        SchemaEvaluationError::DepthExceeded(e) => e,
138        _ => SchemaEvaluationDepthError,
139    }
140}
141
142fn json_error(e: SchemaEvaluationError) -> JsonValidationError {
143    match e {
144        SchemaEvaluationError::InvalidJson(e) => JsonValidationError::InvalidJson(e),
145        other => JsonValidationError::DepthExceeded(without_the_limit(other)),
146    }
147}
148
149/// A compiled schema. Cheap to clone and safe to share between threads.
150#[derive(Clone)]
151pub struct Validator {
152    program: Arc<eval::Program>,
153}
154
155impl std::fmt::Debug for Validator {
156    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157        f.debug_struct("Validator").field("nodes", &self.program.nodes.len()).finish()
158    }
159}
160
161/// Compiles a schema with the default options (2020-12 for documents without `$schema`, `format` as an annotation
162/// unless the vocabularies assert it).
163pub fn compile(schema: &Value) -> Result<Validator, SchemaCompilationError> {
164    compile_with(schema, &CompileOptions::default())
165}
166
167/// Compiles a schema.
168pub fn compile_with(schema: &Value, options: &CompileOptions) -> Result<Validator, SchemaCompilationError> {
169    let compiled = compiler::SchemaCompiler::compile(schema.clone(), options)?;
170    Validator::from_compiled(compiled, options)
171}
172
173/// Compiles the schema document at `uri`, fetched through `options.resolve_document` (or one of the standard
174/// metaschemas).
175pub fn compile_from_uri(uri: &str, options: &CompileOptions) -> Result<Validator, SchemaCompilationError> {
176    let compiled = compiler::SchemaCompiler::compile_from_uri(uri, options)?;
177    Validator::from_compiled(compiled, options)
178}
179
180impl Validator {
181    fn from_compiled(
182        c: compiler::CompiledSchema,
183        options: &CompileOptions,
184    ) -> Result<Validator, SchemaCompilationError> {
185        let program = eval::Program::new(
186            c.nodes,
187            c.root,
188            c.uses_dynamic_scope,
189            options.max_depth,
190            options.formats.clone(),
191            c.documents,
192            c.annotation_sources,
193            options.assert_format.is_some(),
194            pattern::PatternLimit::new(options.pattern_match_limit),
195        );
196        // Compiling matches each pattern of patternProperties against the property names the schema declares.
197        let failure = program.compile_budget.lock().unwrap_or_else(|e| e.into_inner()).take_failure();
198        if let Some(e) = failure {
199            return Err(SchemaCompilationError::of_kind(
200                e.to_string(),
201                SchemaCompilationErrorKind::PatternMatchLimit(e),
202            ));
203        }
204        Ok(Validator { program: Arc::new(program) })
205    }
206
207    /// Whether the instance is valid. It has no way to report an error, so it fails closed: it reports false both
208    /// for a schema that recursed in place beyond the maximum depth and for a match of a regular expression that
209    /// reached its limit (see [`CompileOptions::pattern_match_limit`]). A limit is never reported as valid. In
210    /// neither case is the instance known to be not valid. Use [`Validator::try_validate`] to tell them from an
211    /// instance that is not valid.
212    #[inline]
213    pub fn is_valid(&self, instance: &Value) -> bool {
214        eval::Evaluator::new(&self.program, None).validate(instance)
215    }
216
217    /// Whether the instance is valid, or why the evaluation gave no answer: it recursed in place beyond the maximum
218    /// depth, or a match of a regular expression reached its limit.
219    pub fn try_validate(&self, instance: &Value) -> Result<bool, SchemaEvaluationError> {
220        self.try_validate_instance(instance)
221    }
222
223    /// [`Validator::try_validate`] for any [`Instance`]: a value read in place, such as a host language's own
224    /// objects, with no conversion to `serde_json::Value`.
225    pub fn try_validate_instance<'a, I: Instance<'a>>(&self, instance: I) -> Result<bool, SchemaEvaluationError> {
226        let mut e = eval::Evaluator::new(&self.program, None);
227        let ok = e.validate(instance);
228        e.outcome(ok)
229    }
230
231    /// Whether the JSON text is valid, or why there is no answer. The text is parsed into the thread's reused
232    /// buffers and evaluated in place: in the steady state nothing is allocated. Use [`JsonDocument`] to validate
233    /// the same text more than once.
234    pub fn try_validate_json(&self, json: &str) -> Result<bool, SchemaEvaluationError> {
235        JsonDocument::with_document(json, |d| self.try_validate_instance(d.root()))
236            .map_err(SchemaEvaluationError::InvalidJson)?
237    }
238
239    /// Evaluates the instance exhaustively, reporting to the collector. Returns whether it is valid, or why the
240    /// evaluation gave no answer. With an error the collector's results are not complete.
241    pub fn try_evaluate(
242        &self,
243        instance: &Value,
244        collector: &mut JsonSchemaResultsCollector,
245    ) -> Result<bool, SchemaEvaluationError> {
246        self.try_evaluate_instance(instance, collector)
247    }
248
249    /// [`Validator::try_evaluate`] for any [`Instance`].
250    pub fn try_evaluate_instance<'a, I: Instance<'a>>(
251        &self,
252        instance: I,
253        collector: &mut JsonSchemaResultsCollector,
254    ) -> Result<bool, SchemaEvaluationError> {
255        let mut e = eval::Evaluator::new(&self.program, Some(collector));
256        let ok = e.evaluate(instance);
257        e.outcome(ok)
258    }
259
260    /// [`Validator::try_evaluate`] for JSON text, parsed as [`Validator::try_validate_json`] parses it.
261    pub fn try_evaluate_json(
262        &self,
263        json: &str,
264        collector: &mut JsonSchemaResultsCollector,
265    ) -> Result<bool, SchemaEvaluationError> {
266        JsonDocument::with_document(json, |d| self.try_evaluate_instance(d.root(), collector))
267            .map_err(SchemaEvaluationError::InvalidJson)?
268    }
269
270    /// Whether the instance is valid, or an error if the validation was abandoned at a limit. The error type has
271    /// one value, whose message names both limits, so it does not say whether the evaluation recursed in place
272    /// beyond the maximum depth or a match of a regular expression reached its limit: [`Validator::try_validate`]
273    /// does.
274    pub fn validate(&self, instance: &Value) -> Result<bool, SchemaEvaluationDepthError> {
275        self.try_validate(instance).map_err(without_the_limit)
276    }
277
278    /// Evaluates the instance exhaustively, reporting to the collector. Returns whether it is valid. The error is
279    /// that of [`Validator::validate`], for a validation abandoned at either limit: [`Validator::try_evaluate`]
280    /// says which.
281    pub fn evaluate(
282        &self,
283        instance: &Value,
284        collector: &mut JsonSchemaResultsCollector,
285    ) -> Result<bool, SchemaEvaluationDepthError> {
286        self.try_evaluate(instance, collector).map_err(without_the_limit)
287    }
288
289    /// [`Validator::validate`] for any [`Instance`]: a value read in place, such as a host language's own objects,
290    /// with no conversion to `serde_json::Value`. The error is that of [`Validator::validate`], for a validation
291    /// abandoned at either limit: [`Validator::try_validate_instance`] says which.
292    pub fn validate_instance<'a, I: Instance<'a>>(&self, instance: I) -> Result<bool, SchemaEvaluationDepthError> {
293        self.try_validate_instance(instance).map_err(without_the_limit)
294    }
295
296    /// Whether the JSON text is valid. The text is parsed into the thread's reused buffers and evaluated in place: in
297    /// the steady state nothing is allocated. Use [`JsonDocument`] to validate the same text more than once. An
298    /// evaluation that was abandoned is reported as [`JsonValidationError::DepthExceeded`], whichever limit it
299    /// reached: [`Validator::try_validate_json`] says which.
300    pub fn validate_json(&self, json: &str) -> Result<bool, JsonValidationError> {
301        self.try_validate_json(json).map_err(json_error)
302    }
303
304    /// [`Validator::evaluate`] for JSON text, parsed as [`Validator::validate_json`] parses it. An evaluation that
305    /// was abandoned is [`JsonValidationError::DepthExceeded`], whichever limit it reached:
306    /// [`Validator::try_evaluate_json`] says which.
307    pub fn evaluate_json(
308        &self,
309        json: &str,
310        collector: &mut JsonSchemaResultsCollector,
311    ) -> Result<bool, JsonValidationError> {
312        self.try_evaluate_json(json, collector).map_err(json_error)
313    }
314
315    /// [`Validator::evaluate`] for any [`Instance`]. The error is that of [`Validator::validate`], for a validation
316    /// abandoned at either limit: [`Validator::try_evaluate_instance`] says which.
317    pub fn evaluate_instance<'a, I: Instance<'a>>(
318        &self,
319        instance: I,
320        collector: &mut JsonSchemaResultsCollector,
321    ) -> Result<bool, SchemaEvaluationDepthError> {
322        self.try_evaluate_instance(instance, collector).map_err(without_the_limit)
323    }
324}