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/// Why JSON text could not be validated.
57#[derive(Clone, Debug, PartialEq, Eq)]
58pub enum JsonValidationError {
59    /// The text is not valid JSON.
60    InvalidJson(JsonParseError),
61    /// Evaluation recursed in place beyond the maximum depth.
62    DepthExceeded(SchemaEvaluationDepthError),
63}
64
65impl std::fmt::Display for JsonValidationError {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        match self {
68            JsonValidationError::InvalidJson(e) => write!(f, "invalid JSON: {e}"),
69            JsonValidationError::DepthExceeded(e) => e.fmt(f),
70        }
71    }
72}
73
74impl std::error::Error for JsonValidationError {
75    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
76        match self {
77            JsonValidationError::InvalidJson(e) => Some(e),
78            JsonValidationError::DepthExceeded(e) => Some(e),
79        }
80    }
81}
82
83/// A compiled schema. Cheap to clone and safe to share between threads.
84#[derive(Clone)]
85pub struct Validator {
86    program: Arc<eval::Program>,
87}
88
89impl std::fmt::Debug for Validator {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        f.debug_struct("Validator").field("nodes", &self.program.nodes.len()).finish()
92    }
93}
94
95/// Compiles a schema with the default options (2020-12 for documents without `$schema`, `format` as an annotation
96/// unless the vocabularies assert it).
97pub fn compile(schema: &Value) -> Result<Validator, SchemaCompilationError> {
98    compile_with(schema, &CompileOptions::default())
99}
100
101/// Compiles a schema.
102pub fn compile_with(schema: &Value, options: &CompileOptions) -> Result<Validator, SchemaCompilationError> {
103    let compiled = compiler::SchemaCompiler::compile(schema.clone(), options)?;
104    Ok(Validator::from_compiled(compiled, options))
105}
106
107/// Compiles the schema document at `uri`, fetched through `options.resolve_document` (or one of the standard
108/// metaschemas).
109pub fn compile_from_uri(uri: &str, options: &CompileOptions) -> Result<Validator, SchemaCompilationError> {
110    let compiled = compiler::SchemaCompiler::compile_from_uri(uri, options)?;
111    Ok(Validator::from_compiled(compiled, options))
112}
113
114impl Validator {
115    fn from_compiled(c: compiler::CompiledSchema, options: &CompileOptions) -> Validator {
116        let program = eval::Program::new(
117            c.nodes,
118            c.root,
119            c.uses_dynamic_scope,
120            options.max_depth,
121            options.formats.clone(),
122            c.documents,
123            c.annotation_sources,
124            options.assert_format.is_some(),
125        );
126        Validator { program: Arc::new(program) }
127    }
128
129    /// Whether the instance is valid. A schema that recursed in place beyond the maximum depth is reported as
130    /// invalid; use [`Validator::validate`] to tell the two apart.
131    #[inline]
132    pub fn is_valid(&self, instance: &Value) -> bool {
133        eval::Evaluator::new(&self.program, None).validate(instance)
134    }
135
136    /// Whether the instance is valid, or an error if evaluation recursed in place beyond the maximum depth.
137    pub fn validate(&self, instance: &Value) -> Result<bool, SchemaEvaluationDepthError> {
138        let mut e = eval::Evaluator::new(&self.program, None);
139        let ok = e.validate(instance);
140        if e.depth_exceeded { Err(SchemaEvaluationDepthError) } else { Ok(ok) }
141    }
142
143    /// Evaluates the instance exhaustively, reporting to the collector. Returns whether it is valid.
144    pub fn evaluate(
145        &self,
146        instance: &Value,
147        collector: &mut JsonSchemaResultsCollector,
148    ) -> Result<bool, SchemaEvaluationDepthError> {
149        self.evaluate_instance(instance, collector)
150    }
151
152    /// [`Validator::validate`] for any [`Instance`]: a value read in place, such as a host language's own objects,
153    /// with no conversion to `serde_json::Value`.
154    pub fn validate_instance<'a, I: Instance<'a>>(&self, instance: I) -> Result<bool, SchemaEvaluationDepthError> {
155        let mut e = eval::Evaluator::new(&self.program, None);
156        let ok = e.validate(instance);
157        if e.depth_exceeded { Err(SchemaEvaluationDepthError) } else { Ok(ok) }
158    }
159
160    /// Whether the JSON text is valid. The text is parsed into the thread's reused buffers and evaluated in place: in
161    /// the steady state nothing is allocated. Use [`JsonDocument`] to validate the same text more than once.
162    pub fn validate_json(&self, json: &str) -> Result<bool, JsonValidationError> {
163        JsonDocument::with_document(json, |d| self.validate_instance(d.root()))
164            .map_err(JsonValidationError::InvalidJson)?
165            .map_err(JsonValidationError::DepthExceeded)
166    }
167
168    /// [`Validator::evaluate`] for JSON text, parsed as [`Validator::validate_json`] parses it.
169    pub fn evaluate_json(
170        &self,
171        json: &str,
172        collector: &mut JsonSchemaResultsCollector,
173    ) -> Result<bool, JsonValidationError> {
174        JsonDocument::with_document(json, |d| self.evaluate_instance(d.root(), collector))
175            .map_err(JsonValidationError::InvalidJson)?
176            .map_err(JsonValidationError::DepthExceeded)
177    }
178
179    /// [`Validator::evaluate`] for any [`Instance`].
180    pub fn evaluate_instance<'a, I: Instance<'a>>(
181        &self,
182        instance: I,
183        collector: &mut JsonSchemaResultsCollector,
184    ) -> Result<bool, SchemaEvaluationDepthError> {
185        let mut e = eval::Evaluator::new(&self.program, Some(collector));
186        let ok = e.evaluate(instance);
187        if e.depth_exceeded { Err(SchemaEvaluationDepthError) } else { Ok(ok) }
188    }
189}