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
22mod compiler;
23mod dialect;
24mod document;
25mod eval;
26mod formats;
27mod instance;
28mod loader;
29#[rustfmt::skip]
30mod metaschemas;
31mod node;
32mod numbers;
33mod options;
34mod pattern;
35mod results;
36mod uri;
37
38use std::sync::Arc;
39
40use serde_json::Value;
41
42pub use dialect::Dialect;
43pub use document::{JsonDocument, JsonDocumentArray, JsonDocumentObject, JsonDocumentValue, JsonParseError};
44pub use instance::{ArrayView, Instance, Kind, ObjectView, View};
45pub use options::{
46    CompileOptions, DocumentResolver, FormatValidator, SchemaCompilationError, SchemaEvaluationDepthError,
47};
48pub use results::{
49    Annotation, JsonSchemaResultsCollector, ResultsLevel, SchemaResult, collect_annotations, encode_pointer_segment,
50    enumerate_annotations, schema_location_fragment,
51};
52
53/// The version of this crate.
54pub const VERSION: &str = env!("CARGO_PKG_VERSION");
55
56/// A compiled schema. Cheap to clone and safe to share between threads.
57#[derive(Clone)]
58pub struct Validator {
59    program: Arc<eval::Program>,
60}
61
62impl std::fmt::Debug for Validator {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        f.debug_struct("Validator").field("nodes", &self.program.nodes.len()).finish()
65    }
66}
67
68/// Compiles a schema with the default options (2020-12 for documents without `$schema`, `format` as an annotation
69/// unless the vocabularies assert it).
70pub fn compile(schema: &Value) -> Result<Validator, SchemaCompilationError> {
71    compile_with(schema, &CompileOptions::default())
72}
73
74/// Compiles a schema.
75pub fn compile_with(schema: &Value, options: &CompileOptions) -> Result<Validator, SchemaCompilationError> {
76    let compiled = compiler::SchemaCompiler::compile(schema.clone(), options)?;
77    Ok(Validator::from_compiled(compiled, options))
78}
79
80/// Compiles the schema document at `uri`, fetched through `options.resolve_document` (or one of the standard
81/// metaschemas).
82pub fn compile_from_uri(uri: &str, options: &CompileOptions) -> Result<Validator, SchemaCompilationError> {
83    let compiled = compiler::SchemaCompiler::compile_from_uri(uri, options)?;
84    Ok(Validator::from_compiled(compiled, options))
85}
86
87impl Validator {
88    fn from_compiled(c: compiler::CompiledSchema, options: &CompileOptions) -> Validator {
89        let program = eval::Program::new(
90            c.nodes,
91            c.root,
92            c.uses_dynamic_scope,
93            options.max_depth,
94            options.formats.clone(),
95            c.documents,
96            c.annotation_sources,
97            options.assert_format.is_some(),
98        );
99        Validator { program: Arc::new(program) }
100    }
101
102    /// Whether the instance is valid. A schema that recursed in place beyond the maximum depth is reported as
103    /// invalid; use [`Validator::validate`] to tell the two apart.
104    #[inline]
105    pub fn is_valid(&self, instance: &Value) -> bool {
106        eval::Evaluator::new(&self.program, None).validate(instance)
107    }
108
109    /// Whether the instance is valid, or an error if evaluation recursed in place beyond the maximum depth.
110    pub fn validate(&self, instance: &Value) -> Result<bool, SchemaEvaluationDepthError> {
111        let mut e = eval::Evaluator::new(&self.program, None);
112        let ok = e.validate(instance);
113        if e.depth_exceeded { Err(SchemaEvaluationDepthError) } else { Ok(ok) }
114    }
115
116    /// Evaluates the instance exhaustively, reporting to the collector. Returns whether it is valid.
117    pub fn evaluate(
118        &self,
119        instance: &Value,
120        collector: &mut JsonSchemaResultsCollector,
121    ) -> Result<bool, SchemaEvaluationDepthError> {
122        self.evaluate_instance(instance, collector)
123    }
124
125    /// [`Validator::validate`] for any [`Instance`]: a value read in place, such as a host language's own objects,
126    /// with no conversion to `serde_json::Value`.
127    pub fn validate_instance<'a, I: Instance<'a>>(&self, instance: I) -> Result<bool, SchemaEvaluationDepthError> {
128        let mut e = eval::Evaluator::new(&self.program, None);
129        let ok = e.validate(instance);
130        if e.depth_exceeded { Err(SchemaEvaluationDepthError) } else { Ok(ok) }
131    }
132
133    /// [`Validator::evaluate`] for any [`Instance`].
134    pub fn evaluate_instance<'a, I: Instance<'a>>(
135        &self,
136        instance: I,
137        collector: &mut JsonSchemaResultsCollector,
138    ) -> Result<bool, SchemaEvaluationDepthError> {
139        let mut e = eval::Evaluator::new(&self.program, Some(collector));
140        let ok = e.evaluate(instance);
141        if e.depth_exceeded { Err(SchemaEvaluationDepthError) } else { Ok(ok) }
142    }
143}