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}