jsonschema/lib.rs
1#![allow(clippy::unnecessary_wraps)]
2// Runtime helpers stay `pub` for the macros-gated `__private` re-exports; without `macros` they are crate-internal.
3#![cfg_attr(not(feature = "macros"), allow(unreachable_pub))]
4//! A high-performance JSON Schema validator for Rust.
5//!
6//! - 📚 Support for popular JSON Schema drafts
7//! - 🔧 Custom keywords and format validators
8//! - ⚡ [Compile-time validators](#compile-time-validator-macro), also for
9//! [Python](#python-extension-modules) and [Ruby](#ruby-extension-modules) extension modules
10//! - 🌐 Blocking & non-blocking remote reference fetching (network/file)
11//! - 🎨 Structured Output v1 reports (flag/list/hierarchical)
12//! - ✨ Meta-schema validation for schema documents
13//! - 🧮 Schema canonicalization (experimental; see the [`canonical`] module)
14//! - 🧩 Validation of custom in-memory JSON representations
15//! - 🚀 WebAssembly support
16//!
17//! ## Supported drafts
18//!
19//! Compliance levels vary across drafts, with newer versions having some unimplemented keywords.
20//!
21//! - 
22//! - 
23//! - 
24//! - 
25//! - 
26//!
27//! # Validation
28//!
29//! The `jsonschema` crate offers two main approaches to validation: one-off validation and reusable validators.
30//! When external references are involved, the validator can be constructed using either blocking or non-blocking I/O.
31//!
32//!
33//! For simple use cases where you need to validate an instance against a schema once, use [`is_valid`] or [`validate`] functions:
34//!
35//! ```rust
36//! use serde_json::json;
37//!
38//! let schema = json!({"type": "string"});
39//! let instance = json!("Hello, world!");
40//!
41//! assert!(jsonschema::is_valid(&schema, &instance));
42//! assert!(jsonschema::validate(&schema, &instance).is_ok());
43//! ```
44//!
45//! For better performance, especially when validating multiple instances against the same schema, build a validator once and reuse it:
46//! If your schema contains external references, you can choose between blocking and non-blocking construction:
47//!
48//! ```rust
49//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
50//! use serde_json::json;
51//!
52//! let schema = json!({"type": "string"});
53//! // Blocking construction - will fetch external references synchronously
54//! let validator = jsonschema::validator_for(&schema)?;
55//! // Non-blocking construction - will fetch external references asynchronously
56//! # #[cfg(feature = "resolve-async")]
57//! # async fn async_example() -> Result<(), Box<dyn std::error::Error>> {
58//! # let schema = json!({"type": "string"});
59//! let validator = jsonschema::async_validator_for(&schema).await?;
60//! # Ok(())
61//! # }
62//!
63//! // Once constructed, validation is always synchronous as it works with in-memory data
64//! assert!(validator.is_valid(&json!("Hello, world!")));
65//! assert!(!validator.is_valid(&json!(42)));
66//! assert!(validator.validate(&json!(42)).is_err());
67//!
68//! // Iterate over all errors
69//! let instance = json!(42);
70//! for error in validator.iter_errors(&instance) {
71//! eprintln!("Error: {}", error);
72//! eprintln!("Location: {}", error.instance_path());
73//! }
74//! # Ok(())
75//! # }
76//! ```
77//!
78//! ### Note on `format` keyword
79//!
80//! By default, format validation is draft‑dependent. To opt in for format checks, you can configure your validator like this:
81//!
82//! ```rust
83//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
84//! # use serde_json::json;
85//! #
86//! # let schema = json!({"type": "string"});
87//! let validator = jsonschema::draft202012::options()
88//! .should_validate_formats(true)
89//! .build(&schema)?;
90//! # Ok(())
91//! # }
92//! ```
93//!
94//! Once built, any `format` keywords in your schema will be actively validated according to the chosen draft.
95//!
96//! `idn-hostname` and `idn-email` need the `idna` feature, which is on by default. Without it they
97//! are treated as unknown formats.
98//!
99//! # Reading errors
100//!
101//! [`ErrorIterator::into_errors`] collects the whole set into a [`ValidationErrors`], which
102//! implements `Display` and [`std::error::Error`], so reporting every failure takes no formatting
103//! loop of its own:
104//!
105//! ```rust
106//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
107//! use serde_json::json;
108//!
109//! let validator = jsonschema::validator_for(&json!({
110//! "type": "object",
111//! "properties": {"n": {"minimum": 5}},
112//! "required": ["name"]
113//! }))?;
114//! let instance = json!({"n": 1});
115//! let errors = validator.iter_errors(&instance).into_errors();
116//!
117//! assert_eq!(errors.len(), 2);
118//! // Validation errors:
119//! // 01: "name" is a required property
120//! // 02: /n: 1 is less than the minimum of 5
121//! println!("{errors}");
122//! # Ok(())
123//! # }
124//! ```
125//!
126//! [`ValidationError::kind`] carries the failed keyword's operands, so a caller does not need to
127//! re-read the schema to describe what was expected:
128//!
129//! ```rust
130//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
131//! use serde_json::json;
132//! use jsonschema::error::ValidationErrorKind;
133//!
134//! let validator = jsonschema::validator_for(&json!({"minimum": 5}))?;
135//! let instance = json!(1);
136//! let error = validator.validate(&instance).expect_err("must be invalid");
137//!
138//! match error.kind() {
139//! ValidationErrorKind::Minimum { limit } => assert_eq!(limit, &json!(5)),
140//! other => panic!("unexpected kind: {other:?}"),
141//! }
142//! # Ok(())
143//! # }
144//! ```
145//!
146//! [`ValidationError::absolute_keyword_location`] gives the resolvable URI of the keyword that
147//! produced the error, and [`ValidationError::masked`] hides the instance value in the message.
148//!
149//! # Compile-Time Validator Macro
150//!
151//! The `validator` attribute macro (enabled by the `macros` feature) compiles schemas at build time:
152//! Generated validators are significantly faster than runtime validators, so prefer them when the schema is known at
153//! build time.
154//!
155//! ```ignore
156//! // Inline schema
157//! #[jsonschema::validator(schema = r#"{"maxLength": 5}"#)]
158//! struct Short;
159//!
160//! // Or load it from a file:
161//! // #[jsonschema::validator(path = "schema.json")]
162//!
163//! let instance = serde_json::json!("value");
164//! assert!(Short::is_valid(&instance));
165//! Short::validate(&instance)?;
166//! ```
167//!
168//! Supported macro attributes; each mirrors the matching [`ValidationOptions`] builder method,
169//! which documents its behavior and defaults:
170//!
171//! - schema source (exactly one required): `path = "..."` (file) or `schema = r#"..."#` (inline)
172//! - `backend = SerdeJson|Pyo3|Magnus` (default: `SerdeJson`), the representation the generated
173//! validator reads; `Pyo3` needs the `pyo3` feature (see
174//! [Python Extension Modules](#python-extension-modules)) and `Magnus` the `magnus` feature (see
175//! [Ruby Extension Modules](#ruby-extension-modules))
176//! - `draft = Draft202012` (or another variant) -> [`ValidationOptions::with_draft`]
177//! - `base_uri = "..."` -> [`ValidationOptions::with_base_uri`]
178//! - `resources = { "<uri>" => { schema = r#"..."# } | { path = "..." } }`
179//! - `vocabularies = ["<uri>"]` -> [`ValidationOptions::with_vocabulary`]
180//! - `validate_formats = true|false` -> [`ValidationOptions::should_validate_formats`]
181//! - `ignore_unknown_formats = true|false` -> [`ValidationOptions::should_ignore_unknown_formats`]
182//! - `formats = { "name" => crate::path::to::fn }` -> [`ValidationOptions::with_format`]
183//! - `keywords = { "name" => crate::path::to::factory }` -> [`ValidationOptions::with_keyword`]
184//! - `content_media_types = { "type" => crate::path::to::fn }` -> [`ValidationOptions::with_content_media_type`]
185//! - `content_encodings = { "name" => { check = ..., convert = ... } }` -> [`ValidationOptions::with_content_encoding`]
186//! - `pattern_options = { ... }` -> [`PatternOptions`]
187//! - `email_options = { ... }` -> [`EmailOptions`]
188//! - `methods = { is_valid = true, validate = false, iter_errors = false }`, which methods to
189//! generate (all default to `true`; at least one must stay enabled)
190//!
191//! ## Python Extension Modules
192//!
193//! With `backend = Pyo3`, the generated validator reads Python objects in place, for extension
194//! modules that know their schemas at build time. The methods take `&Bound<'_, PyAny>` and return
195//! `PyResult<...>`, which is an error when the instance holds a value with no JSON counterpart,
196//! such as a `set`:
197//!
198//! ```ignore
199//! use pyo3::prelude::*;
200//!
201//! #[jsonschema::validator(path = "event.json", backend = Pyo3)]
202//! struct Event;
203//!
204//! #[pyfunction]
205//! fn is_valid_event(instance: &Bound<'_, PyAny>) -> PyResult<bool> {
206//! Event::is_valid(instance)
207//! }
208//! ```
209//!
210//! A `keywords` factory for this backend returns `Box<dyn for<'i> Keyword<'i, json::Pyo3>>`. A
211//! complete extension with its build and test commands lives in
212//! [`examples/pyo3-extension`](https://github.com/Stranger6667/jsonschema/tree/master/examples/pyo3-extension).
213//!
214//! Building the extension with `PyO3`'s `abi3` features limits it to the stable Python API, where
215//! list and tuple elements are read through function calls; a build per Python version reads them
216//! in place, which is faster on array-heavy instances.
217//!
218//! ## Ruby Extension Modules
219//!
220//! With `backend = Magnus`, the generated validator reads Ruby objects in place, for extension
221//! modules that know their schemas at build time. The methods take `&magnus::Value` and return
222//! `Result<..., magnus::Error>`, which is an error when the instance holds a value with no JSON
223//! counterpart, such as a `Regexp`. They must run on a thread holding the GVL, as any Ruby call:
224//!
225//! ```ignore
226//! use magnus::{function, Error, Ruby, Value};
227//!
228//! #[jsonschema::validator(path = "event.json", backend = Magnus)]
229//! struct Event;
230//!
231//! fn valid_event(instance: Value) -> Result<bool, Error> {
232//! Event::is_valid(&instance)
233//! }
234//!
235//! #[magnus::init]
236//! fn init(ruby: &Ruby) -> Result<(), Error> {
237//! ruby.define_global_function("valid_event?", function!(valid_event, 1));
238//! Ok(())
239//! }
240//! ```
241//!
242//! A `keywords` factory for this backend returns `Box<dyn for<'i> Keyword<'i, json::Magnus>>`. A
243//! complete extension with its build and test commands lives in
244//! [`examples/magnus-extension`](https://github.com/Stranger6667/jsonschema/tree/master/examples/magnus-extension).
245//!
246//! ## Limitations
247//!
248//! - `is_valid`, `validate`, and `iter_errors` are generated; `evaluate` is not implemented yet.
249//! - Custom keywords cannot override built-in ones: a `keywords` entry named like a built-in keyword runs in
250//! addition to the built-in check, not instead of it.
251//! - When an instance violates several keywords, the first error reported by `validate()` may differ from the runtime
252//! validator's, since generated checks are not ordered by keyword priority; `is_valid` and validity are unaffected.
253//!
254//! # Structured Output
255//!
256//! The `evaluate()` method provides access to structured validation output formats defined by
257//! [JSON Schema Output v1](https://github.com/json-schema-org/json-schema-spec/blob/main/specs/output/jsonschema-validation-output-machines.md).
258//! This is useful when you need detailed information about the validation process beyond simple pass/fail results.
259//!
260//! ```rust
261//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
262//! use serde_json::json;
263//!
264//! let schema = json!({
265//! "type": "object",
266//! "properties": {
267//! "name": {"type": "string"},
268//! "age": {"type": "number", "minimum": 0}
269//! },
270//! "required": ["name"]
271//! });
272//!
273//! let validator = jsonschema::validator_for(&schema)?;
274//! let instance = json!({"name": "Alice", "age": 30});
275//!
276//! // Evaluate the instance
277//! let evaluation = validator.evaluate(&instance);
278//!
279//! // Flag format: Simple boolean validity
280//! let flag = evaluation.flag();
281//! assert!(flag.valid);
282//!
283//! // List format: Flat list of all evaluation steps
284//! let list_output = serde_json::to_value(evaluation.list())?;
285//! println!("List output: {}", serde_json::to_string_pretty(&list_output)?);
286//!
287//! // Hierarchical format: Nested tree structure
288//! let hierarchical_output = serde_json::to_value(evaluation.hierarchical())?;
289//! println!(
290//! "Hierarchical output: {}",
291//! serde_json::to_string_pretty(&hierarchical_output)?
292//! );
293//!
294//! // Iterate over annotations collected during validation
295//! for annotation in evaluation.iter_annotations() {
296//! println!("Annotation at {}: {:?}",
297//! annotation.instance_location,
298//! annotation.annotations
299//! );
300//! }
301//!
302//! // Iterate over errors (if any)
303//! for error in evaluation.iter_errors() {
304//! println!("Error: {}", error.error);
305//! }
306//! # Ok(())
307//! # }
308//! ```
309//!
310//! The structured output formats are particularly useful for:
311//! - **Debugging**: Understanding exactly which schema keywords matched or failed
312//! - **User feedback**: Providing detailed, actionable error messages
313//! - **Annotations**: Collecting metadata produced by successful validation
314//! - **Tooling**: Building development tools that work with JSON Schema
315//!
316//! For example, validating `["hello", "oops"]` against a schema with both `prefixItems` and
317//! `items` produces list output similar to:
318//!
319//! ```json
320//! {
321//! "valid": false,
322//! "details": [
323//! {"valid": false, "evaluationPath": "", "schemaLocation": "", "instanceLocation": ""},
324//! {
325//! "valid": false,
326//! "evaluationPath": "/items",
327//! "schemaLocation": "/items",
328//! "instanceLocation": "",
329//! "droppedAnnotations": true
330//! },
331//! {
332//! "valid": false,
333//! "evaluationPath": "/items",
334//! "schemaLocation": "/items",
335//! "instanceLocation": "/1"
336//! },
337//! {
338//! "valid": false,
339//! "evaluationPath": "/items/type",
340//! "schemaLocation": "/items/type",
341//! "instanceLocation": "/1",
342//! "errors": {"type": "\"oops\" is not of type \"integer\""}
343//! },
344//! {"valid": true, "evaluationPath": "/prefixItems", "schemaLocation": "/prefixItems", "instanceLocation": "", "annotations": 0}
345//! ]
346//! }
347//! ```
348//!
349//! ## Output Formats
350//!
351//! ### Flag Format
352//!
353//! The simplest format, containing only a boolean validity indicator:
354//!
355//! ```rust
356//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
357//! # use serde_json::json;
358//! # let schema = json!({"type": "string"});
359//! # let validator = jsonschema::validator_for(&schema)?;
360//! let evaluation = validator.evaluate(&json!("hello"));
361//! let flag = evaluation.flag();
362//!
363//! let output = serde_json::to_value(flag)?;
364//! // Output: {"valid": true}
365//! # Ok(())
366//! # }
367//! ```
368//!
369//! ### List Format
370//!
371//! A flat list of all evaluation units, where each unit describes a validation step:
372//!
373//! ```rust
374//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
375//! # use serde_json::json;
376//! let schema = json!({
377//! "allOf": [
378//! {"type": "number"},
379//! {"minimum": 0}
380//! ]
381//! });
382//! let validator = jsonschema::validator_for(&schema)?;
383//! let evaluation = validator.evaluate(&json!(42));
384//!
385//! let list = evaluation.list();
386//! let output = serde_json::to_value(list)?;
387//! // Output includes all evaluation steps in a flat array
388//! # Ok(())
389//! # }
390//! ```
391//!
392//! ### Hierarchical Format
393//!
394//! A nested tree structure that mirrors the schema's logical structure:
395//!
396//! ```rust
397//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
398//! # use serde_json::json;
399//! let schema = json!({
400//! "allOf": [
401//! {"type": "number"},
402//! {"minimum": 0}
403//! ]
404//! });
405//! let validator = jsonschema::validator_for(&schema)?;
406//! let evaluation = validator.evaluate(&json!(42));
407//!
408//! let hierarchical = evaluation.hierarchical();
409//! let output = serde_json::to_value(hierarchical)?;
410//! // Output has nested "details" arrays for sub-schema evaluations
411//! # Ok(())
412//! # }
413//! ```
414//!
415//! # Meta-Schema Validation
416//!
417//! The crate provides functionality to validate JSON Schema documents themselves against their meta-schemas.
418//! This ensures your schema documents are valid according to the JSON Schema specification.
419//!
420//! ```rust
421//! use serde_json::json;
422//!
423//! let schema = json!({
424//! "type": "object",
425//! "properties": {
426//! "name": {"type": "string"},
427//! "age": {"type": "integer", "minimum": 0}
428//! }
429//! });
430//!
431//! // Validate schema with automatic draft detection
432//! assert!(jsonschema::meta::is_valid(&schema));
433//! assert!(jsonschema::meta::validate(&schema).is_ok());
434//!
435//! // Invalid schema example
436//! let invalid_schema = json!({
437//! "type": "invalid_type", // must be one of the valid JSON Schema types
438//! "minimum": "not_a_number"
439//! });
440//! assert!(!jsonschema::meta::is_valid(&invalid_schema));
441//! assert!(jsonschema::meta::validate(&invalid_schema).is_err());
442//! ```
443//!
444//! # Configuration
445//!
446//! `jsonschema` provides several ways to configure and use JSON Schema validation.
447//!
448//! ## Draft-specific Modules
449//!
450//! The library offers modules for specific JSON Schema draft versions:
451//!
452//! - [`draft4`]
453//! - [`draft6`]
454//! - [`draft7`]
455//! - [`draft201909`]
456//! - [`draft202012`]
457//!
458//! Each module provides:
459//! - A `new` function to create a validator
460//! - An `is_valid` function for validation with a boolean result
461//! - An `validate` function for getting the first validation error
462//! - An `options` function to create a draft-specific configuration builder
463//! - A `meta` module for draft-specific meta-schema validation
464//!
465//! Here's how you can explicitly use a specific draft version:
466//!
467//! ```rust
468//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
469//! use serde_json::json;
470//!
471//! let schema = json!({"type": "string"});
472//!
473//! // Instance validation
474//! let validator = jsonschema::draft7::new(&schema)?;
475//! assert!(validator.is_valid(&json!("Hello")));
476//!
477//! // Meta-schema validation
478//! assert!(jsonschema::draft7::meta::is_valid(&schema));
479//! # Ok(())
480//! # }
481//! ```
482//!
483//! You can also use the convenience [`is_valid`] and [`validate`] functions:
484//!
485//! ```rust
486//! use serde_json::json;
487//!
488//! let schema = json!({"type": "number", "minimum": 0});
489//! let instance = json!(42);
490//!
491//! assert!(jsonschema::draft202012::is_valid(&schema, &instance));
492//! assert!(jsonschema::draft202012::validate(&schema, &instance).is_ok());
493//! ```
494//!
495//! For more advanced configuration, you can use the draft-specific `options` function:
496//!
497//! ```rust
498//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
499//! use serde_json::json;
500//!
501//! let schema = json!({"type": "string", "format": "ends-with-42"});
502//! let validator = jsonschema::draft202012::options()
503//! .with_format("ends-with-42", |s| s.ends_with("42"))
504//! .should_validate_formats(true)
505//! .build(&schema)?;
506//!
507//! assert!(validator.is_valid(&json!("Hello 42")));
508//! assert!(!validator.is_valid(&json!("No!")));
509//! # Ok(())
510//! # }
511//! ```
512//!
513//! ## General Configuration
514//!
515//! For configuration options that are not draft-specific, `jsonschema` provides a builder via `jsonschema::options()`.
516//!
517//! Here's an example:
518//!
519//! ```rust
520//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
521//! use serde_json::json;
522//!
523//! let schema = json!({"type": "string"});
524//! let validator = jsonschema::options()
525//! // Add configuration options here
526//! .build(&schema)?;
527//!
528//! assert!(validator.is_valid(&json!("Hello")));
529//! # Ok(())
530//! # }
531//! ```
532//!
533//! For a complete list of configuration options and their usage, please refer to the [`ValidationOptions`] struct.
534//!
535//! ## Automatic Draft Detection
536//!
537//! If you don't need to specify a particular draft version, you can use `jsonschema::validator_for`
538//! which automatically detects the appropriate draft:
539//!
540//! ```rust
541//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
542//! use serde_json::json;
543//!
544//! let schema = json!({"$schema": "http://json-schema.org/draft-07/schema#", "type": "string"});
545//! let validator = jsonschema::validator_for(&schema)?;
546//!
547//! assert!(validator.is_valid(&json!("Hello")));
548//! # Ok(())
549//! # }
550//! ```
551//!
552//! # External References
553//!
554//! By default, `jsonschema` resolves HTTP references using `reqwest` and file references from the local file system.
555//! Both blocking and non-blocking retrieval is supported during validator construction. Note that the validation
556//! itself is always synchronous as it operates on in-memory data only.
557//!
558//! ```rust
559//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
560//! use serde_json::json;
561//!
562//! let schema = json!({"$schema": "http://json-schema.org/draft-07/schema#", "type": "string"});
563//!
564//! // Building a validator with blocking retrieval (default)
565//! let validator = jsonschema::validator_for(&schema)?;
566//!
567//! // Building a validator with non-blocking retrieval (requires `resolve-async` feature)
568//! # #[cfg(feature = "resolve-async")]
569//! let validator = jsonschema::async_validator_for(&schema).await?;
570//!
571//! // Validation is always synchronous
572//! assert!(validator.is_valid(&json!("Hello")));
573//! # Ok(())
574//! # }
575//! ```
576//!
577//! To enable HTTPS support, add the `rustls-tls` feature to `reqwest` in your `Cargo.toml`:
578//!
579//! ```toml
580//! reqwest = { version = "*", features = ["rustls-tls"] }
581//! ```
582//!
583//! You can disable the default behavior using crate features:
584//!
585//! - Disable HTTP resolving: `default-features = false, features = ["resolve-file"]`
586//! - Disable file resolving: `default-features = false, features = ["resolve-http", "tls-aws-lc-rs"]`
587//! - Enable async resolution: `features = ["resolve-async"]`
588//! - Disable all resolving: `default-features = false`
589//!
590//! ## Custom retrievers
591//!
592//! A retriever is for documents fetched on demand — from a database, an embedded asset, a cache.
593//! When the set of documents is known up front, put them in a [`Registry`] instead; the example
594//! below is a retriever only because it has to be one to demonstrate the trait.
595//!
596//! You can implement custom retrievers for both blocking and non-blocking retrieval:
597//!
598//! ```rust
599//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
600//! use std::{collections::HashMap, sync::Arc};
601//! use jsonschema::{Retrieve, Uri};
602//! use serde_json::{json, Value};
603//!
604//! struct InMemoryRetriever {
605//! schemas: HashMap<String, Value>,
606//! }
607//!
608//! impl Retrieve for InMemoryRetriever {
609//!
610//! fn retrieve(
611//! &self,
612//! uri: &Uri<String>,
613//! ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
614//! self.schemas
615//! .get(uri.as_str())
616//! .cloned()
617//! .ok_or_else(|| format!("Schema not found: {uri}").into())
618//! }
619//! }
620//!
621//! let mut schemas = HashMap::new();
622//! schemas.insert(
623//! "https://example.com/person.json".to_string(),
624//! json!({
625//! "type": "object",
626//! "properties": {
627//! "name": { "type": "string" },
628//! "age": { "type": "integer" }
629//! },
630//! "required": ["name", "age"]
631//! }),
632//! );
633//!
634//! let retriever = InMemoryRetriever { schemas };
635//!
636//! let schema = json!({
637//! "$ref": "https://example.com/person.json"
638//! });
639//!
640//! let validator = jsonschema::options()
641//! .with_retriever(retriever)
642//! .build(&schema)?;
643//!
644//! assert!(validator.is_valid(&json!({
645//! "name": "Alice",
646//! "age": 30
647//! })));
648//!
649//! assert!(!validator.is_valid(&json!({
650//! "name": "Bob"
651//! })));
652//! # Ok(())
653//! # }
654//! ```
655//!
656//! And non-blocking version with the `resolve-async` feature enabled:
657//!
658//! ```rust,no_run
659//! # #[cfg(all(
660//! # feature = "resolve-async",
661//! # any(
662//! # not(target_arch = "wasm32"),
663//! # all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")),
664//! # ),
665//! # ))]
666//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
667//! use jsonschema::{AsyncRetrieve, Registry, Resource, Uri};
668//! use serde_json::{Value, json};
669//!
670//! struct HttpRetriever;
671//!
672//! #[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
673//! #[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
674//! impl AsyncRetrieve for HttpRetriever {
675//! async fn retrieve(
676//! &self,
677//! uri: &Uri<String>,
678//! ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
679//! reqwest::get(uri.as_str())
680//! .await?
681//! .json()
682//! .await
683//! .map_err(Into::into)
684//! }
685//! }
686//!
687//! // Then use it to build a validator
688//! let validator = jsonschema::async_options()
689//! .with_retriever(HttpRetriever)
690//! .build(&json!({"$ref": "https://example.com/user.json"}))
691//! .await?;
692//! # Ok(())
693//! # }
694//! ```
695//!
696//! On `wasm32` targets, use `async_trait::async_trait(?Send)` so your retriever can rely on `Rc`, `JsFuture`, or other non-thread-safe types.
697//!
698//! ## Validating against schema definitions
699//!
700//! When working with large schemas containing multiple definitions (e.g., Open API schemas, DAP schemas),
701//! you may want to validate data against a specific definition rather than the entire schema. This can be
702//! achieved by registering the root schema as a resource and creating a wrapper schema that references
703//! the target definition:
704//!
705//! ```rust
706//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
707//! use serde_json::json;
708//! use jsonschema::{Registry, Resource};
709//!
710//! // Root schema with multiple definitions
711//! let root_schema = json!({
712//! "$id": "https://example.com/root",
713//! "definitions": {
714//! "User": {
715//! "type": "object",
716//! "properties": {
717//! "name": {"type": "string"},
718//! "age": {"type": "integer", "minimum": 0}
719//! },
720//! "required": ["name"]
721//! },
722//! "Product": {
723//! "type": "object",
724//! "properties": {
725//! "id": {"type": "integer"},
726//! "title": {"type": "string"}
727//! },
728//! "required": ["id", "title"]
729//! }
730//! }
731//! });
732//!
733//! // Create a schema that references the specific definition you want to validate against
734//! let user_schema = json!({"$ref": "https://example.com/root#/definitions/User"});
735//!
736//! let registry = Registry::new()
737//! .add("https://example.com/root", root_schema)?
738//! .prepare()?;
739//!
740//! // Build validator for the specific definition via the shared prepared registry
741//! let validator = jsonschema::options()
742//! .with_registry(®istry)
743//! .build(&user_schema)?;
744//!
745//! // Now validate data against just the User definition
746//! assert!(validator.is_valid(&json!({"name": "Alice", "age": 30})));
747//! assert!(!validator.is_valid(&json!({"age": 25}))); // Missing required "name"
748//! # Ok(())
749//! # }
750//! ```
751//!
752//! This pattern is particularly useful when:
753//! - Working with API schemas that define multiple request/response types
754//! - Validating configuration snippets against specific sections of a larger schema
755//! - Testing individual schema components in isolation
756//!
757//! ## Offline validation
758//!
759//! [`ValidationOptions::offline`] disables retrieval, so a reference that is not already in the
760//! registry fails at construction instead of being fetched:
761//!
762//! ```rust
763//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
764//! use serde_json::json;
765//!
766//! let schema = json!({"$ref": "https://example.com/schema.json"});
767//! assert!(jsonschema::options().offline().build(&schema).is_err());
768//! # Ok(())
769//! # }
770//! ```
771//!
772//! ## Bundling and dereferencing
773//!
774//! [`bundle`] embeds every referenced resource into the schema under `$defs` (`definitions` for
775//! Draft 4/6/7), keeping each `$id` and leaving `$ref` values unchanged. The result is a Compound
776//! Schema Document that needs no retrieval.
777//!
778//! [`dereference`] instead replaces each `$ref` with the schema it points to, for consumers that
779//! do not resolve references. Circular references are left in place.
780//!
781//! # Regular Expression Configuration
782//!
783//! The `jsonschema` crate allows configuring the regular expression engine used for validating
784//! keywords like `pattern` or `patternProperties`.
785//!
786//! By default, the crate uses [`fancy-regex`](https://docs.rs/fancy-regex), which supports advanced
787//! regular expression features such as lookaround and backreferences.
788//!
789//! The primary motivation for switching to the `regex` engine is security and performance:
790//! it guarantees linear-time matching, preventing potential Denial of Service attacks from malicious patterns
791//! in user-provided schemas while offering better performance with a smaller feature set.
792//!
793//! You can configure the engine at **runtime** using the [`PatternOptions`] API:
794//!
795//! ### Example: Configure `fancy-regex` with Backtracking Limit
796//!
797//! ```rust
798//! use serde_json::json;
799//! use jsonschema::PatternOptions;
800//!
801//! let schema = json!({
802//! "type": "string",
803//! "pattern": "^(a+)+$"
804//! });
805//!
806//! let validator = jsonschema::options()
807//! .with_pattern_options(
808//! PatternOptions::fancy_regex()
809//! .backtrack_limit(10_000)
810//! )
811//! .build(&schema)
812//! .expect("A valid schema");
813//! ```
814//!
815//! ### Example: Use the `regex` Engine Instead
816//!
817//! ```rust
818//! use serde_json::json;
819//! use jsonschema::PatternOptions;
820//!
821//! let schema = json!({
822//! "type": "string",
823//! "pattern": "^a+$"
824//! });
825//!
826//! let validator = jsonschema::options()
827//! .with_pattern_options(PatternOptions::regex())
828//! .build(&schema)
829//! .expect("A valid schema");
830//! ```
831//!
832//! ### Notes
833//!
834//! - If neither engine is explicitly set, `fancy-regex` is used by default.
835//! - Regular expressions that rely on advanced features like `(?<=...)` (lookbehind) or backreferences (`\1`) will fail with the `regex` engine.
836//!
837//! # Custom Keywords
838//!
839//! `jsonschema` allows you to extend its functionality by implementing custom validation logic through custom keywords.
840//! This feature is particularly useful when you need to validate against domain-specific rules that aren't covered by the standard JSON Schema keywords.
841//! Keywords are generic over the instance representation ([`json`] module).
842//!
843//! To implement a custom keyword, you need to:
844//! 1. Create a struct that implements the [`Keyword`] trait
845//! 2. Create a factory function or closure that produces instances of your custom keyword
846//! 3. Register the custom keyword with the [`Validator`] instance using the [`ValidationOptions::with_keyword`] method
847//!
848//! Here's a complete example:
849//!
850//! ```rust
851//! use jsonschema::{paths::Location, Keyword, ValidationError};
852//! use serde_json::{json, Map, Value};
853//!
854//! struct EvenNumberValidator;
855//!
856//! impl<'i> Keyword<'i> for EvenNumberValidator {
857//! fn validate(&self, instance: &'i Value) -> Result<(), ValidationError<'i>> {
858//! if let Some(n) = instance.as_u64() {
859//! if n % 2 == 0 {
860//! return Ok(());
861//! }
862//! }
863//! Err(ValidationError::custom("value must be an even integer"))
864//! }
865//!
866//! fn is_valid(&self, instance: &'i Value) -> bool {
867//! instance.as_u64().map_or(false, |n| n % 2 == 0)
868//! }
869//! }
870//!
871//! fn even_number_factory<'a>(
872//! _parent: &'a Map<String, Value>,
873//! value: &'a Value,
874//! _path: Location,
875//! ) -> Result<Box<dyn for<'i> Keyword<'i>>, ValidationError<'a>> {
876//! if value.as_bool() == Some(true) {
877//! Ok(Box::new(EvenNumberValidator))
878//! } else {
879//! Err(ValidationError::schema("The 'even-number' keyword must be set to true"))
880//! }
881//! }
882//!
883//! let schema = json!({"even-number": true, "type": "integer"});
884//! let validator = jsonschema::options()
885//! .with_keyword("even-number", even_number_factory)
886//! .build(&schema)
887//! .expect("Invalid schema");
888//!
889//! assert!(validator.is_valid(&json!(2)));
890//! assert!(!validator.is_valid(&json!(3)));
891//! assert!(!validator.is_valid(&json!("not a number")));
892//! ```
893//!
894//! In this example, we've created a custom `even-number` keyword that validates whether a number is even.
895//! The `EvenNumberValidator` implements the actual validation logic, while the `even_number_factory`
896//! creates instances of the validator and allows for additional configuration based on the keyword's value in the schema.
897//!
898//! You can also use a closure instead of a factory function for simpler cases:
899//!
900//! ```rust
901//! # use jsonschema::{paths::Location, Keyword, ValidationError};
902//! # use serde_json::{json, Map, Value};
903//! #
904//! # struct EvenNumberValidator;
905//! #
906//! # impl<'i> Keyword<'i> for EvenNumberValidator {
907//! # fn validate(&self, instance: &'i Value) -> Result<(), ValidationError<'i>> {
908//! # Ok(())
909//! # }
910//! #
911//! # fn is_valid(&self, instance: &'i Value) -> bool {
912//! # true
913//! # }
914//! # }
915//! let schema = json!({"even-number": true, "type": "integer"});
916//! let validator = jsonschema::options()
917//! .with_keyword("even-number", |_, _, _| {
918//! Ok(Box::new(EvenNumberValidator))
919//! })
920//! .build(&schema)
921//! .expect("Invalid schema");
922//! ```
923//!
924//! # Custom Formats
925//!
926//! JSON Schema allows for format validation through the `format` keyword. While `jsonschema`
927//! provides built-in validators for standard formats, you can also define custom format validators
928//! for domain-specific string formats.
929//!
930//! To implement a custom format validator:
931//!
932//! 1. Define a function or a closure that takes a `&str` and returns a `bool`.
933//! 2. Register the function with `jsonschema::options().with_format()`.
934//!
935//! ```rust
936//! use serde_json::json;
937//!
938//! // Step 1: Define the custom format validator function
939//! fn ends_with_42(s: &str) -> bool {
940//! s.ends_with("42!")
941//! }
942//!
943//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
944//! // Step 2: Create a schema using the custom format
945//! let schema = json!({
946//! "type": "string",
947//! "format": "ends-with-42"
948//! });
949//!
950//! // Step 3: Build the validator with the custom format
951//! let validator = jsonschema::options()
952//! .with_format("ends-with-42", ends_with_42)
953//! .with_format("ends-with-43", |s| s.ends_with("43!"))
954//! .should_validate_formats(true)
955//! .build(&schema)?;
956//!
957//! // Step 4: Validate instances
958//! assert!(validator.is_valid(&json!("Hello42!")));
959//! assert!(!validator.is_valid(&json!("Hello43!")));
960//! assert!(!validator.is_valid(&json!(42))); // Not a string
961//! # Ok(())
962//! # }
963//! ```
964//!
965//! ### Notes on Custom Format Validators
966//!
967//! - Custom format validators are only called for string instances.
968//! - In newer drafts, `format` is purely an annotation and won’t do any checking unless you
969//! opt in by calling `.should_validate_formats(true)` on your options builder. If you omit
970//! it, all `format` keywords are ignored at validation time.
971//!
972//! # Arbitrary Precision Numbers
973//!
974//! Enable the `arbitrary-precision` feature for exact validation of numbers beyond standard numeric ranges:
975//!
976//! ```toml
977//! jsonschema = { version = "x.y.z", features = ["arbitrary-precision"] }
978//! ```
979//!
980//! This provides:
981//! - Arbitrarily large integers (e.g., `18446744073709551616`)
982//! - Exact decimal precision without `f64` rounding (e.g., `0.1`, `0.3`)
983//!
984//! **Important**: Precision is only preserved when parsing JSON from strings. Using Rust literals
985//! or the `json!()` macro converts numbers to `f64`, losing precision.
986//!
987//! ```rust
988//! # use jsonschema::Validator;
989//! // Precision preserved - parsed from JSON string
990//! let schema = serde_json::from_str(r#"{"minimum": 0.1}"#)?;
991//! let instance = serde_json::from_str("0.3")?;
992//! let validator = Validator::new(&schema)?;
993//! assert!(validator.is_valid(&instance));
994//! # Ok::<(), Box<dyn std::error::Error>>(())
995//! ```
996//!
997//! # Custom JSON Representations
998//!
999//! Validators can accept instances in any in-memory JSON representation, not just
1000//! `serde_json::Value`. Implement the traits in the [`json`] module for your representation and
1001//! build validators with [`options_for`]:
1002//!
1003//! ```rust
1004//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
1005//! # type MyJson = jsonschema::json::SerdeJson;
1006//! # let schema = serde_json::json!({"type": "string"});
1007//! # let my_instance = &serde_json::json!("data");
1008//! let validator = jsonschema::options_for::<MyJson>().build(&schema)?;
1009//! assert!(validator.is_valid(my_instance));
1010//! # Ok(())
1011//! # }
1012//! ```
1013//!
1014//! See the [`json`] module for the full trait contract, a worked example, and how to verify an
1015//! implementation. The Python bindings use this mechanism to validate Python objects directly.
1016//!
1017//! # WebAssembly support
1018//!
1019//! `jsonschema` supports WebAssembly with different capabilities based on the target platform:
1020//!
1021//! ## Browser/JavaScript (`wasm32-unknown-unknown`)
1022//!
1023//! When targeting browser or JavaScript environments, external reference resolution is not
1024//! supported by default due to platform limitations:
1025//! - No filesystem access (`resolve-file` feature is not available)
1026//! - No synchronous HTTP requests (`resolve-http` feature is not available)
1027//!
1028//! To use `jsonschema` in these environments, disable default features:
1029//!
1030//! ```toml
1031//! jsonschema = { version = "x.y.z", default-features = false }
1032//! ```
1033//!
1034//! Note: Attempting to compile with `resolve-http` or `resolve-file` features on
1035//! `wasm32-unknown-unknown` will result in a compile error.
1036//!
1037//! For external references in browser environments, implement a custom retriever that uses
1038//! browser APIs (like `fetch`). See the [External References](#external-references) section.
1039//!
1040//! ## WASI (`wasm32-wasip1` / `wasm32-wasip2`)
1041//!
1042//! WASI environments (preview 1 and preview 2) can compile schemas and run validators, but the bundled
1043//! HTTP retriever depends on `reqwest`’s blocking client, which isn't available on these targets. Use
1044//! file access and custom retrievers instead.
1045//!
1046//! **Supported:**
1047//! - Blocking file resolution (`resolve-file` feature)
1048//! - Custom blocking retrievers (including wrapping async operations)
1049//! - Custom async retrievers via the `resolve-async` feature (for example, `jsonschema::async_options`
1050//! together with your own async runtime)
1051//!
1052//! **Not Supported:**
1053//! - The bundled HTTP retriever (depends on `reqwest`’s blocking client)
1054//!
1055//! ```toml
1056//! jsonschema = { version = "x.y.z", default-features = false, features = ["resolve-file"] }
1057//! ```
1058//!
1059//! **Workaround for HTTP:** Implement a custom blocking or async [`Retrieve`] that uses your preferred
1060//! HTTP client, and enable `resolve-async` if you want to build validators through `async_options()`
1061//! on WASI.
1062
1063#[cfg(all(
1064 target_arch = "wasm32",
1065 target_os = "unknown",
1066 any(feature = "resolve-file", feature = "resolve-http")
1067))]
1068compile_error!(
1069 "Features 'resolve-http' and 'resolve-file' are not supported on wasm32-unknown-unknown"
1070);
1071#[cfg(all(
1072 not(target_arch = "wasm32"),
1073 feature = "resolve-http",
1074 not(any(feature = "tls-aws-lc-rs", feature = "tls-ring"))
1075))]
1076compile_error!(
1077 "Feature `resolve-http` requires a TLS provider: enable `tls-aws-lc-rs` \
1078(default) or `tls-ring`."
1079);
1080
1081pub(crate) mod bundler;
1082pub mod canonical;
1083pub(crate) mod compiler;
1084mod content_encoding;
1085mod content_media_type;
1086pub(crate) mod dereferencer;
1087pub mod error;
1088mod evaluation;
1089pub(crate) use jsonschema_value::{
1090 cmp, numeric, unique, Array, Json, LazyInstance, Node, NodeIdentity, Object, SerdeJson,
1091};
1092/// Validating instances in a custom in-memory JSON representation.
1093///
1094/// Implement [`Json`], [`Node`], [`Object`], [`Array`], and [`JsonNumber`](json::JsonNumber)
1095/// for your representation, then build validators with [`options_for`]. Schemas are always
1096/// `serde_json::Value`; only instances use the custom representation. [`SerdeJson`] is the
1097/// built-in representation behind [`validator_for`] and the crate-level convenience functions.
1098///
1099/// Enable the `jsonb` feature for `json::Jsonb`, which reads a Postgres `jsonb` value in
1100/// place. It takes the container bytes without the varlena header, so detoast first:
1101///
1102/// ```rust,ignore
1103/// let detoasted = unsafe { pgrx::pg_sys::pg_detoast_datum_packed(datum.cast_mut_ptr()) };
1104/// let bytes = unsafe { pgrx::varlena_to_byte_slice(detoasted) };
1105/// validator.is_valid(Jsonb::root(bytes))
1106/// ```
1107///
1108/// The slice lives until the memory context resets. The bytes are in the server's native byte
1109/// order, and keys and strings are read as UTF-8. An error's instance is built on first access;
1110/// past 128 levels of nesting it holds `null` there and
1111/// `json::jsonb::take_pending_error` returns the error.
1112///
1113/// The accessors are infallible, so the representation must be total over JSON: reject nodes
1114/// with no JSON meaning (tags, foreign objects) before validation, or track them on a side
1115/// channel of the representation.
1116///
1117/// # Example
1118///
1119/// ```rust
1120/// use std::borrow::Cow;
1121///
1122/// use jsonschema::{
1123/// json::{Array, Json, JsonNumber, Node, NodeIdentity, Object},
1124/// JsonType,
1125/// };
1126/// use serde_json::Value;
1127///
1128/// #[derive(Default)]
1129/// enum ToyValue {
1130/// #[default]
1131/// Null,
1132/// Boolean(bool),
1133/// Number(f64),
1134/// String(String),
1135/// Array(Vec<ToyValue>),
1136/// Object(Vec<(String, ToyValue)>),
1137/// }
1138///
1139/// struct ToyJson;
1140///
1141/// impl Json for ToyJson {
1142/// // A cheap-to-`Clone` handle; `&ToyValue` plays the role `&serde_json::Value` does
1143/// // for the built-in representation.
1144/// type Node<'a> = &'a ToyValue;
1145/// // Property names are prepared once at schema compile time.
1146/// type PreparedKey = String;
1147/// // Scratch storage for nodes made from property names (`propertyNames`).
1148/// type StringBuffer = ToyValue;
1149///
1150/// fn prepare_key(key: &str) -> String {
1151/// key.to_owned()
1152/// }
1153///
1154/// fn with_string_node<T>(
1155/// buffer: &mut ToyValue,
1156/// string: &str,
1157/// f: impl FnOnce(&ToyValue) -> T,
1158/// ) -> T {
1159/// *buffer = ToyValue::String(string.to_owned());
1160/// f(buffer)
1161/// }
1162/// }
1163///
1164/// struct ToyNumber(f64);
1165///
1166/// impl JsonNumber for ToyNumber {
1167/// fn as_u64(&self) -> Option<u64> {
1168/// (self.0.fract() == 0.0 && self.0 >= 0.0).then_some(self.0 as u64)
1169/// }
1170/// fn as_i64(&self) -> Option<i64> {
1171/// (self.0.fract() == 0.0).then_some(self.0 as i64)
1172/// }
1173/// fn as_f64(&self) -> Option<f64> {
1174/// Some(self.0)
1175/// }
1176/// fn as_str(&self) -> Cow<'_, str> {
1177/// Cow::Owned(self.0.to_string())
1178/// }
1179/// fn to_number(&self) -> Cow<'_, serde_json::Number> {
1180/// Cow::Owned(serde_json::Number::from_f64(self.0).expect("finite"))
1181/// }
1182/// }
1183///
1184/// impl<'a> Node<'a, ToyJson> for &'a ToyValue {
1185/// type Object = &'a [(String, ToyValue)];
1186/// type Array = &'a [ToyValue];
1187/// type Number = ToyNumber;
1188///
1189/// fn as_object(&self) -> Option<&'a [(String, ToyValue)]> {
1190/// match self {
1191/// ToyValue::Object(members) => Some(members),
1192/// _ => None,
1193/// }
1194/// }
1195/// fn as_array(&self) -> Option<&'a [ToyValue]> {
1196/// match self {
1197/// ToyValue::Array(items) => Some(items),
1198/// _ => None,
1199/// }
1200/// }
1201/// fn as_string(&self) -> Option<Cow<'a, str>> {
1202/// match self {
1203/// ToyValue::String(string) => Some(Cow::Borrowed(string)),
1204/// _ => None,
1205/// }
1206/// }
1207/// fn as_number(&self) -> Option<ToyNumber> {
1208/// match self {
1209/// ToyValue::Number(number) => Some(ToyNumber(*number)),
1210/// _ => None,
1211/// }
1212/// }
1213/// fn as_boolean(&self) -> Option<bool> {
1214/// match self {
1215/// ToyValue::Boolean(boolean) => Some(*boolean),
1216/// _ => None,
1217/// }
1218/// }
1219/// fn is_null(&self) -> bool {
1220/// matches!(self, ToyValue::Null)
1221/// }
1222/// fn json_type(&self) -> JsonType {
1223/// match self {
1224/// ToyValue::Null => JsonType::Null,
1225/// ToyValue::Boolean(_) => JsonType::Boolean,
1226/// ToyValue::Number(_) => JsonType::Number,
1227/// ToyValue::String(_) => JsonType::String,
1228/// ToyValue::Array(_) => JsonType::Array,
1229/// ToyValue::Object(_) => JsonType::Object,
1230/// }
1231/// }
1232/// // Cold paths only: error messages, `const`/`enum` comparisons, `uniqueItems`.
1233/// fn to_value(&self) -> Cow<'a, Value> {
1234/// Cow::Owned(match self {
1235/// ToyValue::Null => Value::Null,
1236/// ToyValue::Boolean(boolean) => Value::Bool(*boolean),
1237/// ToyValue::Number(number) => serde_json::Number::from_f64(*number)
1238/// .map(Value::Number)
1239/// .expect("finite"),
1240/// ToyValue::String(string) => Value::String(string.clone()),
1241/// ToyValue::Array(items) => Value::Array(
1242/// items.iter().map(|item| item.to_value().into_owned()).collect(),
1243/// ),
1244/// ToyValue::Object(members) => Value::Object(
1245/// members
1246/// .iter()
1247/// .map(|(name, value)| (name.clone(), value.to_value().into_owned()))
1248/// .collect(),
1249/// ),
1250/// })
1251/// }
1252/// // Stable per live node; see the trait docs for the exact contract.
1253/// fn identity(&self) -> Option<NodeIdentity> {
1254/// Some(NodeIdentity::new(std::ptr::from_ref::<ToyValue>(*self) as usize))
1255/// }
1256/// }
1257///
1258/// impl<'a> Object<'a, ToyJson> for &'a [(String, ToyValue)] {
1259/// type Node = &'a ToyValue;
1260/// type MemberName = &'a str;
1261/// type MembersIter = ToyMembersIter<'a>;
1262///
1263/// fn len(&self) -> usize {
1264/// <[(String, ToyValue)]>::len(self)
1265/// }
1266/// fn get(&self, key: &String) -> Option<&'a ToyValue> {
1267/// self.iter().find(|(name, _)| name == key).map(|(_, value)| value)
1268/// }
1269/// fn members(&self) -> ToyMembersIter<'a> {
1270/// ToyMembersIter(self.iter())
1271/// }
1272/// }
1273///
1274/// struct ToyMembersIter<'a>(std::slice::Iter<'a, (String, ToyValue)>);
1275///
1276/// impl<'a> Iterator for ToyMembersIter<'a> {
1277/// type Item = (&'a str, &'a ToyValue);
1278/// fn next(&mut self) -> Option<Self::Item> {
1279/// self.0.next().map(|(name, value)| (name.as_str(), value))
1280/// }
1281/// }
1282///
1283/// impl<'a> Array<'a, ToyJson> for &'a [ToyValue] {
1284/// type Node = &'a ToyValue;
1285/// type ElementsIter = std::slice::Iter<'a, ToyValue>;
1286///
1287/// fn len(&self) -> usize {
1288/// <[ToyValue]>::len(self)
1289/// }
1290/// fn elements(&self) -> std::slice::Iter<'a, ToyValue> {
1291/// self.iter()
1292/// }
1293/// }
1294///
1295/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1296/// let schema = serde_json::json!({
1297/// "type": "object",
1298/// "properties": {"name": {"type": "string", "minLength": 2}},
1299/// "required": ["name"]
1300/// });
1301/// let validator = jsonschema::options_for::<ToyJson>().build(&schema)?;
1302/// # let _ = format!("{:?}", jsonschema::options_for::<ToyJson>());
1303///
1304/// let valid = ToyValue::Object(vec![("name".into(), ToyValue::String("bob".into()))]);
1305/// let invalid = ToyValue::Object(vec![]);
1306/// assert!(validator.is_valid(&valid));
1307/// let error = validator.validate(&invalid).expect_err("missing required");
1308/// assert_eq!(error.to_string(), "\"name\" is a required property");
1309/// # Ok(())
1310/// # }
1311/// ```
1312///
1313/// # Verifying an implementation
1314///
1315/// Enable the `conformance` feature, encode the `conformance::document()` JSON document in your
1316/// representation, and run `conformance::assert_conformance` on it; it checks the accessor
1317/// contract the validator relies on, including the subtle parts (code-point string lengths,
1318/// mathematical number equality, node identity stability).
1319///
1320/// # Custom keywords
1321///
1322/// [`Keyword`] is generic over the representation and operates on `F::Node` directly; register
1323/// implementations with [`ValidationOptions::with_keyword`](crate::ValidationOptions::with_keyword).
1324pub mod json {
1325 #[cfg(feature = "conformance")]
1326 pub use jsonschema_value::conformance;
1327 pub use jsonschema_value::{
1328 cmp, unique, Array, Json, JsonNumber, Node, NodeIdentity, Object, SerdeJson,
1329 };
1330 #[cfg(feature = "pyo3")]
1331 pub use jsonschema_value::{
1332 inspect, narrow_array, narrow_object, object_values, stock_enum_value, Pyo3,
1333 };
1334 #[cfg(feature = "jsonb")]
1335 pub use jsonschema_value::{jsonb, Jsonb, JsonbNode};
1336 #[cfg(feature = "magnus")]
1337 pub use jsonschema_value::{
1338 magnus_child, magnus_invalidate_members_cache, magnus_is_object, magnus_object_values,
1339 magnus_probe_root, magnus_string_node, magnus_take_pending_error, Magnus,
1340 MagnusPendingErrorScope, PendingError, RbNode,
1341 };
1342}
1343mod http;
1344mod keywords;
1345#[cfg(all(feature = "macros", not(target_family = "wasm")))]
1346mod meta_codegen;
1347mod node;
1348mod options;
1349pub mod output;
1350pub mod paths;
1351pub(crate) mod properties;
1352pub(crate) mod regex;
1353mod retriever;
1354mod unfinished_matches;
1355pub mod types {
1356 pub use jsonschema_value::types::{JsonType, JsonTypeSet, JsonTypeSetIterator};
1357}
1358mod validator;
1359
1360pub use canonical::CanonicalizationError;
1361pub use error::{
1362 ErrorIterator, MaskedValidationError, ValidationError, ValidationErrorParts, ValidationErrors,
1363};
1364pub use evaluation::{
1365 AnnotationEntry, ErrorEntry, Evaluation, FlagOutput, HierarchicalOutput, ListOutput,
1366};
1367pub use http::HttpOptions;
1368#[doc(inline)]
1369#[cfg(feature = "macros")]
1370pub use jsonschema_macros::validator;
1371pub use keywords::custom::Keyword;
1372pub use options::{EmailOptions, FancyRegex, PatternOptions, Regex, ValidationOptions};
1373pub use referencing::{
1374 uri, Draft, Error as ReferencingError, Registry, RegistryBuilder, Resource, Retrieve, Uri,
1375};
1376#[cfg(all(feature = "resolve-http", not(target_arch = "wasm32")))]
1377pub use retriever::{HttpRetriever, HttpRetrieverError};
1378pub use types::{JsonType, JsonTypeSet, JsonTypeSetIterator};
1379pub use validator::{ValidationContext, Validator, ValidatorMap};
1380
1381#[cfg(feature = "resolve-async")]
1382pub use referencing::AsyncRetrieve;
1383#[cfg(all(
1384 feature = "resolve-http",
1385 feature = "resolve-async",
1386 not(target_arch = "wasm32")
1387))]
1388pub use retriever::AsyncHttpRetriever;
1389
1390use serde_json::Value;
1391
1392/// Validate `instance` against `schema` and get a `true` if the instance is valid and `false`
1393/// otherwise. Draft is detected automatically.
1394///
1395/// # Examples
1396///
1397/// ```rust
1398/// use serde_json::json;
1399///
1400/// let schema = json!({"maxLength": 5});
1401/// let instance = json!("foo");
1402/// assert!(jsonschema::is_valid(&schema, &instance));
1403/// ```
1404///
1405/// # Panics
1406///
1407/// This function panics if an invalid schema is passed.
1408///
1409/// This function **must not** be called from within an async runtime if the schema contains
1410/// external references that require network requests, or it will panic when attempting to block.
1411/// Use `async_validator_for` for async contexts, or run this in a separate blocking thread
1412/// via `tokio::task::spawn_blocking`.
1413#[must_use]
1414#[inline]
1415pub fn is_valid(schema: &Value, instance: &Value) -> bool {
1416 validator_for(schema)
1417 .expect("Invalid schema")
1418 .is_valid(instance)
1419}
1420
1421/// Validate `instance` against `schema` and return the first error if any. Draft is detected automatically.
1422///
1423/// # Examples
1424///
1425/// ```rust
1426/// use serde_json::json;
1427///
1428/// let schema = json!({"maxLength": 5});
1429/// let instance = json!("foo");
1430/// assert!(jsonschema::validate(&schema, &instance).is_ok());
1431/// ```
1432///
1433/// # Errors
1434///
1435/// Returns the first [`ValidationError`] encountered when `instance` violates `schema`.
1436///
1437/// # Panics
1438///
1439/// This function panics if an invalid schema is passed.
1440///
1441/// This function **must not** be called from within an async runtime if the schema contains
1442/// external references that require network requests, or it will panic when attempting to block.
1443/// Use `async_validator_for` for async contexts, or run this in a separate blocking thread
1444/// via `tokio::task::spawn_blocking`.
1445#[inline]
1446pub fn validate<'i>(schema: &Value, instance: &'i Value) -> Result<(), ValidationError<'i>> {
1447 validator_for(schema)
1448 .expect("Invalid schema")
1449 .validate(instance)
1450}
1451
1452/// Evaluate `instance` against `schema` and return structured validation output. Draft is detected automatically.
1453///
1454/// Returns an [`Evaluation`] containing detailed validation results in JSON Schema Output v1 format,
1455/// including annotations and errors across the entire validation tree.
1456///
1457/// # Examples
1458///
1459/// ```rust
1460/// use serde_json::json;
1461///
1462/// let schema = json!({"type": "string", "minLength": 3});
1463/// let instance = json!("foo");
1464/// let evaluation = jsonschema::evaluate(&schema, &instance);
1465/// assert!(evaluation.flag().valid);
1466/// ```
1467///
1468/// # Panics
1469///
1470/// This function panics if an invalid schema is passed.
1471///
1472/// This function **must not** be called from within an async runtime if the schema contains
1473/// external references that require network requests, or it will panic when attempting to block.
1474/// Use `async_validator_for` for async contexts, or run this in a separate blocking thread
1475/// via `tokio::task::spawn_blocking`.
1476#[must_use]
1477#[inline]
1478pub fn evaluate(schema: &Value, instance: &Value) -> Evaluation {
1479 validator_for(schema)
1480 .expect("Invalid schema")
1481 .evaluate(instance)
1482}
1483
1484/// Create a validator for the input schema with automatic draft detection and default options.
1485///
1486/// # Examples
1487///
1488/// ```rust
1489/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1490/// use serde_json::json;
1491///
1492/// let schema = json!({"minimum": 5});
1493/// let instance = json!(42);
1494///
1495/// let validator = jsonschema::validator_for(&schema)?;
1496/// assert!(validator.is_valid(&instance));
1497/// # Ok(())
1498/// # }
1499/// ```
1500///
1501/// # Errors
1502///
1503/// Returns an error if the schema is invalid or external references cannot be resolved.
1504///
1505/// # Panics
1506///
1507/// This function **must not** be called from within an async runtime if the schema contains
1508/// external references that require network requests, or it will panic when attempting to block.
1509/// Use `async_validator_for` for async contexts, or run this in a separate blocking thread
1510/// via `tokio::task::spawn_blocking`.
1511pub fn validator_for(schema: &Value) -> Result<Validator, ValidationError<'static>> {
1512 Validator::new(schema)
1513}
1514
1515/// Create a [`ValidatorMap`] from the input schema using automatic draft detection and
1516/// default options.
1517///
1518/// Every reachable subschema is compiled eagerly. The root schema is always present
1519/// under the key `"#"`. Subschemas that fail to compile (e.g. unresolvable `$ref`) are
1520/// silently omitted.
1521///
1522/// # Examples
1523///
1524/// ```rust
1525/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1526/// use serde_json::json;
1527///
1528/// let schema = json!({
1529/// "$defs": {
1530/// "User": {"type": "object", "required": ["name"]}
1531/// }
1532/// });
1533///
1534/// let map = jsonschema::validator_map_for(&schema)?;
1535///
1536/// let user_validator = map.get("#/$defs/User").unwrap();
1537/// assert!(user_validator.is_valid(&json!({"name": "Alice"})));
1538/// assert!(!user_validator.is_valid(&json!({})));
1539/// # Ok(())
1540/// # }
1541/// ```
1542///
1543/// # Errors
1544///
1545/// Returns an error if the schema is invalid or external references cannot be resolved.
1546///
1547/// # Panics
1548///
1549/// This function **must not** be called from within an async runtime if the schema contains
1550/// external references that require network requests, or it will panic when attempting to block.
1551/// Use `async_validator_map_for` for async contexts, or run this in a separate blocking thread
1552/// via `tokio::task::spawn_blocking`.
1553pub fn validator_map_for(schema: &Value) -> Result<ValidatorMap, ValidationError<'static>> {
1554 options().build_map(schema)
1555}
1556
1557/// Embed all external `$ref` targets into a draft-appropriate container,
1558/// producing a Compound Schema Document that validates identically to the original.
1559/// Draft 4/6/7 use `definitions`; Draft 2019-09/2020-12 use `$defs`.
1560/// `$ref` values are preserved unchanged.
1561/// For mixed-draft bundles, embedded resources may include both `id` and `$id`
1562/// to maximize interoperability with downstream validators that differ in draft
1563/// handling.
1564///
1565/// **Limitation:** `$dynamicRef` is not followed during bundling.
1566///
1567/// For custom resources or retrievers, use [`options()`] and call `.bundle()`.
1568///
1569/// # Errors
1570///
1571/// Returns an error if draft detection fails, registry construction fails,
1572/// subresource scope resolution fails, or any `$ref` cannot be resolved.
1573///
1574/// # Panics
1575///
1576/// This function **must not** be called from within an async runtime if the schema contains
1577/// external references that require network requests, or it will panic when attempting to block.
1578/// Use `async_options().bundle()` for async contexts, or run this in a separate blocking thread
1579/// via `tokio::task::spawn_blocking`.
1580///
1581/// # Examples
1582///
1583/// ```rust
1584/// use serde_json::json;
1585///
1586/// let schema = json!({"type": "string"});
1587/// let bundled = jsonschema::bundle(&schema).expect("bundling failed");
1588/// assert_eq!(bundled, schema); // no external refs, returned unchanged
1589/// ```
1590pub fn bundle(schema: &Value) -> Result<Value, ReferencingError> {
1591 options().bundle(schema)
1592}
1593
1594/// Dereference a JSON Schema by recursively replacing all `$ref` values
1595/// with the schemas they point to.
1596///
1597/// Circular references are left in place as `$ref` strings.
1598///
1599/// For custom retriever, registry, draft, or base URI, use [`options()`] and call `.dereference()`.
1600///
1601/// # Errors
1602///
1603/// Returns an error if any `$ref` cannot be resolved (e.g. points to an
1604/// external URI not present in the schema and no retriever is available).
1605///
1606/// # Example
1607///
1608/// ```rust
1609/// use serde_json::json;
1610///
1611/// let schema = json!({
1612/// "$defs": {"tag": {"type": "string"}},
1613/// "properties": {"name": {"$ref": "#/$defs/tag"}}
1614/// });
1615/// let result = jsonschema::dereference(&schema).expect("dereference failed");
1616/// assert_eq!(result["properties"]["name"]["type"], "string");
1617/// ```
1618pub fn dereference(schema: &Value) -> Result<Value, ReferencingError> {
1619 options().dereference(schema)
1620}
1621
1622/// Bundle a JSON Schema into a Compound Schema Document,
1623/// using async retrieval for external references.
1624///
1625/// Async counterpart to [`bundle`]. For custom resources or retrievers,
1626/// use [`async_options()`] and call `.bundle()`.
1627///
1628/// # Errors
1629///
1630/// Returns an error if draft detection fails, registry construction fails,
1631/// subresource scope resolution fails, or any `$ref` cannot be resolved.
1632#[cfg(feature = "resolve-async")]
1633pub async fn async_bundle(schema: &Value) -> Result<Value, ReferencingError> {
1634 async_options().bundle(schema).await
1635}
1636
1637/// Dereference a JSON Schema asynchronously.
1638///
1639/// Async counterpart to [`dereference`]. For custom resources or retrievers,
1640/// use [`async_options()`] and call `.dereference()`.
1641///
1642/// # Errors
1643///
1644/// Returns an error if any `$ref` cannot be resolved.
1645#[cfg(feature = "resolve-async")]
1646pub async fn async_dereference(schema: &Value) -> Result<Value, ReferencingError> {
1647 async_options().dereference(schema).await
1648}
1649
1650/// Create a validator for the input schema with automatic draft detection and default options,
1651/// using non-blocking retrieval for external references.
1652///
1653/// This is the async counterpart to [`validator_for`]. Note that only the construction is
1654/// asynchronous - validation itself is always synchronous.
1655///
1656/// # Examples
1657///
1658/// ```rust
1659/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1660/// use serde_json::json;
1661///
1662/// let schema = json!({
1663/// "type": "object",
1664/// "properties": {
1665/// "user": { "$ref": "https://example.com/user.json" }
1666/// }
1667/// });
1668///
1669/// let validator = jsonschema::async_validator_for(&schema).await?;
1670/// assert!(validator.is_valid(&json!({"user": {"name": "Alice"}})));
1671/// # Ok(())
1672/// # }
1673/// ```
1674///
1675/// # Errors
1676///
1677/// Returns an error if the schema is invalid or external references cannot be resolved.
1678#[cfg(feature = "resolve-async")]
1679pub async fn async_validator_for(schema: &Value) -> Result<Validator, ValidationError<'static>> {
1680 Validator::async_new(schema).await
1681}
1682
1683/// Create a [`ValidatorMap`] from the input schema using async retrieval for external references.
1684///
1685/// Async counterpart to [`validator_map_for`]. Note that only construction is asynchronous —
1686/// validation itself is always synchronous.
1687///
1688/// # Examples
1689///
1690/// ```rust
1691/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1692/// use serde_json::json;
1693///
1694/// let schema = json!({"$defs": {"User": {"type": "object", "required": ["name"]}}});
1695/// let map = jsonschema::async_validator_map_for(&schema).await?;
1696/// assert!(map["#/$defs/User"].is_valid(&json!({"name": "Bob"})));
1697/// # Ok(())
1698/// # }
1699/// ```
1700///
1701/// # Errors
1702///
1703/// Returns an error if the schema is invalid or external references cannot be resolved.
1704#[cfg(feature = "resolve-async")]
1705pub async fn async_validator_map_for(
1706 schema: &Value,
1707) -> Result<ValidatorMap, ValidationError<'static>> {
1708 async_options().build_map(schema).await
1709}
1710
1711/// Reduce a JSON Schema to a normal form that accepts the same values.
1712///
1713/// Experimental: keyword coverage is incomplete and the API may change in minor releases.
1714///
1715/// Use [`canonical::options`](fn@canonical::options) to configure canonicalization.
1716///
1717/// A schema the canonical form cannot model exactly still succeeds, as a `Raw` result holding the
1718/// original document; see [Unsupported schemas](canonical#unsupported-schemas).
1719///
1720/// # Errors
1721///
1722/// Returns [`CanonicalizationError`] when the input is not a valid JSON Schema document or a reference cannot be resolved.
1723pub fn canonicalize(value: &Value) -> Result<canonical::CanonicalSchema, CanonicalizationError> {
1724 canonical::options().canonicalize(value)
1725}
1726
1727/// Create a builder for configuring JSON Schema validation options.
1728///
1729/// This function returns a [`ValidationOptions`] struct, which allows you to set various
1730/// options for JSON Schema validation. You can use this builder to specify
1731/// the draft version, set custom formats, and more.
1732///
1733/// If [`with_draft`](ValidationOptions::with_draft) is not called, the draft is
1734/// auto-detected from the schema's `$schema` field — the same behaviour as [`validator_for`].
1735///
1736/// **Note:** When calling [`ValidationOptions::build`], it **must not** be called from within
1737/// an async runtime if the schema contains external references that require network requests,
1738/// or it will panic. Use `async_options` for async contexts.
1739///
1740/// # Examples
1741///
1742/// Basic usage with draft specification:
1743///
1744/// ```
1745/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1746/// use serde_json::json;
1747/// use jsonschema::Draft;
1748///
1749/// let schema = json!({"type": "string"});
1750/// let validator = jsonschema::options()
1751/// .with_draft(Draft::Draft7)
1752/// .build(&schema)?;
1753///
1754/// assert!(validator.is_valid(&json!("Hello")));
1755/// # Ok(())
1756/// # }
1757/// ```
1758///
1759/// Advanced configuration:
1760///
1761/// ```
1762/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1763/// use serde_json::json;
1764///
1765/// let schema = json!({"type": "string", "format": "custom"});
1766/// let validator = jsonschema::options()
1767/// .with_format("custom", |value| value.len() == 3)
1768/// .should_validate_formats(true)
1769/// .build(&schema)?;
1770///
1771/// assert!(validator.is_valid(&json!("abc")));
1772/// assert!(!validator.is_valid(&json!("abcd")));
1773/// # Ok(())
1774/// # }
1775/// ```
1776///
1777/// See [`ValidationOptions`] for all available configuration options.
1778#[must_use]
1779pub fn options<'i>() -> ValidationOptions<'i> {
1780 Validator::options()
1781}
1782
1783/// Create a builder whose validators accept instances in the JSON representation `F`.
1784///
1785/// Same configuration surface as [`options()`]; `build` yields a `Validator<F>`.
1786#[must_use]
1787pub fn options_for<'i, F: Json>() -> ValidationOptions<'i, std::sync::Arc<dyn Retrieve>, F> {
1788 ValidationOptions::default()
1789}
1790
1791/// Create a builder whose validators accept instances in the JSON representation `F`, with
1792/// async retrieval of external references.
1793///
1794/// Same configuration surface as [`async_options()`]; `build` yields a `Validator<F>`.
1795#[cfg(feature = "resolve-async")]
1796#[must_use]
1797pub fn async_options_for<'i, F: Json>(
1798) -> ValidationOptions<'i, std::sync::Arc<dyn AsyncRetrieve>, F> {
1799 ValidationOptions::default()
1800}
1801
1802/// Create a builder for configuring JSON Schema validation options.
1803///
1804/// This function returns a [`ValidationOptions`] struct which allows you to set various options for JSON Schema validation.
1805/// External references will be retrieved using non-blocking I/O.
1806///
1807/// # Examples
1808///
1809/// Basic usage with external references:
1810///
1811/// ```rust
1812/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1813/// use serde_json::json;
1814///
1815/// let schema = json!({
1816/// "$ref": "https://example.com/user.json"
1817/// });
1818///
1819/// let validator = jsonschema::async_options()
1820/// .build(&schema)
1821/// .await?;
1822///
1823/// assert!(validator.is_valid(&json!({"name": "Alice"})));
1824/// # Ok(())
1825/// # }
1826/// ```
1827///
1828/// Advanced configuration:
1829///
1830/// ```rust
1831/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1832/// use serde_json::{Value, json};
1833/// use jsonschema::{Draft, AsyncRetrieve, Uri};
1834///
1835/// // Custom async retriever
1836/// struct MyRetriever;
1837///
1838/// #[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
1839/// #[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
1840/// impl AsyncRetrieve for MyRetriever {
1841/// async fn retrieve(&self, uri: &Uri<String>) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
1842/// // Custom retrieval logic
1843/// Ok(json!({}))
1844/// }
1845/// }
1846///
1847/// let schema = json!({
1848/// "$ref": "https://example.com/user.json"
1849/// });
1850/// let validator = jsonschema::async_options()
1851/// .with_draft(Draft::Draft202012)
1852/// .with_retriever(MyRetriever)
1853/// .build(&schema)
1854/// .await?;
1855/// # Ok(())
1856/// # }
1857/// ```
1858///
1859/// On `wasm32` targets, annotate your implementation with `async_trait::async_trait(?Send)` to drop the `Send + Sync` requirement.
1860///
1861/// See [`ValidationOptions`] for all available configuration options.
1862#[cfg(feature = "resolve-async")]
1863#[must_use]
1864pub fn async_options<'i>() -> ValidationOptions<'i, std::sync::Arc<dyn AsyncRetrieve>> {
1865 Validator::async_options()
1866}
1867
1868/// Functionality for validating JSON Schema documents against their meta-schemas.
1869pub mod meta {
1870 use crate::{error::ValidationError, Draft, Json, Node, Object, Registry, Validator};
1871 use ahash::AHashSet;
1872 use referencing::Retrieve;
1873 use serde_json::Value;
1874 use std::{
1875 any::{Any, TypeId},
1876 borrow::Cow,
1877 sync::{OnceLock, RwLock},
1878 };
1879
1880 pub use validator_handle::MetaValidator;
1881
1882 /// Meta-schema validators that read a schema held as a Python object.
1883 ///
1884 /// The bundled drafts are compiled in, so a schema already in Python form is checked without
1885 /// being converted to [`serde_json::Value`]. A `$schema` outside them is reached through
1886 /// [`is_valid_for`] / [`validate_for`] instead.
1887 ///
1888 /// Needs the `macros` feature; without it, use [`is_valid_for`] / [`validate_for`] with
1889 /// [`json::Pyo3`](crate::json::Pyo3).
1890 #[cfg(all(feature = "macros", feature = "pyo3", not(target_family = "wasm")))]
1891 pub mod pyo3 {
1892 use crate::Draft;
1893 use ::pyo3::{types::PyAny, Borrowed};
1894
1895 pub use crate::meta_codegen::pyo3::{is_valid_fn, validate_fn, IsValidFn, ValidateFn};
1896
1897 /// The draft whose meta-schema `schema` names in `$schema`.
1898 ///
1899 /// `Draft::Unknown` means a URI outside the bundled drafts, which the functions above
1900 /// cannot answer for.
1901 #[must_use]
1902 pub fn draft_of(schema: Borrowed<'_, '_, PyAny>) -> Draft {
1903 super::meta_cache::<crate::json::Pyo3>().draft_of(&schema)
1904 }
1905 }
1906
1907 /// Meta-schema validators that read a schema held as a Ruby object.
1908 ///
1909 /// The bundled drafts are compiled in, so a schema already in Ruby form is checked without
1910 /// being converted to [`serde_json::Value`]. A `$schema` outside them is reached through
1911 /// [`is_valid_for`] / [`validate_for`] instead.
1912 ///
1913 /// Needs the `macros` feature; without it, use [`is_valid_for`] / [`validate_for`] with
1914 /// [`json::Magnus`](crate::json::Magnus).
1915 #[cfg(all(feature = "macros", feature = "magnus", not(target_family = "wasm")))]
1916 pub mod magnus {
1917 use crate::{json::RbNode, Draft};
1918
1919 pub use crate::meta_codegen::magnus::{is_valid_fn, validate_fn, IsValidFn, ValidateFn};
1920
1921 /// The draft whose meta-schema `schema` names in `$schema`.
1922 ///
1923 /// `Draft::Unknown` means a URI outside the bundled drafts, which the functions above
1924 /// cannot answer for.
1925 #[must_use]
1926 pub fn draft_of(schema: RbNode<'_>) -> Draft {
1927 super::meta_cache::<crate::json::Magnus>().draft_of(&schema)
1928 }
1929 }
1930
1931 /// Create a meta-validation options builder.
1932 ///
1933 /// # Examples
1934 ///
1935 /// ```rust
1936 /// use serde_json::json;
1937 /// use jsonschema::{Registry, Resource};
1938 ///
1939 /// let custom_meta = Resource::from_contents(json!({
1940 /// "$schema": "https://json-schema.org/draft/2020-12/schema",
1941 /// "type": "object"
1942 /// }));
1943 ///
1944 /// let registry = Registry::new()
1945 /// .add("http://example.com/meta", custom_meta)
1946 /// .unwrap()
1947 /// .prepare()
1948 /// .unwrap();
1949 ///
1950 /// let schema = json!({
1951 /// "$schema": "http://example.com/meta",
1952 /// "type": "string"
1953 /// });
1954 ///
1955 /// assert!(jsonschema::meta::options()
1956 /// .with_registry(®istry)
1957 /// .is_valid(&schema));
1958 /// ```
1959 #[must_use]
1960 pub fn options<'a>() -> MetaSchemaOptions<'a> {
1961 MetaSchemaOptions::default()
1962 }
1963
1964 /// Options for meta-schema validation.
1965 #[derive(Clone, Default)]
1966 pub struct MetaSchemaOptions<'a> {
1967 registry: Option<&'a Registry<'a>>,
1968 }
1969
1970 impl<'a> MetaSchemaOptions<'a> {
1971 /// Use a registry for resolving custom meta-schemas.
1972 ///
1973 /// # Examples
1974 ///
1975 /// ```rust
1976 /// use serde_json::json;
1977 /// use jsonschema::{Registry, Resource};
1978 ///
1979 /// let custom_meta = Resource::from_contents(json!({
1980 /// "$schema": "https://json-schema.org/draft/2020-12/schema",
1981 /// "type": "object"
1982 /// }));
1983 ///
1984 /// let registry = Registry::new()
1985 /// .add("http://example.com/meta", custom_meta)
1986 /// .unwrap()
1987 /// .prepare()
1988 /// .unwrap();
1989 ///
1990 /// let options = jsonschema::meta::options()
1991 /// .with_registry(®istry);
1992 /// ```
1993 #[must_use]
1994 pub fn with_registry(mut self, registry: &'a Registry<'a>) -> Self {
1995 self.registry = Some(registry);
1996 self
1997 }
1998
1999 /// Check if a schema is valid according to its meta-schema.
2000 ///
2001 /// # Panics
2002 ///
2003 /// Panics if the meta-schema cannot be resolved.
2004 #[must_use]
2005 pub fn is_valid(&self, schema: &Value) -> bool {
2006 match try_meta_validator_for(schema, self.registry) {
2007 Ok(validator) => validator.is_valid(schema),
2008 Err(e) => panic!("Failed to resolve meta-schema: {e}"),
2009 }
2010 }
2011
2012 /// Validate a schema according to its meta-schema.
2013 ///
2014 /// # Errors
2015 ///
2016 /// Returns [`ValidationError`] if the schema is invalid or if the meta-schema cannot be resolved.
2017 pub fn validate<'schema>(
2018 &self,
2019 schema: &'schema Value,
2020 ) -> Result<(), ValidationError<'schema>> {
2021 let validator = try_meta_validator_for(schema, self.registry)?;
2022 validator.validate(schema)
2023 }
2024 }
2025
2026 mod validator_handle {
2027 use crate::{ValidationError, Validator};
2028 use serde_json::Value;
2029 use std::{marker::PhantomData, ops::Deref};
2030
2031 /// Handle to a draft-specific meta-schema [`Validator`]. Borrows cached validators on native
2032 /// targets and owns validators on `wasm32`.
2033 pub struct MetaValidator<'a>(MetaValidatorInner<'a>);
2034
2035 // Native builds can hand out references to cached validators or own dynamic ones,
2036 // while wasm targets need owned instances because the validator type does not implement `Sync` there.
2037 // Under `macros`, native builds dispatch to compile-time generated validators; the cached
2038 // runtime validator is resolved lazily by draft only for the `evaluate` path (`AsRef`).
2039 enum MetaValidatorInner<'a> {
2040 #[cfg(all(not(feature = "macros"), not(target_family = "wasm")))]
2041 Borrowed(&'a Validator),
2042 Owned(Box<Validator>, PhantomData<&'a Validator>),
2043 #[cfg(all(feature = "macros", not(target_family = "wasm")))]
2044 Generated {
2045 draft: crate::Draft,
2046 is_valid: fn(&Value) -> bool,
2047 validate: crate::meta_codegen::ValidateFn,
2048 iter_errors: crate::meta_codegen::IterErrorsFn,
2049 },
2050 }
2051
2052 // `borrowed` is the only method using `'a`; it is absent on wasm and under `macros`.
2053 #[cfg_attr(
2054 any(target_family = "wasm", feature = "macros"),
2055 allow(clippy::elidable_lifetime_names)
2056 )]
2057 impl<'a> MetaValidator<'a> {
2058 #[cfg(all(not(feature = "macros"), not(target_family = "wasm")))]
2059 pub(crate) fn borrowed(validator: &'a Validator) -> Self {
2060 Self(MetaValidatorInner::Borrowed(validator))
2061 }
2062
2063 pub(crate) fn owned(validator: Validator) -> Self {
2064 Self(MetaValidatorInner::Owned(Box::new(validator), PhantomData))
2065 }
2066
2067 #[cfg(all(feature = "macros", not(target_family = "wasm")))]
2068 pub(crate) fn generated(draft: crate::Draft) -> Self {
2069 Self(MetaValidatorInner::Generated {
2070 draft,
2071 is_valid: crate::meta_codegen::is_valid_fn(draft),
2072 validate: crate::meta_codegen::validate_fn(draft),
2073 iter_errors: crate::meta_codegen::iter_errors_fn(draft),
2074 })
2075 }
2076
2077 /// Validate `schema` against the meta-schema, returning `true` if valid.
2078 #[must_use]
2079 pub fn is_valid(&self, schema: &Value) -> bool {
2080 match &self.0 {
2081 #[cfg(all(feature = "macros", not(target_family = "wasm")))]
2082 MetaValidatorInner::Generated { is_valid, .. } => is_valid(schema),
2083 _ => self.as_ref().is_valid(schema),
2084 }
2085 }
2086
2087 /// Validate `schema` against the meta-schema, returning the first error if any.
2088 ///
2089 /// # Errors
2090 ///
2091 /// Returns the first [`ValidationError`] describing why the schema violates the meta-schema.
2092 pub fn validate<'i>(&self, schema: &'i Value) -> Result<(), ValidationError<'i>> {
2093 match &self.0 {
2094 #[cfg(all(feature = "macros", not(target_family = "wasm")))]
2095 MetaValidatorInner::Generated { validate, .. } => validate(schema),
2096 _ => self.as_ref().validate(schema),
2097 }
2098 }
2099
2100 /// Validate `schema` against the meta-schema, yielding every error.
2101 #[must_use]
2102 pub fn iter_errors<'i>(&'i self, schema: &'i Value) -> crate::ErrorIterator<'i> {
2103 match &self.0 {
2104 #[cfg(all(feature = "macros", not(target_family = "wasm")))]
2105 MetaValidatorInner::Generated { iter_errors, .. } => iter_errors(schema),
2106 _ => self.as_ref().iter_errors(schema),
2107 }
2108 }
2109 }
2110
2111 impl AsRef<Validator> for MetaValidator<'_> {
2112 fn as_ref(&self) -> &Validator {
2113 match &self.0 {
2114 #[cfg(all(not(feature = "macros"), not(target_family = "wasm")))]
2115 MetaValidatorInner::Borrowed(validator) => validator,
2116 MetaValidatorInner::Owned(validator, _) => validator,
2117 // `evaluate` has no generated counterpart; fall back to the runtime validator.
2118 #[cfg(all(feature = "macros", not(target_family = "wasm")))]
2119 MetaValidatorInner::Generated { draft, .. } => {
2120 crate::meta::validators::runtime_validator_for_draft(*draft)
2121 }
2122 }
2123 }
2124 }
2125
2126 impl Deref for MetaValidator<'_> {
2127 type Target = Validator;
2128
2129 fn deref(&self) -> &Self::Target {
2130 self.as_ref()
2131 }
2132 }
2133 }
2134
2135 pub(crate) mod validators {
2136 use crate::Validator;
2137 #[cfg(not(target_family = "wasm"))]
2138 use std::sync::LazyLock;
2139
2140 fn build_validator(schema: &serde_json::Value) -> Validator {
2141 crate::options()
2142 .without_schema_validation()
2143 .build(schema)
2144 .expect("Meta-schema should be valid")
2145 }
2146
2147 #[cfg(not(target_family = "wasm"))]
2148 pub(crate) static DRAFT4_META_VALIDATOR: LazyLock<Validator> =
2149 LazyLock::new(|| build_validator(&referencing::meta::DRAFT4));
2150 #[cfg(target_family = "wasm")]
2151 pub(crate) fn draft4_meta_validator() -> Validator {
2152 build_validator(&referencing::meta::DRAFT4)
2153 }
2154
2155 #[cfg(not(target_family = "wasm"))]
2156 pub(crate) static DRAFT6_META_VALIDATOR: LazyLock<Validator> =
2157 LazyLock::new(|| build_validator(&referencing::meta::DRAFT6));
2158 #[cfg(target_family = "wasm")]
2159 pub(crate) fn draft6_meta_validator() -> Validator {
2160 build_validator(&referencing::meta::DRAFT6)
2161 }
2162
2163 #[cfg(not(target_family = "wasm"))]
2164 pub(crate) static DRAFT7_META_VALIDATOR: LazyLock<Validator> =
2165 LazyLock::new(|| build_validator(&referencing::meta::DRAFT7));
2166 #[cfg(target_family = "wasm")]
2167 pub(crate) fn draft7_meta_validator() -> Validator {
2168 build_validator(&referencing::meta::DRAFT7)
2169 }
2170
2171 #[cfg(not(target_family = "wasm"))]
2172 pub(crate) static DRAFT201909_META_VALIDATOR: LazyLock<Validator> =
2173 LazyLock::new(|| build_validator(&referencing::meta::DRAFT201909));
2174 #[cfg(target_family = "wasm")]
2175 pub(crate) fn draft201909_meta_validator() -> Validator {
2176 build_validator(&referencing::meta::DRAFT201909)
2177 }
2178
2179 #[cfg(not(target_family = "wasm"))]
2180 pub(crate) static DRAFT202012_META_VALIDATOR: LazyLock<Validator> =
2181 LazyLock::new(|| build_validator(&referencing::meta::DRAFT202012));
2182 #[cfg(target_family = "wasm")]
2183 pub(crate) fn draft202012_meta_validator() -> Validator {
2184 build_validator(&referencing::meta::DRAFT202012)
2185 }
2186
2187 // Backs the `evaluate`/`iter_errors` fallback for generated meta validators.
2188 #[cfg(all(feature = "macros", not(target_family = "wasm")))]
2189 pub(crate) fn runtime_validator_for_draft(draft: crate::Draft) -> &'static Validator {
2190 use crate::Draft;
2191 match draft {
2192 Draft::Draft4 => &DRAFT4_META_VALIDATOR,
2193 Draft::Draft6 => &DRAFT6_META_VALIDATOR,
2194 Draft::Draft7 => &DRAFT7_META_VALIDATOR,
2195 Draft::Draft201909 => &DRAFT201909_META_VALIDATOR,
2196 _ => &DRAFT202012_META_VALIDATOR,
2197 }
2198 }
2199 }
2200
2201 pub(crate) fn validator_for_draft(draft: Draft) -> MetaValidator<'static> {
2202 #[cfg(all(feature = "macros", not(target_family = "wasm")))]
2203 {
2204 MetaValidator::generated(draft)
2205 }
2206 #[cfg(all(not(feature = "macros"), not(target_family = "wasm")))]
2207 {
2208 match draft {
2209 Draft::Draft4 => MetaValidator::borrowed(&validators::DRAFT4_META_VALIDATOR),
2210 Draft::Draft6 => MetaValidator::borrowed(&validators::DRAFT6_META_VALIDATOR),
2211 Draft::Draft7 => MetaValidator::borrowed(&validators::DRAFT7_META_VALIDATOR),
2212 Draft::Draft201909 => {
2213 MetaValidator::borrowed(&validators::DRAFT201909_META_VALIDATOR)
2214 }
2215 // Draft202012, Unknown, or any future draft variants
2216 _ => MetaValidator::borrowed(&validators::DRAFT202012_META_VALIDATOR),
2217 }
2218 }
2219 #[cfg(target_family = "wasm")]
2220 {
2221 let validator = match draft {
2222 Draft::Draft4 => validators::draft4_meta_validator(),
2223 Draft::Draft6 => validators::draft6_meta_validator(),
2224 Draft::Draft7 => validators::draft7_meta_validator(),
2225 Draft::Draft201909 => validators::draft201909_meta_validator(),
2226 // Draft202012, Unknown, or any future draft variants
2227 _ => validators::draft202012_meta_validator(),
2228 };
2229 MetaValidator::owned(validator)
2230 }
2231 }
2232
2233 /// Validate a JSON Schema document against its meta-schema and get a `true` if the schema is valid
2234 /// and `false` otherwise. Draft version is detected automatically.
2235 ///
2236 /// # Examples
2237 ///
2238 /// ```rust
2239 /// use serde_json::json;
2240 ///
2241 /// let schema = json!({
2242 /// "type": "string",
2243 /// "maxLength": 5
2244 /// });
2245 /// assert!(jsonschema::meta::is_valid(&schema));
2246 /// ```
2247 ///
2248 /// # Panics
2249 ///
2250 /// This function panics if the meta-schema can't be detected.
2251 ///
2252 /// # Note
2253 ///
2254 /// This helper only works with the built-in JSON Schema drafts. For schemas that declare a
2255 /// custom `$schema`, construct a registry that contains your meta-schema and use
2256 /// [`meta::options().with_registry(...)`](crate::meta::options) to validate it.
2257 #[must_use]
2258 pub fn is_valid(schema: &Value) -> bool {
2259 match try_meta_validator_for(schema, None) {
2260 Ok(validator) => validator.is_valid(schema),
2261 Err(error) => panic!("Failed to resolve meta-schema: {error}"),
2262 }
2263 }
2264 /// Validate a JSON Schema document against its meta-schema and return the first error if any.
2265 /// Draft version is detected automatically.
2266 ///
2267 /// # Examples
2268 ///
2269 /// ```rust
2270 /// use serde_json::json;
2271 ///
2272 /// let schema = json!({
2273 /// "type": "string",
2274 /// "maxLength": 5
2275 /// });
2276 /// assert!(jsonschema::meta::validate(&schema).is_ok());
2277 ///
2278 /// // Invalid schema
2279 /// let invalid_schema = json!({
2280 /// "type": "invalid_type"
2281 /// });
2282 /// assert!(jsonschema::meta::validate(&invalid_schema).is_err());
2283 /// ```
2284 ///
2285 /// # Errors
2286 ///
2287 /// Returns the first [`ValidationError`] describing why the schema violates the detected meta-schema.
2288 ///
2289 /// # Panics
2290 ///
2291 /// This function panics if the meta-schema can't be detected.
2292 ///
2293 /// # Note
2294 ///
2295 /// Like [`is_valid`], this helper only handles the bundled JSON
2296 /// Schema drafts. For custom meta-schemas, use [`meta::options().with_registry(...)`](crate::meta::options)
2297 /// so the registry can supply the meta-schema.
2298 pub fn validate(schema: &Value) -> Result<(), ValidationError<'_>> {
2299 let validator = try_meta_validator_for(schema, None)?;
2300 validator.validate(schema)
2301 }
2302
2303 /// Build a validator for a JSON Schema's meta-schema.
2304 /// Draft version is detected automatically.
2305 ///
2306 /// Returns a [`MetaValidator`] that can be used to validate the schema or access
2307 /// structured validation output via the evaluate API.
2308 ///
2309 /// # Examples
2310 ///
2311 /// ```rust
2312 /// use serde_json::json;
2313 ///
2314 /// let schema = json!({
2315 /// "type": "string",
2316 /// "maxLength": 5
2317 /// });
2318 ///
2319 /// let validator = jsonschema::meta::validator_for(&schema)
2320 /// .expect("Valid meta-schema");
2321 ///
2322 /// // Use evaluate API for structured output
2323 /// let evaluation = validator.evaluate(&schema);
2324 /// assert!(evaluation.flag().valid);
2325 /// ```
2326 ///
2327 /// # Errors
2328 ///
2329 /// Returns [`ValidationError`] if the meta-schema cannot be resolved or built.
2330 ///
2331 /// # Panics
2332 ///
2333 /// This function panics if the meta-schema can't be detected.
2334 ///
2335 /// # Note
2336 ///
2337 /// This helper only handles the bundled JSON Schema drafts. For custom meta-schemas,
2338 /// use [`meta::options().with_registry(...)`](crate::meta::options).
2339 pub fn validator_for(
2340 schema: &Value,
2341 ) -> Result<MetaValidator<'static>, ValidationError<'static>> {
2342 try_meta_validator_for(schema, None)
2343 }
2344
2345 /// Validate a schema document held in a foreign representation against its meta-schema.
2346 /// Draft version is detected automatically.
2347 ///
2348 /// # Errors
2349 ///
2350 /// Returns the first [`ValidationError`], or a referencing error if the meta-schema cannot be
2351 /// resolved.
2352 ///
2353 /// # Panics
2354 ///
2355 /// Panics if a bundled meta-schema fails to build.
2356 pub fn validate_for<F: Json>(schema: F::Node<'_>) -> Result<(), ValidationError<'_>> {
2357 let cache = meta_cache::<F>();
2358 match cache.draft_of(&schema) {
2359 Draft::Unknown => custom_meta_validator(cache, &schema)?.validate(schema),
2360 draft => cache.validator(draft).validate(schema),
2361 }
2362 }
2363
2364 /// Check a schema document held in a foreign representation against its meta-schema.
2365 /// Draft version is detected automatically.
2366 ///
2367 /// # Errors
2368 ///
2369 /// Returns a referencing error if the meta-schema cannot be resolved.
2370 ///
2371 /// # Panics
2372 ///
2373 /// Panics if a bundled meta-schema fails to build.
2374 pub fn is_valid_for<F: Json>(schema: F::Node<'_>) -> Result<bool, ValidationError<'static>> {
2375 let cache = meta_cache::<F>();
2376 Ok(match cache.draft_of(&schema) {
2377 Draft::Unknown => custom_meta_validator(cache, &schema)?.is_valid(schema),
2378 draft => cache.validator(draft).is_valid(schema),
2379 })
2380 }
2381
2382 /// Meta-schemas outside the bundled drafts are resolved through the `$schema` chain.
2383 fn custom_meta_validator<F: Json>(
2384 cache: &MetaCache<F>,
2385 schema: &F::Node<'_>,
2386 ) -> Result<Validator<F>, ValidationError<'static>> {
2387 let uri = cache
2388 .meta_schema_uri(schema)
2389 .expect("`$schema` must exist when draft is Unknown")
2390 .into_owned();
2391 let (custom_meta_schema, resolved_draft) = resolve_meta_schema_chain(&uri)?;
2392 crate::options_for::<F>()
2393 .with_draft(resolved_draft)
2394 .without_schema_validation()
2395 .build(&custom_meta_schema)
2396 }
2397
2398 /// A `Validator<F>` only accepts nodes of the representation it was built for, so meta-schema
2399 /// state is shared per `F` rather than globally.
2400 struct MetaCache<F: Json> {
2401 schema_key: F::PreparedKey,
2402 validators: [OnceLock<Validator<F>>; 5],
2403 }
2404
2405 impl<F: Json> MetaCache<F> {
2406 fn meta_schema_uri<'a>(&self, schema: &F::Node<'a>) -> Option<Cow<'a, str>> {
2407 schema.as_object()?.get(&self.schema_key)?.as_string()
2408 }
2409
2410 fn draft_of(&self, schema: &F::Node<'_>) -> Draft {
2411 self.meta_schema_uri(schema)
2412 .map_or_else(Draft::default, |uri| Draft::from_schema_uri(&uri))
2413 }
2414
2415 fn validator(&self, draft: Draft) -> &Validator<F> {
2416 let (index, meta_schema) = match draft {
2417 Draft::Draft4 => (0, &referencing::meta::DRAFT4),
2418 Draft::Draft6 => (1, &referencing::meta::DRAFT6),
2419 Draft::Draft7 => (2, &referencing::meta::DRAFT7),
2420 Draft::Draft201909 => (3, &referencing::meta::DRAFT201909),
2421 // Draft202012, Unknown, or any future draft variants
2422 _ => (4, &referencing::meta::DRAFT202012),
2423 };
2424 self.validators[index].get_or_init(|| {
2425 crate::options_for::<F>()
2426 .without_schema_validation()
2427 .build(meta_schema)
2428 .expect("Meta-schema should be valid")
2429 })
2430 }
2431 }
2432
2433 /// Statics cannot be generic over `F`, so entries are found by type. There is one per
2434 /// representation reaching this code, which a linear scan covers.
2435 fn meta_cache<F: Json>() -> &'static MetaCache<F> {
2436 static CACHE: RwLock<Vec<(TypeId, &'static (dyn Any + Send + Sync))>> =
2437 RwLock::new(Vec::new());
2438
2439 fn find<F: Json>(
2440 entries: &[(TypeId, &'static (dyn Any + Send + Sync))],
2441 ) -> Option<&'static MetaCache<F>> {
2442 let type_id = TypeId::of::<F>();
2443 entries.iter().find(|(id, _)| *id == type_id).map(|(_, e)| {
2444 e.downcast_ref()
2445 .expect("Entries are found by representation type")
2446 })
2447 }
2448
2449 if let Some(cache) = find::<F>(&CACHE.read().expect("Meta-validator cache is poisoned")) {
2450 return cache;
2451 }
2452 let mut entries = CACHE.write().expect("Meta-validator cache is poisoned");
2453 if let Some(cache) = find::<F>(&entries) {
2454 return cache;
2455 }
2456 let cache: &'static MetaCache<F> = Box::leak(Box::new(MetaCache {
2457 schema_key: F::prepare_key("$schema"),
2458 validators: Default::default(),
2459 }));
2460 entries.push((TypeId::of::<F>(), cache));
2461 cache
2462 }
2463
2464 fn try_meta_validator_for<'a>(
2465 schema: &Value,
2466 registry: Option<&'a Registry<'a>>,
2467 ) -> Result<MetaValidator<'a>, ValidationError<'static>> {
2468 let draft = Draft::default().detect(schema);
2469
2470 // For custom meta-schemas (Draft::Unknown), attempt to resolve the meta-schema
2471 if draft == Draft::Unknown {
2472 if let Some(meta_schema_uri) = schema
2473 .as_object()
2474 .and_then(|obj| obj.get("$schema"))
2475 .and_then(|s| s.as_str())
2476 {
2477 // Try registry first if available
2478 if let Some(registry) = registry {
2479 let (custom_meta_schema, resolved_draft) =
2480 resolve_meta_schema_with_registry(meta_schema_uri, registry)?;
2481 let validator = crate::options()
2482 .with_draft(resolved_draft)
2483 .with_registry(registry)
2484 .with_base_uri(meta_schema_uri.trim_end_matches('#'))
2485 .without_schema_validation()
2486 .build(&custom_meta_schema)?;
2487 return Ok(MetaValidator::owned(validator));
2488 }
2489
2490 // Use default retriever
2491 let (custom_meta_schema, resolved_draft) =
2492 resolve_meta_schema_chain(meta_schema_uri)?;
2493 let validator = crate::options()
2494 .with_draft(resolved_draft)
2495 .without_schema_validation()
2496 .build(&custom_meta_schema)?;
2497 return Ok(MetaValidator::owned(validator));
2498 }
2499 }
2500
2501 Ok(validator_for_draft(draft))
2502 }
2503
2504 fn resolve_meta_schema_with_registry(
2505 uri: &str,
2506 registry: &Registry<'_>,
2507 ) -> Result<(Value, Draft), ValidationError<'static>> {
2508 let resolver = registry.resolver(referencing::uri::from_str(uri)?);
2509 let first_resolved = resolver.lookup("")?;
2510 let first_meta_schema = first_resolved.contents().clone();
2511
2512 let draft = walk_meta_schema_chain(uri, |current_uri| {
2513 let resolver = registry.resolver(referencing::uri::from_str(current_uri)?);
2514 let resolved = resolver.lookup("")?;
2515 Ok(resolved.contents().clone())
2516 })?;
2517
2518 Ok((first_meta_schema, draft))
2519 }
2520
2521 fn resolve_meta_schema_chain(uri: &str) -> Result<(Value, Draft), ValidationError<'static>> {
2522 let retriever = crate::retriever::DefaultRetriever;
2523 let first_meta_uri = referencing::uri::from_str(uri)?;
2524 let first_meta_schema = retriever
2525 .retrieve(&first_meta_uri)
2526 .map_err(|e| referencing::Error::unretrievable(uri, e))?;
2527
2528 let draft = walk_meta_schema_chain(uri, |current_uri| {
2529 let meta_uri = referencing::uri::from_str(current_uri)?;
2530 retriever
2531 .retrieve(&meta_uri)
2532 .map_err(|e| referencing::Error::unretrievable(current_uri, e))
2533 })?;
2534
2535 Ok((first_meta_schema, draft))
2536 }
2537
2538 pub(crate) fn walk_meta_schema_chain(
2539 start_uri: &str,
2540 mut fetch: impl FnMut(&str) -> Result<Value, referencing::Error>,
2541 ) -> Result<Draft, referencing::Error> {
2542 let mut visited = AHashSet::new();
2543 let mut current_uri = start_uri.to_string();
2544
2545 loop {
2546 if !visited.insert(current_uri.clone()) {
2547 return Err(referencing::Error::circular_metaschema(current_uri));
2548 }
2549
2550 let meta_schema = fetch(¤t_uri)?;
2551 let draft = Draft::default().detect(&meta_schema);
2552
2553 if draft != Draft::Unknown {
2554 return Ok(draft);
2555 }
2556
2557 current_uri = meta_schema
2558 .get("$schema")
2559 .and_then(|s| s.as_str())
2560 .expect("`$schema` must exist when draft is Unknown")
2561 .to_string();
2562 }
2563 }
2564}
2565
2566/// Functionality specific to JSON Schema Draft 4.
2567///
2568/// [](https://bowtie.report/#/implementations/rust-jsonschema)
2569///
2570/// This module provides functions for creating validators and performing validation
2571/// according to the JSON Schema Draft 4 specification.
2572///
2573/// # Examples
2574///
2575/// ```rust
2576/// use serde_json::json;
2577///
2578/// let schema = json!({"type": "number", "multipleOf": 2});
2579/// let instance = json!(4);
2580///
2581/// assert!(jsonschema::draft4::is_valid(&schema, &instance));
2582/// ```
2583pub mod draft4 {
2584 use super::{Draft, ValidationError, ValidationOptions, Validator, Value};
2585
2586 /// Create a new JSON Schema validator using Draft 4 specifications.
2587 ///
2588 /// # Examples
2589 ///
2590 /// ```rust
2591 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
2592 /// use serde_json::json;
2593 ///
2594 /// let schema = json!({"minimum": 5});
2595 /// let instance = json!(42);
2596 ///
2597 /// let validator = jsonschema::draft4::new(&schema)?;
2598 /// assert!(validator.is_valid(&instance));
2599 /// # Ok(())
2600 /// # }
2601 /// ```
2602 ///
2603 /// # Errors
2604 ///
2605 /// Returns an error if the schema is not a valid Draft 4 document or if referenced resources
2606 /// cannot be resolved.
2607 pub fn new(schema: &Value) -> Result<Validator, ValidationError<'static>> {
2608 options().build(schema)
2609 }
2610 /// Validate an instance against a schema using Draft 4 specifications without creating a validator.
2611 ///
2612 /// # Examples
2613 ///
2614 /// ```rust
2615 /// use serde_json::json;
2616 ///
2617 /// let schema = json!({"minimum": 5});
2618 /// let valid = json!(42);
2619 /// let invalid = json!(3);
2620 ///
2621 /// assert!(jsonschema::draft4::is_valid(&schema, &valid));
2622 /// assert!(!jsonschema::draft4::is_valid(&schema, &invalid));
2623 /// ```
2624 ///
2625 /// # Panics
2626 ///
2627 /// Panics if `schema` cannot be compiled into a Draft 4 validator.
2628 #[must_use]
2629 pub fn is_valid(schema: &Value, instance: &Value) -> bool {
2630 new(schema).expect("Invalid schema").is_valid(instance)
2631 }
2632 /// Validate an instance against a schema using Draft 4 specifications without creating a validator.
2633 ///
2634 /// # Examples
2635 ///
2636 /// ```rust
2637 /// use serde_json::json;
2638 ///
2639 /// let schema = json!({"minimum": 5});
2640 /// let valid = json!(42);
2641 /// let invalid = json!(3);
2642 ///
2643 /// assert!(jsonschema::draft4::validate(&schema, &valid).is_ok());
2644 /// assert!(jsonschema::draft4::validate(&schema, &invalid).is_err());
2645 /// ```
2646 ///
2647 /// # Errors
2648 ///
2649 /// Returns the first [`ValidationError`] when `instance` violates the schema.
2650 ///
2651 /// # Panics
2652 ///
2653 /// Panics if `schema` cannot be compiled into a Draft 4 validator.
2654 pub fn validate<'i>(schema: &Value, instance: &'i Value) -> Result<(), ValidationError<'i>> {
2655 new(schema).expect("Invalid schema").validate(instance)
2656 }
2657 /// Creates a [`ValidationOptions`] builder pre-configured for JSON Schema Draft 4.
2658 ///
2659 /// This function provides a shorthand for `jsonschema::options().with_draft(Draft::Draft4)`.
2660 ///
2661 /// # Examples
2662 ///
2663 /// ```
2664 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
2665 /// use serde_json::json;
2666 ///
2667 /// let schema = json!({"type": "string", "format": "ends-with-42"});
2668 /// let validator = jsonschema::draft4::options()
2669 /// .with_format("ends-with-42", |s| s.ends_with("42"))
2670 /// .should_validate_formats(true)
2671 /// .build(&schema)?;
2672 ///
2673 /// assert!(validator.is_valid(&json!("Hello 42")));
2674 /// assert!(!validator.is_valid(&json!("No!")));
2675 /// # Ok(())
2676 /// # }
2677 /// ```
2678 ///
2679 /// See [`ValidationOptions`] for all available configuration options.
2680 #[must_use]
2681 pub fn options<'i>() -> ValidationOptions<'i> {
2682 crate::options().with_draft(Draft::Draft4)
2683 }
2684
2685 /// Functionality for validating JSON Schema Draft 4 documents.
2686 pub mod meta {
2687 use crate::{meta::MetaValidator, ValidationError};
2688 use serde_json::Value;
2689
2690 /// Returns a handle to the Draft 4 meta-schema validator. Native targets borrow cached
2691 /// statics while `wasm32` builds an owned validator.
2692 #[must_use]
2693 pub fn validator() -> MetaValidator<'static> {
2694 crate::meta::validator_for_draft(super::Draft::Draft4)
2695 }
2696
2697 /// Validate a JSON Schema document against Draft 4 meta-schema and get a `true` if the schema is valid
2698 /// and `false` otherwise.
2699 ///
2700 /// # Examples
2701 ///
2702 /// ```rust
2703 /// use serde_json::json;
2704 ///
2705 /// let schema = json!({
2706 /// "type": "string",
2707 /// "maxLength": 5
2708 /// });
2709 /// assert!(jsonschema::draft4::meta::is_valid(&schema));
2710 /// ```
2711 #[must_use]
2712 #[inline]
2713 pub fn is_valid(schema: &Value) -> bool {
2714 validator().is_valid(schema)
2715 }
2716
2717 /// Validate a JSON Schema document against Draft 4 meta-schema and return the first error if any.
2718 ///
2719 /// # Examples
2720 ///
2721 /// ```rust
2722 /// use serde_json::json;
2723 ///
2724 /// let schema = json!({
2725 /// "type": "string",
2726 /// "maxLength": 5
2727 /// });
2728 /// assert!(jsonschema::draft4::meta::validate(&schema).is_ok());
2729 ///
2730 /// // Invalid schema
2731 /// let invalid_schema = json!({
2732 /// "type": "invalid_type"
2733 /// });
2734 /// assert!(jsonschema::draft4::meta::validate(&invalid_schema).is_err());
2735 /// ```
2736 ///
2737 /// # Errors
2738 ///
2739 /// Returns the first [`ValidationError`] describing why the schema violates the Draft 4 meta-schema.
2740 #[inline]
2741 pub fn validate(schema: &Value) -> Result<(), ValidationError<'_>> {
2742 validator().validate(schema)
2743 }
2744 }
2745}
2746
2747/// Functionality specific to JSON Schema Draft 6.
2748///
2749/// [](https://bowtie.report/#/implementations/rust-jsonschema)
2750///
2751/// This module provides functions for creating validators and performing validation
2752/// according to the JSON Schema Draft 6 specification.
2753///
2754/// # Examples
2755///
2756/// ```rust
2757/// use serde_json::json;
2758///
2759/// let schema = json!({"type": "string", "format": "uri"});
2760/// let instance = json!("https://www.example.com");
2761///
2762/// assert!(jsonschema::draft6::is_valid(&schema, &instance));
2763/// ```
2764pub mod draft6 {
2765 use super::{Draft, ValidationError, ValidationOptions, Validator, Value};
2766
2767 /// Create a new JSON Schema validator using Draft 6 specifications.
2768 ///
2769 /// # Examples
2770 ///
2771 /// ```rust
2772 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
2773 /// use serde_json::json;
2774 ///
2775 /// let schema = json!({"minimum": 5});
2776 /// let instance = json!(42);
2777 ///
2778 /// let validator = jsonschema::draft6::new(&schema)?;
2779 /// assert!(validator.is_valid(&instance));
2780 /// # Ok(())
2781 /// # }
2782 /// ```
2783 ///
2784 /// # Errors
2785 ///
2786 /// Returns an error if the schema is not a valid Draft 6 document or if referenced resources
2787 /// cannot be resolved.
2788 pub fn new(schema: &Value) -> Result<Validator, ValidationError<'static>> {
2789 options().build(schema)
2790 }
2791 /// Validate an instance against a schema using Draft 6 specifications without creating a validator.
2792 ///
2793 /// # Examples
2794 ///
2795 /// ```rust
2796 /// use serde_json::json;
2797 ///
2798 /// let schema = json!({"minimum": 5});
2799 /// let valid = json!(42);
2800 /// let invalid = json!(3);
2801 ///
2802 /// assert!(jsonschema::draft6::is_valid(&schema, &valid));
2803 /// assert!(!jsonschema::draft6::is_valid(&schema, &invalid));
2804 /// ```
2805 ///
2806 /// # Panics
2807 ///
2808 /// Panics if `schema` cannot be compiled into a Draft 6 validator.
2809 #[must_use]
2810 pub fn is_valid(schema: &Value, instance: &Value) -> bool {
2811 new(schema).expect("Invalid schema").is_valid(instance)
2812 }
2813 /// Validate an instance against a schema using Draft 6 specifications without creating a validator.
2814 ///
2815 /// # Examples
2816 ///
2817 /// ```rust
2818 /// use serde_json::json;
2819 ///
2820 /// let schema = json!({"minimum": 5});
2821 /// let valid = json!(42);
2822 /// let invalid = json!(3);
2823 ///
2824 /// assert!(jsonschema::draft6::validate(&schema, &valid).is_ok());
2825 /// assert!(jsonschema::draft6::validate(&schema, &invalid).is_err());
2826 /// ```
2827 ///
2828 /// # Errors
2829 ///
2830 /// Returns the first [`ValidationError`] when `instance` violates the schema.
2831 ///
2832 /// # Panics
2833 ///
2834 /// Panics if `schema` cannot be compiled into a Draft 6 validator.
2835 pub fn validate<'i>(schema: &Value, instance: &'i Value) -> Result<(), ValidationError<'i>> {
2836 new(schema).expect("Invalid schema").validate(instance)
2837 }
2838 /// Creates a [`ValidationOptions`] builder pre-configured for JSON Schema Draft 6.
2839 ///
2840 /// This function provides a shorthand for `jsonschema::options().with_draft(Draft::Draft6)`.
2841 ///
2842 /// # Examples
2843 ///
2844 /// ```
2845 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
2846 /// use serde_json::json;
2847 ///
2848 /// let schema = json!({"type": "string", "format": "ends-with-42"});
2849 /// let validator = jsonschema::draft6::options()
2850 /// .with_format("ends-with-42", |s| s.ends_with("42"))
2851 /// .should_validate_formats(true)
2852 /// .build(&schema)?;
2853 ///
2854 /// assert!(validator.is_valid(&json!("Hello 42")));
2855 /// assert!(!validator.is_valid(&json!("No!")));
2856 /// # Ok(())
2857 /// # }
2858 /// ```
2859 ///
2860 /// See [`ValidationOptions`] for all available configuration options.
2861 #[must_use]
2862 pub fn options<'i>() -> ValidationOptions<'i> {
2863 crate::options().with_draft(Draft::Draft6)
2864 }
2865
2866 /// Functionality for validating JSON Schema Draft 6 documents.
2867 pub mod meta {
2868 use crate::{meta::MetaValidator, ValidationError};
2869 use serde_json::Value;
2870
2871 /// Returns a handle to the Draft 6 meta-schema validator. Native targets borrow cached
2872 /// statics while `wasm32` builds an owned validator.
2873 #[must_use]
2874 pub fn validator() -> MetaValidator<'static> {
2875 crate::meta::validator_for_draft(super::Draft::Draft6)
2876 }
2877
2878 /// Validate a JSON Schema document against Draft 6 meta-schema and get a `true` if the schema is valid
2879 /// and `false` otherwise.
2880 ///
2881 /// # Examples
2882 ///
2883 /// ```rust
2884 /// use serde_json::json;
2885 ///
2886 /// let schema = json!({
2887 /// "type": "string",
2888 /// "maxLength": 5
2889 /// });
2890 /// assert!(jsonschema::draft6::meta::is_valid(&schema));
2891 /// ```
2892 #[must_use]
2893 #[inline]
2894 pub fn is_valid(schema: &Value) -> bool {
2895 validator().is_valid(schema)
2896 }
2897
2898 /// Validate a JSON Schema document against Draft 6 meta-schema and return the first error if any.
2899 ///
2900 /// # Examples
2901 ///
2902 /// ```rust
2903 /// use serde_json::json;
2904 ///
2905 /// let schema = json!({
2906 /// "type": "string",
2907 /// "maxLength": 5
2908 /// });
2909 /// assert!(jsonschema::draft6::meta::validate(&schema).is_ok());
2910 ///
2911 /// // Invalid schema
2912 /// let invalid_schema = json!({
2913 /// "type": "invalid_type"
2914 /// });
2915 /// assert!(jsonschema::draft6::meta::validate(&invalid_schema).is_err());
2916 /// ```
2917 ///
2918 /// # Errors
2919 ///
2920 /// Returns the first [`ValidationError`] describing why the schema violates the Draft 6 meta-schema.
2921 #[inline]
2922 pub fn validate(schema: &Value) -> Result<(), ValidationError<'_>> {
2923 validator().validate(schema)
2924 }
2925 }
2926}
2927
2928/// Functionality specific to JSON Schema Draft 7.
2929///
2930/// [](https://bowtie.report/#/implementations/rust-jsonschema)
2931///
2932/// This module provides functions for creating validators and performing validation
2933/// according to the JSON Schema Draft 7 specification.
2934///
2935/// # Examples
2936///
2937/// ```rust
2938/// use serde_json::json;
2939///
2940/// let schema = json!({"type": "string", "pattern": "^[a-zA-Z0-9]+$"});
2941/// let instance = json!("abc123");
2942///
2943/// assert!(jsonschema::draft7::is_valid(&schema, &instance));
2944/// ```
2945pub mod draft7 {
2946 use super::{Draft, ValidationError, ValidationOptions, Validator, Value};
2947
2948 /// Create a new JSON Schema validator using Draft 7 specifications.
2949 ///
2950 /// # Examples
2951 ///
2952 /// ```rust
2953 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
2954 /// use serde_json::json;
2955 ///
2956 /// let schema = json!({"minimum": 5});
2957 /// let instance = json!(42);
2958 ///
2959 /// let validator = jsonschema::draft7::new(&schema)?;
2960 /// assert!(validator.is_valid(&instance));
2961 /// # Ok(())
2962 /// # }
2963 /// ```
2964 ///
2965 /// # Errors
2966 ///
2967 /// Returns an error if the schema is not a valid Draft 7 document or if referenced resources
2968 /// cannot be resolved.
2969 pub fn new(schema: &Value) -> Result<Validator, ValidationError<'static>> {
2970 options().build(schema)
2971 }
2972 /// Validate an instance against a schema using Draft 7 specifications without creating a validator.
2973 ///
2974 /// # Examples
2975 ///
2976 /// ```rust
2977 /// use serde_json::json;
2978 ///
2979 /// let schema = json!({"minimum": 5});
2980 /// let valid = json!(42);
2981 /// let invalid = json!(3);
2982 ///
2983 /// assert!(jsonschema::draft7::is_valid(&schema, &valid));
2984 /// assert!(!jsonschema::draft7::is_valid(&schema, &invalid));
2985 /// ```
2986 ///
2987 /// # Panics
2988 ///
2989 /// Panics if `schema` cannot be compiled into a Draft 7 validator.
2990 #[must_use]
2991 pub fn is_valid(schema: &Value, instance: &Value) -> bool {
2992 new(schema).expect("Invalid schema").is_valid(instance)
2993 }
2994 /// Validate an instance against a schema using Draft 7 specifications without creating a validator.
2995 ///
2996 /// # Examples
2997 ///
2998 /// ```rust
2999 /// use serde_json::json;
3000 ///
3001 /// let schema = json!({"minimum": 5});
3002 /// let valid = json!(42);
3003 /// let invalid = json!(3);
3004 ///
3005 /// assert!(jsonschema::draft7::validate(&schema, &valid).is_ok());
3006 /// assert!(jsonschema::draft7::validate(&schema, &invalid).is_err());
3007 /// ```
3008 ///
3009 /// # Errors
3010 ///
3011 /// Returns the first [`ValidationError`] when `instance` violates the schema.
3012 ///
3013 /// # Panics
3014 ///
3015 /// Panics if `schema` cannot be compiled into a Draft 7 validator.
3016 pub fn validate<'i>(schema: &Value, instance: &'i Value) -> Result<(), ValidationError<'i>> {
3017 new(schema).expect("Invalid schema").validate(instance)
3018 }
3019 /// Creates a [`ValidationOptions`] builder pre-configured for JSON Schema Draft 7.
3020 ///
3021 /// This function provides a shorthand for `jsonschema::options().with_draft(Draft::Draft7)`.
3022 ///
3023 /// # Examples
3024 ///
3025 /// ```
3026 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
3027 /// use serde_json::json;
3028 ///
3029 /// let schema = json!({"type": "string", "format": "ends-with-42"});
3030 /// let validator = jsonschema::draft7::options()
3031 /// .with_format("ends-with-42", |s| s.ends_with("42"))
3032 /// .should_validate_formats(true)
3033 /// .build(&schema)?;
3034 ///
3035 /// assert!(validator.is_valid(&json!("Hello 42")));
3036 /// assert!(!validator.is_valid(&json!("No!")));
3037 /// # Ok(())
3038 /// # }
3039 /// ```
3040 ///
3041 /// See [`ValidationOptions`] for all available configuration options.
3042 #[must_use]
3043 pub fn options<'i>() -> ValidationOptions<'i> {
3044 crate::options().with_draft(Draft::Draft7)
3045 }
3046
3047 /// Functionality for validating JSON Schema Draft 7 documents.
3048 pub mod meta {
3049 use crate::{meta::MetaValidator, ValidationError};
3050 use serde_json::Value;
3051
3052 /// Returns a handle to the Draft 7 meta-schema validator. Native targets borrow cached
3053 /// statics while `wasm32` builds an owned validator.
3054 #[must_use]
3055 pub fn validator() -> MetaValidator<'static> {
3056 crate::meta::validator_for_draft(super::Draft::Draft7)
3057 }
3058
3059 /// Validate a JSON Schema document against Draft 7 meta-schema and get a `true` if the schema is valid
3060 /// and `false` otherwise.
3061 ///
3062 /// # Examples
3063 ///
3064 /// ```rust
3065 /// use serde_json::json;
3066 ///
3067 /// let schema = json!({
3068 /// "type": "string",
3069 /// "maxLength": 5
3070 /// });
3071 /// assert!(jsonschema::draft7::meta::is_valid(&schema));
3072 /// ```
3073 #[must_use]
3074 #[inline]
3075 pub fn is_valid(schema: &Value) -> bool {
3076 validator().is_valid(schema)
3077 }
3078
3079 /// Validate a JSON Schema document against Draft 7 meta-schema and return the first error if any.
3080 ///
3081 /// # Examples
3082 ///
3083 /// ```rust
3084 /// use serde_json::json;
3085 ///
3086 /// let schema = json!({
3087 /// "type": "string",
3088 /// "maxLength": 5
3089 /// });
3090 /// assert!(jsonschema::draft7::meta::validate(&schema).is_ok());
3091 ///
3092 /// // Invalid schema
3093 /// let invalid_schema = json!({
3094 /// "type": "invalid_type"
3095 /// });
3096 /// assert!(jsonschema::draft7::meta::validate(&invalid_schema).is_err());
3097 /// ```
3098 ///
3099 /// # Errors
3100 ///
3101 /// Returns the first [`ValidationError`] describing why the schema violates the Draft 7 meta-schema.
3102 #[inline]
3103 pub fn validate(schema: &Value) -> Result<(), ValidationError<'_>> {
3104 validator().validate(schema)
3105 }
3106 }
3107}
3108
3109/// Functionality specific to JSON Schema Draft 2019-09.
3110///
3111/// [](https://bowtie.report/#/implementations/rust-jsonschema)
3112///
3113/// This module provides functions for creating validators and performing validation
3114/// according to the JSON Schema Draft 2019-09 specification.
3115///
3116/// # Examples
3117///
3118/// ```rust
3119/// use serde_json::json;
3120///
3121/// let schema = json!({"type": "array", "minItems": 2, "uniqueItems": true});
3122/// let instance = json!([1, 2]);
3123///
3124/// assert!(jsonschema::draft201909::is_valid(&schema, &instance));
3125/// ```
3126pub mod draft201909 {
3127 use super::{Draft, ValidationError, ValidationOptions, Validator, Value};
3128
3129 /// Create a new JSON Schema validator using Draft 2019-09 specifications.
3130 ///
3131 /// # Examples
3132 ///
3133 /// ```rust
3134 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
3135 /// use serde_json::json;
3136 ///
3137 /// let schema = json!({"minimum": 5});
3138 /// let instance = json!(42);
3139 ///
3140 /// let validator = jsonschema::draft201909::new(&schema)?;
3141 /// assert!(validator.is_valid(&instance));
3142 /// # Ok(())
3143 /// # }
3144 /// ```
3145 ///
3146 /// # Errors
3147 ///
3148 /// Returns an error if the schema is not a valid Draft 2019-09 document or if referenced resources
3149 /// cannot be resolved.
3150 pub fn new(schema: &Value) -> Result<Validator, ValidationError<'static>> {
3151 options().build(schema)
3152 }
3153 /// Validate an instance against a schema using Draft 2019-09 specifications without creating a validator.
3154 ///
3155 /// # Examples
3156 ///
3157 /// ```rust
3158 /// use serde_json::json;
3159 ///
3160 /// let schema = json!({"minimum": 5});
3161 /// let valid = json!(42);
3162 /// let invalid = json!(3);
3163 ///
3164 /// assert!(jsonschema::draft201909::is_valid(&schema, &valid));
3165 /// assert!(!jsonschema::draft201909::is_valid(&schema, &invalid));
3166 /// ```
3167 ///
3168 /// # Panics
3169 ///
3170 /// Panics if `schema` cannot be compiled into a Draft 2019-09 validator.
3171 #[must_use]
3172 pub fn is_valid(schema: &Value, instance: &Value) -> bool {
3173 new(schema).expect("Invalid schema").is_valid(instance)
3174 }
3175 /// Validate an instance against a schema using Draft 2019-09 specifications without creating a validator.
3176 ///
3177 /// # Examples
3178 ///
3179 /// ```rust
3180 /// use serde_json::json;
3181 ///
3182 /// let schema = json!({"minimum": 5});
3183 /// let valid = json!(42);
3184 /// let invalid = json!(3);
3185 ///
3186 /// assert!(jsonschema::draft201909::validate(&schema, &valid).is_ok());
3187 /// assert!(jsonschema::draft201909::validate(&schema, &invalid).is_err());
3188 /// ```
3189 ///
3190 /// # Errors
3191 ///
3192 /// Returns the first [`ValidationError`] when `instance` violates the schema.
3193 ///
3194 /// # Panics
3195 ///
3196 /// Panics if `schema` cannot be compiled into a Draft 2019-09 validator.
3197 pub fn validate<'i>(schema: &Value, instance: &'i Value) -> Result<(), ValidationError<'i>> {
3198 new(schema).expect("Invalid schema").validate(instance)
3199 }
3200 /// Creates a [`ValidationOptions`] builder pre-configured for JSON Schema Draft 2019-09.
3201 ///
3202 /// This function provides a shorthand for `jsonschema::options().with_draft(Draft::Draft201909)`.
3203 ///
3204 /// # Examples
3205 ///
3206 /// ```
3207 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
3208 /// use serde_json::json;
3209 ///
3210 /// let schema = json!({"type": "string", "format": "ends-with-42"});
3211 /// let validator = jsonschema::draft201909::options()
3212 /// .with_format("ends-with-42", |s| s.ends_with("42"))
3213 /// .should_validate_formats(true)
3214 /// .build(&schema)?;
3215 ///
3216 /// assert!(validator.is_valid(&json!("Hello 42")));
3217 /// assert!(!validator.is_valid(&json!("No!")));
3218 /// # Ok(())
3219 /// # }
3220 /// ```
3221 ///
3222 /// See [`ValidationOptions`] for all available configuration options.
3223 #[must_use]
3224 pub fn options<'i>() -> ValidationOptions<'i> {
3225 crate::options().with_draft(Draft::Draft201909)
3226 }
3227
3228 /// Functionality for validating JSON Schema Draft 2019-09 documents.
3229 pub mod meta {
3230 use crate::{meta::MetaValidator, ValidationError};
3231 use serde_json::Value;
3232
3233 /// Returns a handle to the Draft 2019-09 meta-schema validator. Native targets borrow cached
3234 /// statics while `wasm32` builds an owned validator.
3235 #[must_use]
3236 pub fn validator() -> MetaValidator<'static> {
3237 crate::meta::validator_for_draft(super::Draft::Draft201909)
3238 }
3239 /// Validate a JSON Schema document against Draft 2019-09 meta-schema and get a `true` if the schema is valid
3240 /// and `false` otherwise.
3241 ///
3242 /// # Examples
3243 ///
3244 /// ```rust
3245 /// use serde_json::json;
3246 ///
3247 /// let schema = json!({
3248 /// "type": "string",
3249 /// "maxLength": 5
3250 /// });
3251 /// assert!(jsonschema::draft201909::meta::is_valid(&schema));
3252 /// ```
3253 #[must_use]
3254 #[inline]
3255 pub fn is_valid(schema: &Value) -> bool {
3256 validator().is_valid(schema)
3257 }
3258
3259 /// Validate a JSON Schema document against Draft 2019-09 meta-schema and return the first error if any.
3260 ///
3261 /// # Examples
3262 ///
3263 /// ```rust
3264 /// use serde_json::json;
3265 ///
3266 /// let schema = json!({
3267 /// "type": "string",
3268 /// "maxLength": 5
3269 /// });
3270 /// assert!(jsonschema::draft201909::meta::validate(&schema).is_ok());
3271 ///
3272 /// // Invalid schema
3273 /// let invalid_schema = json!({
3274 /// "type": "invalid_type"
3275 /// });
3276 /// assert!(jsonschema::draft201909::meta::validate(&invalid_schema).is_err());
3277 /// ```
3278 ///
3279 /// # Errors
3280 ///
3281 /// Returns the first [`ValidationError`] describing why the schema violates the Draft 2019-09 meta-schema.
3282 #[inline]
3283 pub fn validate(schema: &Value) -> Result<(), ValidationError<'_>> {
3284 validator().validate(schema)
3285 }
3286 }
3287}
3288
3289/// Functionality specific to JSON Schema Draft 2020-12.
3290///
3291/// [](https://bowtie.report/#/implementations/rust-jsonschema)
3292///
3293/// This module provides functions for creating validators and performing validation
3294/// according to the JSON Schema Draft 2020-12 specification.
3295///
3296/// # Examples
3297///
3298/// ```rust
3299/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
3300/// use serde_json::json;
3301///
3302/// let schema = json!({"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]});
3303/// let instance = json!({"name": "John Doe"});
3304///
3305/// assert!(jsonschema::draft202012::is_valid(&schema, &instance));
3306/// # Ok(())
3307/// # }
3308/// ```
3309pub mod draft202012 {
3310 use super::{Draft, ValidationError, ValidationOptions, Validator, Value};
3311
3312 /// Create a new JSON Schema validator using Draft 2020-12 specifications.
3313 ///
3314 /// # Examples
3315 ///
3316 /// ```rust
3317 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
3318 /// use serde_json::json;
3319 ///
3320 /// let schema = json!({"minimum": 5});
3321 /// let instance = json!(42);
3322 ///
3323 /// let validator = jsonschema::draft202012::new(&schema)?;
3324 /// assert!(validator.is_valid(&instance));
3325 /// # Ok(())
3326 /// # }
3327 /// ```
3328 ///
3329 /// # Errors
3330 ///
3331 /// Returns an error if the schema is not a valid Draft 2020-12 document or if referenced resources
3332 /// cannot be resolved.
3333 pub fn new(schema: &Value) -> Result<Validator, ValidationError<'static>> {
3334 options().build(schema)
3335 }
3336 /// Validate an instance against a schema using Draft 2020-12 specifications without creating a validator.
3337 ///
3338 /// # Examples
3339 ///
3340 /// ```rust
3341 /// use serde_json::json;
3342 ///
3343 /// let schema = json!({"minimum": 5});
3344 /// let valid = json!(42);
3345 /// let invalid = json!(3);
3346 ///
3347 /// assert!(jsonschema::draft202012::is_valid(&schema, &valid));
3348 /// assert!(!jsonschema::draft202012::is_valid(&schema, &invalid));
3349 /// ```
3350 ///
3351 /// # Panics
3352 ///
3353 /// Panics if `schema` cannot be compiled into a Draft 2020-12 validator.
3354 #[must_use]
3355 pub fn is_valid(schema: &Value, instance: &Value) -> bool {
3356 new(schema).expect("Invalid schema").is_valid(instance)
3357 }
3358 /// Validate an instance against a schema using Draft 2020-12 specifications without creating a validator.
3359 ///
3360 /// # Examples
3361 ///
3362 /// ```rust
3363 /// use serde_json::json;
3364 ///
3365 /// let schema = json!({"minimum": 5});
3366 /// let valid = json!(42);
3367 /// let invalid = json!(3);
3368 ///
3369 /// assert!(jsonschema::draft202012::validate(&schema, &valid).is_ok());
3370 /// assert!(jsonschema::draft202012::validate(&schema, &invalid).is_err());
3371 /// ```
3372 ///
3373 /// # Errors
3374 ///
3375 /// Returns the first [`ValidationError`] when `instance` violates the schema.
3376 ///
3377 /// # Panics
3378 ///
3379 /// Panics if `schema` cannot be compiled into a Draft 2020-12 validator.
3380 pub fn validate<'i>(schema: &Value, instance: &'i Value) -> Result<(), ValidationError<'i>> {
3381 new(schema).expect("Invalid schema").validate(instance)
3382 }
3383 /// Creates a [`ValidationOptions`] builder pre-configured for JSON Schema Draft 2020-12.
3384 ///
3385 /// This function provides a shorthand for `jsonschema::options().with_draft(Draft::Draft202012)`.
3386 ///
3387 /// # Examples
3388 ///
3389 /// ```
3390 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
3391 /// use serde_json::json;
3392 ///
3393 /// let schema = json!({"type": "string", "format": "ends-with-42"});
3394 /// let validator = jsonschema::draft202012::options()
3395 /// .with_format("ends-with-42", |s| s.ends_with("42"))
3396 /// .should_validate_formats(true)
3397 /// .build(&schema)?;
3398 ///
3399 /// assert!(validator.is_valid(&json!("Hello 42")));
3400 /// assert!(!validator.is_valid(&json!("No!")));
3401 /// # Ok(())
3402 /// # }
3403 /// ```
3404 ///
3405 /// See [`ValidationOptions`] for all available configuration options.
3406 #[must_use]
3407 pub fn options<'i>() -> ValidationOptions<'i> {
3408 crate::options().with_draft(Draft::Draft202012)
3409 }
3410
3411 /// Functionality for validating JSON Schema Draft 2020-12 documents.
3412 pub mod meta {
3413 use crate::{meta::MetaValidator, ValidationError};
3414 use serde_json::Value;
3415
3416 /// Returns a handle to the Draft 2020-12 meta-schema validator. Native targets borrow
3417 /// cached statics while `wasm32` builds an owned validator.
3418 #[must_use]
3419 pub fn validator() -> MetaValidator<'static> {
3420 crate::meta::validator_for_draft(super::Draft::Draft202012)
3421 }
3422
3423 /// Validate a JSON Schema document against Draft 2020-12 meta-schema and get a `true` if the schema is valid
3424 /// and `false` otherwise.
3425 ///
3426 /// # Examples
3427 ///
3428 /// ```rust
3429 /// use serde_json::json;
3430 ///
3431 /// let schema = json!({
3432 /// "type": "string",
3433 /// "maxLength": 5
3434 /// });
3435 /// assert!(jsonschema::draft202012::meta::is_valid(&schema));
3436 /// ```
3437 #[must_use]
3438 #[inline]
3439 pub fn is_valid(schema: &Value) -> bool {
3440 validator().is_valid(schema)
3441 }
3442
3443 /// Validate a JSON Schema document against Draft 2020-12 meta-schema and return the first error if any.
3444 ///
3445 /// # Examples
3446 ///
3447 /// ```rust
3448 /// use serde_json::json;
3449 ///
3450 /// let schema = json!({
3451 /// "type": "string",
3452 /// "maxLength": 5
3453 /// });
3454 /// assert!(jsonschema::draft202012::meta::validate(&schema).is_ok());
3455 ///
3456 /// // Invalid schema
3457 /// let invalid_schema = json!({
3458 /// "type": "invalid_type"
3459 /// });
3460 /// assert!(jsonschema::draft202012::meta::validate(&invalid_schema).is_err());
3461 /// ```
3462 ///
3463 /// # Errors
3464 ///
3465 /// Returns the first [`ValidationError`] describing why the schema violates the Draft 2020-12 meta-schema.
3466 #[inline]
3467 pub fn validate(schema: &Value) -> Result<(), ValidationError<'_>> {
3468 validator().validate(schema)
3469 }
3470 }
3471}
3472
3473#[cfg(feature = "macros")]
3474#[doc(hidden)]
3475pub mod __private {
3476 #[cfg(feature = "pyo3")]
3477 pub mod pyo3 {
3478 pub use pyo3::{intern, types::PyString, Borrowed, Bound, PyAny, PyResult};
3479 }
3480
3481 // Wraps a `backend = Pyo3` validator, so a build without the feature reports that alone.
3482 #[cfg(feature = "pyo3")]
3483 #[doc(hidden)]
3484 #[macro_export]
3485 macro_rules! __pyo3_backend {
3486 ($($generated:tt)*) => { $($generated)* };
3487 }
3488
3489 #[cfg(not(feature = "pyo3"))]
3490 #[doc(hidden)]
3491 #[macro_export]
3492 macro_rules! __pyo3_backend {
3493 ($($generated:tt)*) => {
3494 ::core::compile_error!("`backend = Pyo3` needs the `pyo3` feature of `jsonschema`");
3495 };
3496 }
3497
3498 pub use crate::__pyo3_backend as pyo3_backend;
3499
3500 #[cfg(feature = "magnus")]
3501 pub mod magnus {
3502 pub use ::magnus::{rb_sys::AsRawValue, Error, Value};
3503 }
3504
3505 // Wraps a `backend = Magnus` validator, so a build without the feature reports that alone.
3506 #[cfg(feature = "magnus")]
3507 #[doc(hidden)]
3508 #[macro_export]
3509 macro_rules! __magnus_backend {
3510 ($($generated:tt)*) => { $($generated)* };
3511 }
3512
3513 #[cfg(not(feature = "magnus"))]
3514 #[doc(hidden)]
3515 #[macro_export]
3516 macro_rules! __magnus_backend {
3517 ($($generated:tt)*) => {
3518 ::core::compile_error!("`backend = Magnus` needs the `magnus` feature of `jsonschema`");
3519 };
3520 }
3521
3522 pub use crate::__magnus_backend as magnus_backend;
3523
3524 pub use ::serde_json;
3525
3526 pub mod fancy_regex {
3527 pub use fancy_regex::{Regex, RegexBuilder};
3528 }
3529 pub mod regex {
3530 pub use jsonschema_regex::contains_ecma_whitespace;
3531 pub use regex::{Regex, RegexBuilder};
3532 }
3533 /// Regex matching for generated code: a match the engine cannot finish is resolved by running
3534 /// validation under each of its outcomes, as the runtime validator does.
3535 pub mod unfinished_matches {
3536 use std::cell::{Cell, RefCell};
3537
3538 use crate::{
3539 paths::LazyLocation,
3540 regex::FancyRegexError,
3541 unfinished_matches::{
3542 assume_each_outcome, errors, validity, MatchAssumptions, MatchSite, Outcomes,
3543 },
3544 LazyInstance, ValidationError,
3545 };
3546
3547 std::thread_local! {
3548 static MATCHES: RefCell<Option<Box<MatchAssumptions>>> = const { RefCell::new(None) };
3549 /// Whether `MATCHES` holds a record, for the one check an entry makes after its run.
3550 static MET: Cell<bool> = const { Cell::new(false) };
3551 }
3552
3553 /// Whether a run on this thread met a regex match the engine could not finish.
3554 #[inline]
3555 #[must_use]
3556 pub fn met() -> bool {
3557 MET.get()
3558 }
3559
3560 /// Whether `regex` matches `subject`. A match the engine cannot finish is recorded at
3561 /// `schema_path` with the `pattern` keyword's source pattern and the instance location
3562 /// when the caller tracks one.
3563 #[inline]
3564 #[must_use]
3565 pub fn fancy_is_match(
3566 regex: &fancy_regex::Regex,
3567 subject: &str,
3568 schema_path: &str,
3569 pattern: Option<&str>,
3570 instance_path: Option<&LazyLocation>,
3571 ) -> bool {
3572 match regex.is_match(subject) {
3573 Ok(matches) => matches,
3574 Err(error) => resolve(regex, subject, error, schema_path, pattern, instance_path),
3575 }
3576 }
3577
3578 #[cold]
3579 fn resolve(
3580 regex: &fancy_regex::Regex,
3581 subject: &str,
3582 error: fancy_regex::Error,
3583 schema_path: &str,
3584 pattern: Option<&str>,
3585 instance_path: Option<&LazyLocation>,
3586 ) -> bool {
3587 let error = FancyRegexError::Engine {
3588 error,
3589 pattern: regex.as_str().into(),
3590 };
3591 MET.set(true);
3592 MATCHES.with_borrow_mut(|matches| {
3593 matches
3594 .get_or_insert_with(Box::default)
3595 .resolve(subject, error, || {
3596 let site = MatchSite::escaped(schema_path, instance_path.map(Into::into));
3597 match pattern {
3598 Some(pattern) => site.pattern(pattern),
3599 None => site,
3600 }
3601 })
3602 })
3603 }
3604
3605 fn swap(matches: &mut Option<Box<MatchAssumptions>>) {
3606 MATCHES.with_borrow_mut(|current| std::mem::swap(current, matches));
3607 MET.set(MATCHES.with_borrow(Option::is_some));
3608 }
3609
3610 /// Runs `run` under each combination of outcomes for the unfinished matches it meets,
3611 /// starting from a state of its own: the record the entry's run left may belong to an
3612 /// enclosing run. `Err` carries the result of a run that met none.
3613 #[cold]
3614 fn under_each_outcome<T>(mut run: impl FnMut() -> T) -> Result<Outcomes<T>, T> {
3615 let mut swapped = |matches: &mut Option<Box<MatchAssumptions>>| {
3616 swap(matches);
3617 let result = run();
3618 swap(matches);
3619 result
3620 };
3621 let mut first = None;
3622 let result = swapped(&mut first);
3623 match first {
3624 Some(first) => Ok(assume_each_outcome(swapped, *first)),
3625 None => Err(result),
3626 }
3627 }
3628
3629 /// `is_valid` for an entry whose run met an unfinished match. `run` is the generated
3630 /// function itself, taken by pointer so the entry's own call to it stays inlinable.
3631 #[cold]
3632 #[must_use]
3633 pub fn is_valid<N: Copy>(run: fn(N) -> bool, node: N) -> bool {
3634 match under_each_outcome(|| run(node)) {
3635 Ok(outcomes) => validity(outcomes),
3636 Err(valid) => valid,
3637 }
3638 }
3639
3640 /// `validate` for an entry whose run met an unfinished match.
3641 #[cold]
3642 #[must_use]
3643 pub fn validate<'i, N: Copy>(
3644 instance: impl FnOnce() -> LazyInstance<'i>,
3645 collect: fn(N, &LazyLocation, &mut Vec<ValidationError<'i>>),
3646 node: N,
3647 ) -> Option<ValidationError<'i>> {
3648 collect_errors(instance, collect, node).into_iter().next()
3649 }
3650
3651 /// `collect_errors` for an entry whose run met an unfinished match.
3652 #[cold]
3653 #[must_use]
3654 pub fn collect_errors<'i, N: Copy>(
3655 instance: impl FnOnce() -> LazyInstance<'i>,
3656 collect: fn(N, &LazyLocation, &mut Vec<ValidationError<'i>>),
3657 node: N,
3658 ) -> Vec<ValidationError<'i>> {
3659 let run = || {
3660 let mut errors = Vec::new();
3661 collect(node, &LazyLocation::new(), &mut errors);
3662 errors
3663 };
3664 match under_each_outcome(run) {
3665 Ok(outcomes) => errors(outcomes, instance),
3666 Err(errors) => errors,
3667 }
3668 }
3669 }
3670 pub mod unique_items {
3671 pub use crate::unique::is_unique;
3672 }
3673 pub mod cmp {
3674 pub use crate::cmp::{equal, equal_numbers};
3675 }
3676 pub mod custom {
3677 use crate::{
3678 json::{Json, Node},
3679 paths::Location,
3680 };
3681
3682 #[must_use]
3683 pub fn location(pointer: &str) -> Location {
3684 Location::from_escaped(pointer)
3685 }
3686
3687 /// Run a custom keyword and fill in error context exactly like the runtime validator's `CustomKeyword` wrapper.
3688 pub fn validate<'i, F: Json>(
3689 keyword: &dyn crate::Keyword<'i, F>,
3690 instance: &F::Node<'i>,
3691 instance_path: Location,
3692 schema_path: &str,
3693 keyword_name: &str,
3694 ) -> Option<crate::ValidationError<'i>> {
3695 match keyword.validate(instance.clone()) {
3696 Ok(()) => None,
3697 Err(error) => Some(error.with_generated_context(
3698 instance.to_value(),
3699 instance_path,
3700 Location::from_escaped(schema_path),
3701 keyword_name,
3702 )),
3703 }
3704 }
3705
3706 /// Run a custom keyword's `iter_errors`, filling in context exactly like [`validate`] does.
3707 pub fn collect_errors<'i, F: Json>(
3708 keyword: &dyn crate::Keyword<'i, F>,
3709 instance: &F::Node<'i>,
3710 instance_path: &Location,
3711 schema_path: &str,
3712 keyword_name: &str,
3713 errors: &mut Vec<crate::ValidationError<'i>>,
3714 ) {
3715 let mut found = keyword.iter_errors(instance.clone()).peekable();
3716 if found.peek().is_none() {
3717 return;
3718 }
3719 let value = instance.to_value();
3720 for error in found {
3721 errors.push(error.with_generated_context(
3722 value.clone(),
3723 instance_path.clone(),
3724 Location::from_escaped(schema_path),
3725 keyword_name,
3726 ));
3727 }
3728 }
3729 }
3730 pub mod types {
3731 /// Integer check per drafts 6+: floats with zero fractional part count.
3732 #[must_use]
3733 pub fn is_integer(n: &serde_json::Number) -> bool {
3734 jsonschema_value::types::number_is_integer(n)
3735 }
3736 /// Integer check per draft 4: numbers written with a decimal point are not integers.
3737 #[must_use]
3738 pub fn is_integer_draft4(n: &serde_json::Number) -> bool {
3739 crate::keywords::legacy::type_draft_4::is_integer(n)
3740 }
3741 }
3742 pub mod format {
3743 #[cfg(feature = "idna")]
3744 pub use crate::keywords::format::is_valid_idn_hostname;
3745 pub use crate::keywords::format::{
3746 is_valid_date, is_valid_datetime, is_valid_duration, is_valid_hostname,
3747 is_valid_hostname_rfc1034, is_valid_ipv4, is_valid_ipv6, is_valid_iri,
3748 is_valid_iri_reference, is_valid_json_pointer, is_valid_regex,
3749 is_valid_relative_json_pointer, is_valid_time, is_valid_uri, is_valid_uri_reference,
3750 is_valid_uri_template, is_valid_uuid,
3751 };
3752
3753 // Without `idna` the format is unknown, and an unknown format admits every instance.
3754 #[cfg(not(feature = "idna"))]
3755 #[must_use]
3756 pub fn is_valid_idn_hostname(_: &str) -> bool {
3757 true
3758 }
3759
3760 /// Per-call memoization cache for URI/IRI `format` checks in generated validators.
3761 pub type Cache = std::collections::HashMap<Box<str>, bool, ahash::RandomState>;
3762
3763 /// Validate an email format using configured [`crate::EmailOptions`].
3764 #[must_use]
3765 pub fn is_valid_email_with_options(
3766 value: &str,
3767 options: Option<&crate::EmailOptions>,
3768 ) -> bool {
3769 crate::keywords::format::is_valid_email(value, options.map(|opts| &opts.inner))
3770 }
3771
3772 /// Validate an IDN email format using configured [`crate::EmailOptions`].
3773 #[cfg(feature = "idna")]
3774 #[must_use]
3775 pub fn is_valid_idn_email_with_options(
3776 value: &str,
3777 options: Option<&crate::EmailOptions>,
3778 ) -> bool {
3779 crate::keywords::format::is_valid_idn_email(value, options.map(|opts| &opts.inner))
3780 }
3781
3782 // Without `idna` the format is unknown, and an unknown format admits every instance.
3783 #[cfg(not(feature = "idna"))]
3784 #[must_use]
3785 pub fn is_valid_idn_email_with_options(_: &str, _: Option<&crate::EmailOptions>) -> bool {
3786 true
3787 }
3788 }
3789 pub mod content {
3790 pub use crate::{
3791 content_encoding::{
3792 from_base16, from_base32, from_base32hex, from_base64, from_base64url, is_base16,
3793 is_base32, is_base32hex, is_base64, is_base64url,
3794 },
3795 content_media_type::is_json,
3796 };
3797 }
3798 pub mod numeric {
3799 /// Compare `value` >= `limit` using runtime numeric semantics.
3800 pub fn ge<T>(value: &serde_json::Number, limit: T) -> bool
3801 where
3802 T: Copy + num_traits::ToPrimitive,
3803 u64: num_cmp::NumCmp<T>,
3804 i64: num_cmp::NumCmp<T>,
3805 f64: num_cmp::NumCmp<T>,
3806 {
3807 crate::numeric::ge(value, limit)
3808 }
3809
3810 /// Compare `value` <= `limit` using runtime numeric semantics.
3811 pub fn le<T>(value: &serde_json::Number, limit: T) -> bool
3812 where
3813 T: Copy + num_traits::ToPrimitive,
3814 u64: num_cmp::NumCmp<T>,
3815 i64: num_cmp::NumCmp<T>,
3816 f64: num_cmp::NumCmp<T>,
3817 {
3818 crate::numeric::le(value, limit)
3819 }
3820
3821 pub fn eq<T>(value: &serde_json::Number, limit: T) -> bool
3822 where
3823 T: Copy + num_traits::ToPrimitive,
3824 u64: num_cmp::NumCmp<T>,
3825 i64: num_cmp::NumCmp<T>,
3826 f64: num_cmp::NumCmp<T>,
3827 {
3828 crate::numeric::eq(value, limit)
3829 }
3830
3831 /// Compare `value` > `limit` using runtime numeric semantics.
3832 pub fn gt<T>(value: &serde_json::Number, limit: T) -> bool
3833 where
3834 T: Copy + num_traits::ToPrimitive,
3835 u64: num_cmp::NumCmp<T>,
3836 i64: num_cmp::NumCmp<T>,
3837 f64: num_cmp::NumCmp<T>,
3838 {
3839 crate::numeric::gt(value, limit)
3840 }
3841
3842 /// Compare `value` < `limit` using runtime numeric semantics.
3843 pub fn lt<T>(value: &serde_json::Number, limit: T) -> bool
3844 where
3845 T: Copy + num_traits::ToPrimitive,
3846 u64: num_cmp::NumCmp<T>,
3847 i64: num_cmp::NumCmp<T>,
3848 f64: num_cmp::NumCmp<T>,
3849 {
3850 crate::numeric::lt(value, limit)
3851 }
3852
3853 /// Check `multipleOf` with integer divisors using runtime numeric semantics.
3854 #[must_use]
3855 pub fn is_multiple_of_integer(value: &serde_json::Number, multiple: f64) -> bool {
3856 crate::numeric::is_multiple_of_integer(value, multiple)
3857 }
3858
3859 /// Check `multipleOf` with fractional divisors using runtime numeric semantics.
3860 #[must_use]
3861 pub fn is_multiple_of_float(value: &serde_json::Number, multiple: f64) -> bool {
3862 crate::numeric::is_multiple_of_float(value, multiple)
3863 }
3864
3865 /// Check numeric bounds with a compiled descriptor for arbitrary-precision schemas.
3866 #[cfg(feature = "arbitrary-precision")]
3867 #[must_use]
3868 pub fn check_compiled_bound(
3869 value: &serde_json::Number,
3870 op: u8,
3871 limit_literal: &'static str,
3872 ) -> bool {
3873 use jsonschema_value::numeric_check::{
3874 check_bound, compile_bound, BoundOp, CompiledBound,
3875 };
3876
3877 #[inline]
3878 fn literal_key(literal: &'static str) -> (usize, usize) {
3879 (literal.as_ptr() as usize, literal.len())
3880 }
3881
3882 type BoundCache =
3883 std::cell::RefCell<ahash::AHashMap<((usize, usize), u8), CompiledBound>>;
3884 std::thread_local! {
3885 static CACHE: BoundCache = std::cell::RefCell::new(ahash::AHashMap::default());
3886 }
3887
3888 let op_tag = op;
3889 let Some(op) = BoundOp::from_u8(op_tag) else {
3890 unreachable!("codegen emits only bound op tags 0..=3")
3891 };
3892 let key = (literal_key(limit_literal), op_tag);
3893
3894 CACHE.with(|cache| {
3895 let mut cache = cache.borrow_mut();
3896 let compiled = cache.entry(key).or_insert_with(|| {
3897 let limit = serde_json::from_str::<serde_json::Number>(limit_literal)
3898 .expect("Codegen emitted an invalid numeric literal");
3899 compile_bound(op, &limit)
3900 });
3901 check_bound(compiled, value)
3902 })
3903 }
3904
3905 /// Check `multipleOf` with a compiled descriptor for arbitrary-precision schemas.
3906 #[cfg(feature = "arbitrary-precision")]
3907 #[must_use]
3908 pub fn check_compiled_multiple_of(
3909 value: &serde_json::Number,
3910 limit_literal: &'static str,
3911 ) -> bool {
3912 use jsonschema_value::numeric_check::{
3913 check_multiple_of, compile_multiple_of, CompiledMultipleOf,
3914 };
3915
3916 #[inline]
3917 fn literal_key(literal: &'static str) -> (usize, usize) {
3918 (literal.as_ptr() as usize, literal.len())
3919 }
3920
3921 std::thread_local! {
3922 static CACHE: std::cell::RefCell<ahash::AHashMap<(usize, usize), CompiledMultipleOf>> =
3923 std::cell::RefCell::new(ahash::AHashMap::default());
3924 }
3925 let key = literal_key(limit_literal);
3926
3927 CACHE.with(|cache| {
3928 let mut cache = cache.borrow_mut();
3929 let compiled = cache.entry(key).or_insert_with(|| {
3930 let limit = serde_json::from_str::<serde_json::Number>(limit_literal)
3931 .expect("Codegen emitted an invalid numeric literal");
3932 compile_multiple_of(&limit)
3933 });
3934 check_multiple_of(compiled, value)
3935 })
3936 }
3937 }
3938
3939 /// Per-keyword error constructors for generated code; evaluation path always equals schema path.
3940 pub mod error {
3941 use serde_json::Value;
3942
3943 use crate::{
3944 paths::Location,
3945 types::{JsonType, JsonTypeSet},
3946 validator::LazyEvaluationPath,
3947 ErrorIterator, LazyInstance, ValidationError,
3948 };
3949
3950 /// Wrap errors collected by generated `collect_errors` code into an [`ErrorIterator`].
3951 #[inline]
3952 #[must_use]
3953 pub fn iterator_from(errors: Vec<ValidationError<'_>>) -> ErrorIterator<'_> {
3954 ErrorIterator::from_iterator(errors.into_iter())
3955 }
3956
3957 /// `contentEncoding` keyword violation.
3958 #[inline]
3959 pub fn content_encoding<'i>(
3960 schema_path: &str,
3961 instance_path: Location,
3962 instance: impl Into<LazyInstance<'i>>,
3963 encoding: &str,
3964 ) -> ValidationError<'i> {
3965 ValidationError::content_encoding(
3966 Location::from_escaped(schema_path),
3967 LazyEvaluationPath::SameAsSchemaPath,
3968 instance_path,
3969 instance.into(),
3970 encoding,
3971 )
3972 }
3973
3974 /// `contentMediaType` keyword violation.
3975 #[inline]
3976 pub fn content_media_type<'i>(
3977 schema_path: &str,
3978 instance_path: Location,
3979 instance: impl Into<LazyInstance<'i>>,
3980 media_type: &str,
3981 ) -> ValidationError<'i> {
3982 ValidationError::content_media_type(
3983 Location::from_escaped(schema_path),
3984 LazyEvaluationPath::SameAsSchemaPath,
3985 instance_path,
3986 instance.into(),
3987 media_type,
3988 )
3989 }
3990
3991 /// `minLength` keyword violation: string is shorter than `limit` characters.
3992 #[inline]
3993 pub fn min_length<'i>(
3994 schema_path: &str,
3995 instance_path: Location,
3996 instance: impl Into<LazyInstance<'i>>,
3997 limit: u64,
3998 ) -> ValidationError<'i> {
3999 ValidationError::min_length(
4000 Location::from_escaped(schema_path),
4001 LazyEvaluationPath::SameAsSchemaPath,
4002 instance_path,
4003 instance.into(),
4004 limit,
4005 )
4006 }
4007
4008 /// `maxLength` keyword violation: string exceeds `limit` characters.
4009 #[inline]
4010 pub fn max_length<'i>(
4011 schema_path: &str,
4012 instance_path: Location,
4013 instance: impl Into<LazyInstance<'i>>,
4014 limit: u64,
4015 ) -> ValidationError<'i> {
4016 ValidationError::max_length(
4017 Location::from_escaped(schema_path),
4018 LazyEvaluationPath::SameAsSchemaPath,
4019 instance_path,
4020 instance.into(),
4021 limit,
4022 )
4023 }
4024
4025 /// `pattern` keyword violation.
4026 #[inline]
4027 pub fn pattern<'i>(
4028 schema_path: &str,
4029 instance_path: Location,
4030 instance: impl Into<LazyInstance<'i>>,
4031 pattern: &str,
4032 ) -> ValidationError<'i> {
4033 ValidationError::pattern(
4034 Location::from_escaped(schema_path),
4035 LazyEvaluationPath::SameAsSchemaPath,
4036 instance_path,
4037 instance.into(),
4038 pattern.to_string(),
4039 )
4040 }
4041
4042 /// `format` keyword violation.
4043 #[inline]
4044 pub fn format<'i>(
4045 schema_path: &str,
4046 instance_path: Location,
4047 instance: impl Into<LazyInstance<'i>>,
4048 format: &str,
4049 ) -> ValidationError<'i> {
4050 ValidationError::format(
4051 Location::from_escaped(schema_path),
4052 LazyEvaluationPath::SameAsSchemaPath,
4053 instance_path,
4054 instance.into(),
4055 format,
4056 )
4057 }
4058
4059 /// `type` keyword violation: instance is not one of the expected JSON types.
4060 ///
4061 /// Use [`single_type`] when only one type is expected, [`multiple_types`] for union types.
4062 #[inline]
4063 pub fn single_type<'i>(
4064 schema_path: &str,
4065 instance_path: Location,
4066 instance: impl Into<LazyInstance<'i>>,
4067 ty: JsonType,
4068 ) -> ValidationError<'i> {
4069 ValidationError::single_type_error(
4070 Location::from_escaped(schema_path),
4071 LazyEvaluationPath::SameAsSchemaPath,
4072 instance_path,
4073 instance.into(),
4074 ty,
4075 )
4076 }
4077
4078 /// `type` keyword violation for a union of expected types.
4079 #[inline]
4080 pub fn multiple_types<'i>(
4081 schema_path: &str,
4082 instance_path: Location,
4083 instance: impl Into<LazyInstance<'i>>,
4084 types: JsonTypeSet,
4085 ) -> ValidationError<'i> {
4086 ValidationError::multiple_type_error(
4087 Location::from_escaped(schema_path),
4088 LazyEvaluationPath::SameAsSchemaPath,
4089 instance_path,
4090 instance.into(),
4091 types,
4092 )
4093 }
4094
4095 /// `minimum` keyword violation: value is less than `limit`.
4096 #[inline]
4097 pub fn minimum<'i>(
4098 schema_path: &str,
4099 instance_path: Location,
4100 instance: impl Into<LazyInstance<'i>>,
4101 limit: Value,
4102 ) -> ValidationError<'i> {
4103 ValidationError::minimum(
4104 Location::from_escaped(schema_path),
4105 LazyEvaluationPath::SameAsSchemaPath,
4106 instance_path,
4107 instance.into(),
4108 limit,
4109 )
4110 }
4111
4112 /// `maximum` keyword violation: value exceeds `limit`.
4113 #[inline]
4114 pub fn maximum<'i>(
4115 schema_path: &str,
4116 instance_path: Location,
4117 instance: impl Into<LazyInstance<'i>>,
4118 limit: Value,
4119 ) -> ValidationError<'i> {
4120 ValidationError::maximum(
4121 Location::from_escaped(schema_path),
4122 LazyEvaluationPath::SameAsSchemaPath,
4123 instance_path,
4124 instance.into(),
4125 limit,
4126 )
4127 }
4128
4129 /// `exclusiveMinimum` keyword violation: value is not strictly greater than `limit`.
4130 #[inline]
4131 pub fn exclusive_minimum<'i>(
4132 schema_path: &str,
4133 instance_path: Location,
4134 instance: impl Into<LazyInstance<'i>>,
4135 limit: Value,
4136 ) -> ValidationError<'i> {
4137 ValidationError::exclusive_minimum(
4138 Location::from_escaped(schema_path),
4139 LazyEvaluationPath::SameAsSchemaPath,
4140 instance_path,
4141 instance.into(),
4142 limit,
4143 )
4144 }
4145
4146 /// `exclusiveMaximum` keyword violation: value is not strictly less than `limit`.
4147 #[inline]
4148 pub fn exclusive_maximum<'i>(
4149 schema_path: &str,
4150 instance_path: Location,
4151 instance: impl Into<LazyInstance<'i>>,
4152 limit: Value,
4153 ) -> ValidationError<'i> {
4154 ValidationError::exclusive_maximum(
4155 Location::from_escaped(schema_path),
4156 LazyEvaluationPath::SameAsSchemaPath,
4157 instance_path,
4158 instance.into(),
4159 limit,
4160 )
4161 }
4162
4163 /// `multipleOf` keyword violation (non-arbitrary-precision).
4164 #[cfg(not(feature = "arbitrary-precision"))]
4165 #[inline]
4166 pub fn multiple_of<'i>(
4167 schema_path: &str,
4168 instance_path: Location,
4169 instance: impl Into<LazyInstance<'i>>,
4170 multiple_of: f64,
4171 ) -> ValidationError<'i> {
4172 ValidationError::multiple_of(
4173 Location::from_escaped(schema_path),
4174 LazyEvaluationPath::SameAsSchemaPath,
4175 instance_path,
4176 instance.into(),
4177 multiple_of,
4178 )
4179 }
4180
4181 /// `multipleOf` keyword violation (arbitrary-precision).
4182 #[cfg(feature = "arbitrary-precision")]
4183 #[inline]
4184 pub fn multiple_of<'i>(
4185 schema_path: &str,
4186 instance_path: Location,
4187 instance: impl Into<LazyInstance<'i>>,
4188 multiple_of: Value,
4189 ) -> ValidationError<'i> {
4190 ValidationError::multiple_of(
4191 Location::from_escaped(schema_path),
4192 LazyEvaluationPath::SameAsSchemaPath,
4193 instance_path,
4194 instance.into(),
4195 multiple_of,
4196 )
4197 }
4198
4199 /// `minItems` keyword violation: array has fewer items than `limit`.
4200 #[inline]
4201 pub fn min_items<'i>(
4202 schema_path: &str,
4203 instance_path: Location,
4204 instance: impl Into<LazyInstance<'i>>,
4205 limit: u64,
4206 ) -> ValidationError<'i> {
4207 ValidationError::min_items(
4208 Location::from_escaped(schema_path),
4209 LazyEvaluationPath::SameAsSchemaPath,
4210 instance_path,
4211 instance.into(),
4212 limit,
4213 )
4214 }
4215
4216 /// `maxItems` keyword violation: array exceeds `limit` items.
4217 #[inline]
4218 pub fn max_items<'i>(
4219 schema_path: &str,
4220 instance_path: Location,
4221 instance: impl Into<LazyInstance<'i>>,
4222 limit: u64,
4223 ) -> ValidationError<'i> {
4224 ValidationError::max_items(
4225 Location::from_escaped(schema_path),
4226 LazyEvaluationPath::SameAsSchemaPath,
4227 instance_path,
4228 instance.into(),
4229 limit,
4230 )
4231 }
4232
4233 /// `additionalItems` keyword violation (Draft 4 / 2019-09 tuple form).
4234 #[inline]
4235 pub fn additional_items<'i>(
4236 schema_path: &str,
4237 instance_path: Location,
4238 instance: impl Into<LazyInstance<'i>>,
4239 limit: usize,
4240 ) -> ValidationError<'i> {
4241 ValidationError::additional_items(
4242 Location::from_escaped(schema_path),
4243 LazyEvaluationPath::SameAsSchemaPath,
4244 instance_path,
4245 instance.into(),
4246 limit,
4247 )
4248 }
4249
4250 /// `uniqueItems` keyword violation: array contains duplicate elements.
4251 #[inline]
4252 pub fn unique_items<'i>(
4253 schema_path: &str,
4254 instance_path: Location,
4255 instance: impl Into<LazyInstance<'i>>,
4256 ) -> ValidationError<'i> {
4257 ValidationError::unique_items(
4258 Location::from_escaped(schema_path),
4259 LazyEvaluationPath::SameAsSchemaPath,
4260 instance_path,
4261 instance.into(),
4262 )
4263 }
4264
4265 /// `contains` keyword violation: no array item matched the `contains` schema.
4266 #[inline]
4267 pub fn contains<'i>(
4268 schema_path: &str,
4269 instance_path: Location,
4270 instance: impl Into<LazyInstance<'i>>,
4271 ) -> ValidationError<'i> {
4272 ValidationError::contains(
4273 Location::from_escaped(schema_path),
4274 LazyEvaluationPath::SameAsSchemaPath,
4275 instance_path,
4276 instance.into(),
4277 )
4278 }
4279
4280 /// `minProperties` keyword violation: object has fewer properties than `limit`.
4281 #[inline]
4282 pub fn min_properties<'i>(
4283 schema_path: &str,
4284 instance_path: Location,
4285 instance: impl Into<LazyInstance<'i>>,
4286 limit: u64,
4287 ) -> ValidationError<'i> {
4288 ValidationError::min_properties(
4289 Location::from_escaped(schema_path),
4290 LazyEvaluationPath::SameAsSchemaPath,
4291 instance_path,
4292 instance.into(),
4293 limit,
4294 )
4295 }
4296
4297 /// `maxProperties` keyword violation: object exceeds `limit` properties.
4298 #[inline]
4299 pub fn max_properties<'i>(
4300 schema_path: &str,
4301 instance_path: Location,
4302 instance: impl Into<LazyInstance<'i>>,
4303 limit: u64,
4304 ) -> ValidationError<'i> {
4305 ValidationError::max_properties(
4306 Location::from_escaped(schema_path),
4307 LazyEvaluationPath::SameAsSchemaPath,
4308 instance_path,
4309 instance.into(),
4310 limit,
4311 )
4312 }
4313
4314 /// `required` keyword violation: object is missing `property`.
4315 #[inline]
4316 pub fn required<'i>(
4317 schema_path: &str,
4318 instance_path: Location,
4319 instance: impl Into<LazyInstance<'i>>,
4320 property: &str,
4321 ) -> ValidationError<'i> {
4322 ValidationError::required(
4323 Location::from_escaped(schema_path),
4324 LazyEvaluationPath::SameAsSchemaPath,
4325 instance_path,
4326 instance.into(),
4327 Value::String(property.to_owned()),
4328 )
4329 }
4330
4331 /// `additionalProperties` keyword violation (the `false` schema form).
4332 #[inline]
4333 pub fn additional_properties<'i>(
4334 schema_path: &str,
4335 instance_path: Location,
4336 instance: impl Into<LazyInstance<'i>>,
4337 unexpected: Vec<String>,
4338 ) -> ValidationError<'i> {
4339 ValidationError::additional_properties(
4340 Location::from_escaped(schema_path),
4341 LazyEvaluationPath::SameAsSchemaPath,
4342 instance_path,
4343 instance.into(),
4344 unexpected,
4345 )
4346 }
4347
4348 /// `const` keyword violation: instance does not equal the expected constant.
4349 #[inline]
4350 pub fn constant<'i>(
4351 schema_path: &str,
4352 instance_path: Location,
4353 instance: impl Into<LazyInstance<'i>>,
4354 expected: Value,
4355 ) -> ValidationError<'i> {
4356 ValidationError::new(
4357 instance.into(),
4358 crate::error::ValidationErrorKind::Constant {
4359 expected_value: expected,
4360 },
4361 instance_path,
4362 Location::from_escaped(schema_path),
4363 LazyEvaluationPath::SameAsSchemaPath,
4364 )
4365 }
4366
4367 /// `enum` keyword violation: instance does not match any of the allowed values.
4368 #[inline]
4369 pub fn enumeration<'i>(
4370 schema_path: &str,
4371 instance_path: Location,
4372 instance: impl Into<LazyInstance<'i>>,
4373 options: &Value,
4374 ) -> ValidationError<'i> {
4375 ValidationError::enumeration(
4376 Location::from_escaped(schema_path),
4377 LazyEvaluationPath::SameAsSchemaPath,
4378 instance_path,
4379 instance.into(),
4380 options,
4381 )
4382 }
4383
4384 /// `false` schema violation: nothing is valid against a `false` schema.
4385 #[inline]
4386 pub fn false_schema<'i>(
4387 schema_path: &str,
4388 instance_path: Location,
4389 instance: impl Into<LazyInstance<'i>>,
4390 ) -> ValidationError<'i> {
4391 ValidationError::false_schema(
4392 Location::from_escaped(schema_path),
4393 LazyEvaluationPath::SameAsSchemaPath,
4394 instance_path,
4395 instance.into(),
4396 )
4397 }
4398
4399 /// `not` keyword violation: instance is valid against the negated schema.
4400 #[inline]
4401 pub fn not<'i>(
4402 schema_path: &str,
4403 instance_path: Location,
4404 instance: impl Into<LazyInstance<'i>>,
4405 schema: Value,
4406 ) -> ValidationError<'i> {
4407 ValidationError::not(
4408 Location::from_escaped(schema_path),
4409 LazyEvaluationPath::SameAsSchemaPath,
4410 instance_path,
4411 instance.into(),
4412 schema,
4413 )
4414 }
4415
4416 /// `anyOf` keyword violation: instance is not valid under any of the listed schemas.
4417 #[inline]
4418 pub fn any_of<'i>(
4419 schema_path: &str,
4420 instance_path: Location,
4421 instance: impl Into<LazyInstance<'i>>,
4422 context: Vec<Vec<ValidationError<'i>>>,
4423 ) -> ValidationError<'i> {
4424 ValidationError::any_of(
4425 Location::from_escaped(schema_path),
4426 LazyEvaluationPath::SameAsSchemaPath,
4427 instance_path,
4428 instance.into(),
4429 context,
4430 )
4431 }
4432
4433 /// `oneOf` keyword violation: instance is not valid under any of the listed schemas.
4434 #[inline]
4435 pub fn one_of_not_valid<'i>(
4436 schema_path: &str,
4437 instance_path: Location,
4438 instance: impl Into<LazyInstance<'i>>,
4439 context: Vec<Vec<ValidationError<'i>>>,
4440 ) -> ValidationError<'i> {
4441 ValidationError::one_of_not_valid(
4442 Location::from_escaped(schema_path),
4443 LazyEvaluationPath::SameAsSchemaPath,
4444 instance_path,
4445 instance.into(),
4446 context,
4447 )
4448 }
4449
4450 /// `oneOf` keyword violation: instance is valid under more than one of the listed schemas.
4451 #[inline]
4452 pub fn one_of_multiple_valid<'i>(
4453 schema_path: &str,
4454 instance_path: Location,
4455 instance: impl Into<LazyInstance<'i>>,
4456 context: Vec<Vec<ValidationError<'i>>>,
4457 ) -> ValidationError<'i> {
4458 ValidationError::one_of_multiple_valid(
4459 Location::from_escaped(schema_path),
4460 LazyEvaluationPath::SameAsSchemaPath,
4461 instance_path,
4462 instance.into(),
4463 context,
4464 )
4465 }
4466
4467 /// `unevaluatedProperties` keyword violation.
4468 #[inline]
4469 pub fn unevaluated_properties<'i>(
4470 schema_path: &str,
4471 instance_path: Location,
4472 instance: impl Into<LazyInstance<'i>>,
4473 unexpected: Vec<String>,
4474 ) -> ValidationError<'i> {
4475 ValidationError::unevaluated_properties(
4476 Location::from_escaped(schema_path),
4477 LazyEvaluationPath::SameAsSchemaPath,
4478 instance_path,
4479 instance.into(),
4480 unexpected,
4481 )
4482 }
4483
4484 /// `unevaluatedItems` keyword violation.
4485 #[inline]
4486 pub fn unevaluated_items<'i>(
4487 schema_path: &str,
4488 instance_path: Location,
4489 instance: impl Into<LazyInstance<'i>>,
4490 unexpected: Vec<String>,
4491 ) -> ValidationError<'i> {
4492 ValidationError::unevaluated_items(
4493 Location::from_escaped(schema_path),
4494 LazyEvaluationPath::SameAsSchemaPath,
4495 instance_path,
4496 instance.into(),
4497 unexpected,
4498 )
4499 }
4500 }
4501}
4502
4503#[cfg(test)]
4504pub(crate) mod tests_util {
4505 use super::Validator;
4506 use crate::ValidationError;
4507 use serde_json::Value;
4508
4509 #[track_caller]
4510 pub(crate) fn is_not_valid_with(validator: &Validator, instance: &Value) {
4511 assert!(
4512 !validator.is_valid(instance),
4513 "{instance} should not be valid (via is_valid)",
4514 );
4515 assert!(
4516 validator.validate(instance).is_err(),
4517 "{instance} should not be valid (via validate)",
4518 );
4519 assert!(
4520 validator.iter_errors(instance).next().is_some(),
4521 "{instance} should not be valid (via validate)",
4522 );
4523 let evaluation = validator.evaluate(instance);
4524 assert!(
4525 !evaluation.flag().valid,
4526 "{instance} should not be valid (via evaluate)",
4527 );
4528 }
4529
4530 /// `is_valid` and every error as `(schema path, message)` per instance, under a 2020-12
4531 /// meta-schema that enables only `vocabulary` beside core.
4532 pub(crate) fn outcomes_with_only_vocabulary(
4533 vocabulary: &str,
4534 schema: &Value,
4535 instances: &[Value],
4536 ) -> Vec<(bool, Vec<(String, String)>)> {
4537 let meta_id = format!("json-schema:///meta/only-{vocabulary}");
4538 let meta = serde_json::json!({
4539 "$id": meta_id,
4540 "$schema": "https://json-schema.org/draft/2020-12/schema",
4541 "$vocabulary": {
4542 "https://json-schema.org/draft/2020-12/vocab/core": true,
4543 format!("https://json-schema.org/draft/2020-12/vocab/{vocabulary}"): true
4544 }
4545 });
4546 let registry = crate::Registry::new()
4547 .add(meta_id.as_str(), &meta)
4548 .expect("meta-schema registers")
4549 .prepare()
4550 .expect("registry prepares");
4551 let mut schema = schema.clone();
4552 schema["$schema"] = Value::String(meta_id);
4553 let validator = crate::options()
4554 .with_registry(®istry)
4555 .build(&schema)
4556 .expect("schema compiles");
4557 instances
4558 .iter()
4559 .map(|instance| {
4560 let errors = validator
4561 .iter_errors(instance)
4562 .map(|error| (error.schema_path().as_str().to_string(), error.to_string()))
4563 .collect();
4564 (validator.is_valid(instance), errors)
4565 })
4566 .collect()
4567 }
4568
4569 #[track_caller]
4570 pub(crate) fn is_not_valid(schema: &Value, instance: &Value) {
4571 let validator = crate::options()
4572 .should_validate_formats(true)
4573 .build(schema)
4574 .expect("Invalid schema");
4575 is_not_valid_with(&validator, instance);
4576 }
4577
4578 pub(crate) fn expect_errors(schema: &Value, instance: &Value, errors: &[&str]) {
4579 let mut actual = crate::validator_for(schema)
4580 .expect("Should be a valid schema")
4581 .iter_errors(instance)
4582 .map(|e| e.to_string())
4583 .collect::<Vec<String>>();
4584 actual.sort();
4585 assert_eq!(actual, errors);
4586 }
4587
4588 #[track_caller]
4589 pub(crate) fn is_valid_with(validator: &Validator, instance: &Value) {
4590 if let Some(first) = validator.iter_errors(instance).next() {
4591 panic!(
4592 "{} should be valid (via validate). Error: {} at {}",
4593 instance,
4594 first,
4595 first.instance_path()
4596 );
4597 }
4598 assert!(
4599 validator.is_valid(instance),
4600 "{instance} should be valid (via is_valid)",
4601 );
4602 assert!(
4603 validator.validate(instance).is_ok(),
4604 "{instance} should be valid (via is_valid)",
4605 );
4606 let evaluation = validator.evaluate(instance);
4607 assert!(
4608 evaluation.flag().valid,
4609 "{instance} should be valid (via evaluate)",
4610 );
4611 }
4612
4613 #[track_caller]
4614 pub(crate) fn is_valid(schema: &Value, instance: &Value) {
4615 let validator = crate::options()
4616 .should_validate_formats(true)
4617 .build(schema)
4618 .expect("Invalid schema");
4619 is_valid_with(&validator, instance);
4620 }
4621
4622 #[track_caller]
4623 pub(crate) fn validate(schema: &Value, instance: &Value) -> ValidationError<'static> {
4624 let validator = crate::options()
4625 .should_validate_formats(true)
4626 .build(schema)
4627 .expect("Invalid schema");
4628 let err = validator
4629 .validate(instance)
4630 .expect_err("Should be an error")
4631 .to_owned();
4632 err
4633 }
4634
4635 #[track_caller]
4636 pub(crate) fn assert_schema_location(schema: &Value, instance: &Value, expected: &str) {
4637 let error = validate(schema, instance);
4638 assert_eq!(error.schema_path().as_str(), expected);
4639 }
4640
4641 #[track_caller]
4642 pub(crate) fn assert_absolute_keyword_locations(
4643 schema: &Value,
4644 instance: &Value,
4645 expected: &[(&str, &str)],
4646 ) {
4647 let validator = crate::validator_for(schema).expect("Invalid schema");
4648 let actual: Vec<(String, String)> = validator
4649 .iter_errors(instance)
4650 .map(|error| {
4651 (
4652 error.kind().keyword().to_string(),
4653 error
4654 .absolute_keyword_location()
4655 .expect("Absolute keyword location")
4656 .to_string(),
4657 )
4658 })
4659 .collect();
4660 let expected: Vec<(String, String)> = expected
4661 .iter()
4662 .map(|(keyword, location)| ((*keyword).to_string(), (*location).to_string()))
4663 .collect();
4664 assert_eq!(actual, expected);
4665 }
4666
4667 /// Message, instance path, schema path and absolute keyword location of an error.
4668 type ErrorLocations = (String, String, String, Option<String>);
4669
4670 fn error_locations(error: &ValidationError<'_>) -> ErrorLocations {
4671 (
4672 error.to_string(),
4673 error.instance_path().as_str().to_owned(),
4674 error.schema_path().as_str().to_owned(),
4675 error.absolute_keyword_location().map(ToString::to_string),
4676 )
4677 }
4678
4679 /// Asserts the errors from `iter_errors` and the one from `validate` against `expected`
4680 /// (message, instance path, schema path), with `schema` both at the root of a resource and
4681 /// behind a `$ref`. The absolute keyword location must name the schema path.
4682 #[track_caller]
4683 pub(crate) fn assert_error_locations(
4684 schema: &Value,
4685 instance: &Value,
4686 expected: &[(&str, &str, &str)],
4687 ) {
4688 const BASE: &str = "https://example.com/s.json";
4689 let mut root = schema.clone();
4690 root["$id"] = Value::from(BASE);
4691 let behind_ref =
4692 serde_json::json!({"$id": BASE, "$defs": {"t": schema}, "$ref": "#/$defs/t"});
4693 for (schema, prefix) in [(root, ""), (behind_ref, "/$defs/t")] {
4694 let validator = crate::validator_for(&schema).expect("Invalid schema");
4695 let expected: Vec<ErrorLocations> = expected
4696 .iter()
4697 .map(|(message, instance_path, schema_path)| {
4698 (
4699 (*message).to_owned(),
4700 (*instance_path).to_owned(),
4701 format!("{prefix}{schema_path}"),
4702 Some(format!("{BASE}#{prefix}{schema_path}")),
4703 )
4704 })
4705 .collect();
4706 let actual: Vec<ErrorLocations> = validator
4707 .iter_errors(instance)
4708 .map(|error| error_locations(&error))
4709 .collect();
4710 assert_eq!(actual, expected, "iter_errors for {schema}");
4711 let first = validator
4712 .validate(instance)
4713 .expect_err("Should be an error");
4714 assert_eq!(
4715 Some(error_locations(&first)),
4716 expected.first().cloned(),
4717 "validate for {schema}"
4718 );
4719 }
4720 }
4721
4722 #[track_caller]
4723 pub(crate) fn assert_evaluation_path(schema: &Value, instance: &Value, expected: &str) {
4724 let error = validate(schema, instance);
4725 assert_eq!(error.evaluation_path().as_str(), expected);
4726 }
4727
4728 #[track_caller]
4729 pub(crate) fn assert_locations(schema: &Value, instance: &Value, expected: &[&str]) {
4730 let validator = crate::validator_for(schema).unwrap();
4731 let mut errors: Vec<_> = validator
4732 .iter_errors(instance)
4733 .map(|error| error.schema_path().as_str().to_string())
4734 .collect();
4735 errors.sort();
4736 for (error, location) in errors.into_iter().zip(expected) {
4737 assert_eq!(error, *location);
4738 }
4739 }
4740
4741 #[track_caller]
4742 pub(crate) fn assert_keyword_location(
4743 validator: &Validator,
4744 instance: &Value,
4745 instance_pointer: &str,
4746 keyword_pointer: &str,
4747 ) {
4748 fn pointer_from_schema_location(location: &str) -> &str {
4749 location
4750 .split_once('#')
4751 .map_or(location, |(_, fragment)| fragment)
4752 }
4753
4754 let evaluation = validator.evaluate(instance);
4755 let serialized =
4756 serde_json::to_value(evaluation.list()).expect("List output should be serializable");
4757 let details = serialized
4758 .get("details")
4759 .and_then(|value| value.as_array())
4760 .expect("List output must contain details");
4761 let mut available = Vec::new();
4762 for entry in details {
4763 let Some(instance_location) = entry
4764 .get("instanceLocation")
4765 .and_then(|value| value.as_str())
4766 else {
4767 continue;
4768 };
4769 if instance_location != instance_pointer {
4770 continue;
4771 }
4772 let schema_location = entry
4773 .get("schemaLocation")
4774 .and_then(|value| value.as_str())
4775 .unwrap_or("");
4776 let pointer = pointer_from_schema_location(schema_location);
4777 if pointer == keyword_pointer {
4778 return;
4779 }
4780 available.push(pointer.to_string());
4781 }
4782
4783 panic!(
4784 "No annotation for instance pointer `{instance_pointer}` with keyword location `{keyword_pointer}`. Available keyword locations for pointer: {available:?}"
4785 );
4786 }
4787
4788 #[track_caller]
4789 pub(crate) fn is_valid_with_draft4(schema: &Value, instance: &Value) {
4790 let validator = crate::options()
4791 .with_draft(crate::Draft::Draft4)
4792 .should_validate_formats(true)
4793 .build(schema)
4794 .expect("Invalid schema");
4795 is_valid_with(&validator, instance);
4796 }
4797
4798 #[track_caller]
4799 pub(crate) fn is_not_valid_with_draft4(schema: &Value, instance: &Value) {
4800 let validator = crate::options()
4801 .with_draft(crate::Draft::Draft4)
4802 .should_validate_formats(true)
4803 .build(schema)
4804 .expect("Invalid schema");
4805 is_not_valid_with(&validator, instance);
4806 }
4807
4808 /// Compiles `subschema` behind a `$ref`: the meta-schema never sees a subschema under an
4809 /// unknown keyword, so malformed keyword values reach the keyword compilers. `location` is
4810 /// relative to `subschema`.
4811 #[track_caller]
4812 pub(crate) fn assert_compile_error_with(
4813 options: &crate::ValidationOptions,
4814 subschema: &Value,
4815 message: &str,
4816 location: &str,
4817 ) {
4818 let error = options
4819 .build(&serde_json::json!({"$ref": "#/x", "x": subschema}))
4820 .expect_err("Should fail to compile");
4821 let location = format!("/x{location}");
4822 assert_eq!(
4823 (
4824 error.to_string(),
4825 error.instance_path().as_str(),
4826 error.schema_path().as_str()
4827 ),
4828 (message.to_string(), location.as_str(), location.as_str())
4829 );
4830 }
4831
4832 #[track_caller]
4833 pub(crate) fn assert_compile_error(subschema: &Value, message: &str, location: &str) {
4834 assert_compile_error_with(&crate::options(), subschema, message, location);
4835 }
4836}
4837
4838#[cfg(test)]
4839mod tests {
4840 use crate::{validator_for, Registry, SerdeJson, ValidationError};
4841
4842 use super::Draft;
4843 use serde_json::{json, Value};
4844 use test_case::test_case;
4845
4846 // A build error judges the schema itself, so its instance path is the location that failed
4847 // to compile.
4848 #[test_case(&json!({"type": "object", "properties": {"sku": {"type": "string", "pattern": "([unclosed"}}}), "/properties/sku/pattern")]
4849 #[test_case(&json!({"patternProperties": {"([unclosed": {"type": "string"}}}), "/patternProperties/([unclosed")]
4850 fn build_error_instance_path(schema: &Value, expected: &str) {
4851 let error = validator_for(schema).expect_err("Should fail to compile");
4852 assert_eq!(error.instance_path().as_str(), expected);
4853 }
4854
4855 // A subschema's `$schema` switches the draft for that subschema, whatever the root draft.
4856 #[test_case(Draft::Draft4, &json!({"$schema": "https://json-schema.org/draft/2020-12/schema", "prefixItems": [{"type": "string"}]}), false ; "2020-12 under draft 4")]
4857 #[test_case(Draft::Draft4, &json!({"$schema": "https://json-schema.org/draft/2019-09/schema", "items": [{"type": "string"}]}), false ; "2019-09 under draft 4")]
4858 #[test_case(Draft::Draft7, &json!({"$schema": "https://json-schema.org/draft/2020-12/schema", "prefixItems": [{"type": "string"}]}), false ; "2020-12 under draft 7")]
4859 #[test_case(Draft::Draft4, &json!({"$schema": "http://json-schema.org/draft-07/schema#", "items": [{"type": "string"}]}), false ; "draft 7 under draft 4")]
4860 #[test_case(Draft::Draft201909, &json!({"$schema": "https://json-schema.org/draft/2020-12/schema", "prefixItems": [{"type": "string"}]}), false ; "2020-12 under 2019-09")]
4861 #[test_case(Draft::Draft202012, &json!({"$schema": "http://json-schema.org/draft-04/schema#", "prefixItems": [{"type": "string"}]}), true ; "draft 4 under 2020-12")]
4862 fn nested_schema_switches_draft(root: Draft, pair: &Value, expected: bool) {
4863 let validator = crate::options()
4864 .with_draft(root)
4865 .build(&json!({"properties": {"pair": pair}}))
4866 .expect("Should build validator");
4867 assert_eq!(validator.is_valid(&json!({"pair": [1]})), expected);
4868 }
4869
4870 // The fragment of an absolute keyword location is a JSON Pointer from the root of the
4871 // resource that holds the keyword, not from the root of the document embedding it.
4872 #[test_case(
4873 &json!({"properties": {"tree": {"$id": "https://example.com/tree", "type": "object", "properties": {"value": {"type": "string"}}}}}),
4874 &json!({"tree": {"value": 1}}),
4875 &["https://example.com/tree#/properties/value/type"];
4876 "draft 2020-12"
4877 )]
4878 #[test_case(
4879 &json!({"properties": {"tree": {"$id": "https://example.com/tree", "type": "object"}}}),
4880 &json!({"tree": 1}),
4881 &["https://example.com/tree#/type"];
4882 "keyword at resource root"
4883 )]
4884 #[test_case(
4885 &json!({"$schema": "https://json-schema.org/draft/2019-09/schema", "properties": {"tree": {"$id": "https://example.com/tree", "properties": {"value": {"type": "string"}}}}}),
4886 &json!({"tree": {"value": 1}}),
4887 &["https://example.com/tree#/properties/value/type"];
4888 "draft 2019-09"
4889 )]
4890 #[test_case(
4891 &json!({"$schema": "http://json-schema.org/draft-07/schema#", "properties": {"tree": {"$id": "https://example.com/tree", "properties": {"value": {"type": "string"}}}}}),
4892 &json!({"tree": {"value": 1}}),
4893 &["https://example.com/tree#/properties/value/type"];
4894 "draft 7"
4895 )]
4896 #[test_case(
4897 &json!({"$schema": "http://json-schema.org/draft-06/schema#", "properties": {"tree": {"$id": "https://example.com/tree", "properties": {"value": {"type": "string"}}}}}),
4898 &json!({"tree": {"value": 1}}),
4899 &["https://example.com/tree#/properties/value/type"];
4900 "draft 6"
4901 )]
4902 #[test_case(
4903 &json!({"$schema": "http://json-schema.org/draft-04/schema#", "properties": {"tree": {"id": "https://example.com/tree", "properties": {"value": {"type": "string"}}}}}),
4904 &json!({"tree": {"value": 1}}),
4905 &["https://example.com/tree#/properties/value/type"];
4906 "draft 4"
4907 )]
4908 #[test_case(
4909 &json!({"$id": "https://example.com/root.json", "properties": {"tree": {"$id": "tree.json", "properties": {"value": {"type": "string"}}}}}),
4910 &json!({"tree": {"value": 1}}),
4911 &["https://example.com/tree.json#/properties/value/type"];
4912 "relative id"
4913 )]
4914 #[test_case(
4915 &json!({
4916 "$id": "https://example.com/root.json",
4917 "properties": {
4918 "outer": {
4919 "$id": "outer/",
4920 "properties": {
4921 "inner": {"$id": "inner.json", "properties": {"value": {"type": "string"}}},
4922 "count": {"type": "integer"}
4923 }
4924 },
4925 "name": {"type": "string"}
4926 }
4927 }),
4928 &json!({"outer": {"inner": {"value": 1}, "count": "x"}, "name": 1}),
4929 &[
4930 "https://example.com/outer/#/properties/count/type",
4931 "https://example.com/outer/inner.json#/properties/value/type",
4932 "https://example.com/root.json#/properties/name/type",
4933 ];
4934 "id nested in id"
4935 )]
4936 #[test_case(
4937 &json!({"$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://example.com/root", "properties": {"tree": {"$id": "#tree", "properties": {"value": {"type": "string"}}}}}),
4938 &json!({"tree": {"value": 1}}),
4939 &["https://example.com/root#/properties/tree/properties/value/type"];
4940 "draft 7 fragment-only id names no resource"
4941 )]
4942 #[test_case(
4943 &json!({"$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://example.com/root", "properties": {"tree": {"$id": "https://example.com/tree", "$ref": "#/definitions/leaf"}}, "definitions": {"leaf": {"type": "string"}}}),
4944 &json!({"tree": 1}),
4945 &["https://example.com/root#/definitions/leaf/type"];
4946 "draft 7 id beside ref names no resource"
4947 )]
4948 fn absolute_keyword_location_in_nested_resource(
4949 schema: &Value,
4950 instance: &Value,
4951 expected: &[&str],
4952 ) {
4953 let validator = validator_for(schema).expect("Should build validator");
4954 let mut locations: Vec<String> = validator
4955 .iter_errors(instance)
4956 .map(|error| {
4957 error
4958 .absolute_keyword_location()
4959 .expect("Absolute keyword location")
4960 .to_string()
4961 })
4962 .collect();
4963 locations.sort();
4964 assert_eq!(locations, expected);
4965 let evaluation = validator.evaluate(instance);
4966 let mut locations: Vec<String> = evaluation
4967 .iter_errors()
4968 .map(|entry| {
4969 entry
4970 .absolute_keyword_location
4971 .expect("Absolute keyword location")
4972 .to_string()
4973 })
4974 .collect();
4975 locations.sort();
4976 assert_eq!(locations, expected);
4977 }
4978
4979 #[test_case(
4980 &json!({"properties": {"tree": {"$id": "https://example.com/tree", "properties": {"value": false}}}}),
4981 &json!({"tree": {"value": 1}}),
4982 &[("https://example.com/tree#/properties/value", Some("https://example.com/tree#/properties/value"))],
4983 &[Some("https://example.com/tree#/properties/value")];
4984 "false subschema in nested resource"
4985 )]
4986 #[test_case(
4987 &json!({"properties": {"tree": {"$id": "https://example.com/tree", "unevaluatedProperties": false}}}),
4988 &json!({"tree": {"a": 1, "b": 2}}),
4989 &[
4990 ("https://example.com/tree#/unevaluatedProperties", Some("https://example.com/tree#/unevaluatedProperties")),
4991 ("https://example.com/tree#/unevaluatedProperties", Some("https://example.com/tree#/unevaluatedProperties")),
4992 ("https://example.com/tree#/unevaluatedProperties", Some("https://example.com/tree#/unevaluatedProperties")),
4993 ],
4994 &[Some("https://example.com/tree#/unevaluatedProperties")];
4995 "unevaluatedProperties false in nested resource"
4996 )]
4997 #[test_case(
4998 &json!({"$id": "https://example.com/root", "$ref": "#/$defs/never", "$defs": {"never": false}}),
4999 &json!(1),
5000 &[("https://example.com/root#/$defs/never", Some("https://example.com/root#/$defs/never"))],
5001 &[Some("https://example.com/root#/$defs/never")];
5002 "ref to false in same resource"
5003 )]
5004 #[test_case(
5005 &json!({"$id": "https://example.com/root", "$ref": "other#/$defs/never", "$defs": {"other": {"$id": "other", "$defs": {"never": false}}}}),
5006 &json!(1),
5007 &[("https://example.com/other#/$defs/never", Some("https://example.com/other#/$defs/never"))],
5008 &[Some("https://example.com/other#/$defs/never")];
5009 "ref to false in other resource"
5010 )]
5011 #[test_case(
5012 &json!({"properties": {"value": false}}),
5013 &json!({"value": 1}),
5014 &[("/properties/value", None)],
5015 &[None];
5016 "false subschema without id"
5017 )]
5018 fn false_schema_error_locations(
5019 schema: &Value,
5020 instance: &Value,
5021 evaluate: &[(&str, Option<&str>)],
5022 iter_errors: &[Option<&str>],
5023 ) {
5024 let validator = validator_for(schema).expect("Should build validator");
5025 let evaluation = validator.evaluate(instance);
5026 let locations: Vec<(&str, Option<String>)> = evaluation
5027 .iter_errors()
5028 .map(|entry| {
5029 (
5030 entry.schema_location,
5031 entry.absolute_keyword_location.map(ToString::to_string),
5032 )
5033 })
5034 .collect();
5035 let expected: Vec<(&str, Option<String>)> = evaluate
5036 .iter()
5037 .map(|(schema_location, absolute)| (*schema_location, absolute.map(str::to_string)))
5038 .collect();
5039 assert_eq!(locations, expected);
5040 let locations: Vec<Option<String>> = validator
5041 .iter_errors(instance)
5042 .map(|error| error.absolute_keyword_location().map(ToString::to_string))
5043 .collect();
5044 let expected: Vec<Option<String>> = iter_errors
5045 .iter()
5046 .map(|absolute| absolute.map(str::to_string))
5047 .collect();
5048 assert_eq!(locations, expected);
5049 }
5050
5051 #[test_case(
5052 &json!({"$id": "https://example.com/tree", "properties": {"a": {"type": "string"}, "b": {"type": "string"}}, "required": ["a", "b"]}),
5053 &json!({}),
5054 &["", "/properties", "https://example.com/tree#", "https://example.com/tree#/properties", "https://example.com/tree#/required"];
5055 "required"
5056 )]
5057 #[test_case(
5058 &json!({"$id": "https://example.com/tree", "minLength": 1, "maxLength": 2}),
5059 &json!("abc"),
5060 &["", "/properties", "https://example.com/tree#", "https://example.com/tree#/minLength", "https://example.com/tree#/maxLength"];
5061 "maxLength beside minLength"
5062 )]
5063 #[test_case(
5064 &json!({"$id": "https://example.com/tree", "type": "array", "minItems": 2, "maxItems": 0, "items": {"type": "string"}}),
5065 &json!([1]),
5066 &["", "/properties", "https://example.com/tree#", "https://example.com/tree#/type", "https://example.com/tree#/minItems", "https://example.com/tree#/maxItems", "https://example.com/tree#/items", "https://example.com/tree#/items", "https://example.com/tree#/items/type"];
5067 "array bounds beside items"
5068 )]
5069 #[test_case(
5070 &json!({"$id": "https://example.com/tree", "type": "array", "items": {"type": "string"}}),
5071 &json!(1),
5072 &["", "/properties", "https://example.com/tree#", "https://example.com/tree#/type", "https://example.com/tree#/items"];
5073 "type beside items"
5074 )]
5075 #[test_case(
5076 &json!({"$id": "https://example.com/tree", "patternProperties": {"^a": {}}, "additionalProperties": false}),
5077 &json!({"a": 1, "b": 1}),
5078 &["", "/properties", "https://example.com/tree#", "https://example.com/tree#/additionalProperties", "https://example.com/tree#/patternProperties/%5Ea", "https://example.com/tree#/patternProperties"];
5079 "patternProperties beside additionalProperties"
5080 )]
5081 fn list_schema_locations_in_nested_resource(tree: &Value, instance: &Value, expected: &[&str]) {
5082 let validator =
5083 validator_for(&json!({"properties": {"tree": tree}})).expect("Should build validator");
5084 let evaluation = validator.evaluate(&json!({"tree": instance}));
5085 let list = serde_json::to_value(evaluation.list()).expect("List output");
5086 let locations: Vec<&str> = list["details"]
5087 .as_array()
5088 .expect("List output details")
5089 .iter()
5090 .map(|entry| entry["schemaLocation"].as_str().expect("Schema location"))
5091 .collect();
5092 assert_eq!(locations, expected);
5093 }
5094
5095 #[test]
5096 fn output_locations_in_nested_resource() {
5097 let validator = validator_for(&json!({
5098 "properties": {
5099 "tree": {"$id": "https://example.com/tree", "properties": {"value": {"type": "string"}}}
5100 }
5101 }))
5102 .expect("Should build validator");
5103
5104 let evaluation = validator.evaluate(&json!({"tree": {"value": 1}}));
5105 let list = serde_json::to_value(evaluation.list()).expect("List output");
5106 let locations: Vec<&str> = list["details"]
5107 .as_array()
5108 .expect("List output details")
5109 .iter()
5110 .map(|entry| entry["schemaLocation"].as_str().expect("Schema location"))
5111 .collect();
5112 assert_eq!(
5113 locations,
5114 [
5115 "",
5116 "/properties",
5117 "https://example.com/tree#",
5118 "https://example.com/tree#/properties",
5119 "https://example.com/tree#/properties/value",
5120 "https://example.com/tree#/properties/value/type",
5121 ]
5122 );
5123
5124 let evaluation = validator.evaluate(&json!({"tree": {"value": "leaf"}}));
5125 let annotations: Vec<Option<String>> = evaluation
5126 .iter_annotations()
5127 .map(|entry| entry.absolute_keyword_location.map(ToString::to_string))
5128 .collect();
5129 assert_eq!(
5130 annotations,
5131 [
5132 None,
5133 Some("https://example.com/tree#/properties".to_string())
5134 ]
5135 );
5136 }
5137
5138 #[test_case(
5139 &json!({"$id": "https://example.com/root", "patternProperties": {"^a": {}}, "additionalProperties": false}),
5140 &json!({"a": 1}),
5141 &[("https://example.com/root#/patternProperties", Some("https://example.com/root#/patternProperties"))];
5142 "patternProperties beside additionalProperties at resource root"
5143 )]
5144 #[test_case(
5145 &json!({"properties": {"tree": {"$id": "https://example.com/tree", "patternProperties": {"^a": {}}, "additionalProperties": false}}}),
5146 &json!({"tree": {"a": 1}}),
5147 &[
5148 ("/properties", None),
5149 ("https://example.com/tree#/patternProperties", Some("https://example.com/tree#/patternProperties")),
5150 ];
5151 "patternProperties beside additionalProperties in nested resource"
5152 )]
5153 #[test_case(
5154 &json!({"properties": {"tree": {"$id": "https://example.com/tree", "unknown": 1}}}),
5155 &json!({"tree": 1}),
5156 &[
5157 ("/properties", None),
5158 ("https://example.com/tree#", Some("https://example.com/tree#")),
5159 ];
5160 "unknown keyword at nested resource root"
5161 )]
5162 fn annotation_locations(schema: &Value, instance: &Value, expected: &[(&str, Option<&str>)]) {
5163 let validator = validator_for(schema).expect("Should build validator");
5164 let evaluation = validator.evaluate(instance);
5165 let locations: Vec<(&str, Option<String>)> = evaluation
5166 .iter_annotations()
5167 .map(|entry| {
5168 (
5169 entry.schema_location,
5170 entry.absolute_keyword_location.map(ToString::to_string),
5171 )
5172 })
5173 .collect();
5174 let expected: Vec<(&str, Option<String>)> = expected
5175 .iter()
5176 .map(|(schema_location, absolute)| (*schema_location, absolute.map(str::to_string)))
5177 .collect();
5178 assert_eq!(locations, expected);
5179 }
5180
5181 fn property_with_space() -> crate::Validator {
5182 validator_for(&json!({
5183 "$id": "https://example.com/root",
5184 "properties": {"s p": {"type": "string"}}
5185 }))
5186 .expect("Should build validator")
5187 }
5188
5189 #[test]
5190 fn list_output_encodes_schema_locations() {
5191 let validator = property_with_space();
5192 let evaluation = validator.evaluate(&json!({"s p": 1}));
5193 assert_eq!(
5194 serde_json::to_value(evaluation.list()).expect("List output"),
5195 json!({
5196 "valid": false,
5197 "details": [
5198 {
5199 "valid": false,
5200 "evaluationPath": "",
5201 "schemaLocation": "https://example.com/root#",
5202 "instanceLocation": ""
5203 },
5204 {
5205 "valid": false,
5206 "evaluationPath": "/properties",
5207 "schemaLocation": "https://example.com/root#/properties",
5208 "instanceLocation": "",
5209 "droppedAnnotations": ["s p"]
5210 },
5211 {
5212 "valid": false,
5213 "evaluationPath": "/properties/s p",
5214 "schemaLocation": "https://example.com/root#/properties/s%20p",
5215 "instanceLocation": "/s p"
5216 },
5217 {
5218 "valid": false,
5219 "evaluationPath": "/properties/s p/type",
5220 "schemaLocation": "https://example.com/root#/properties/s%20p/type",
5221 "instanceLocation": "/s p",
5222 "errors": {"type": "1 is not of type \"string\""}
5223 }
5224 ]
5225 })
5226 );
5227 }
5228
5229 #[test]
5230 fn hierarchical_output_encodes_schema_locations() {
5231 let validator = property_with_space();
5232 let evaluation = validator.evaluate(&json!({"s p": 1}));
5233 assert_eq!(
5234 serde_json::to_value(evaluation.hierarchical()).expect("Hierarchical output"),
5235 json!({
5236 "valid": false,
5237 "evaluationPath": "",
5238 "schemaLocation": "https://example.com/root#",
5239 "instanceLocation": "",
5240 "details": [
5241 {
5242 "valid": false,
5243 "evaluationPath": "/properties",
5244 "schemaLocation": "https://example.com/root#/properties",
5245 "instanceLocation": "",
5246 "droppedAnnotations": ["s p"],
5247 "details": [
5248 {
5249 "valid": false,
5250 "evaluationPath": "/properties/s p",
5251 "schemaLocation": "https://example.com/root#/properties/s%20p",
5252 "instanceLocation": "/s p",
5253 "details": [
5254 {
5255 "valid": false,
5256 "evaluationPath": "/properties/s p/type",
5257 "schemaLocation": "https://example.com/root#/properties/s%20p/type",
5258 "instanceLocation": "/s p",
5259 "errors": {"type": "1 is not of type \"string\""}
5260 }
5261 ]
5262 }
5263 ]
5264 }
5265 ]
5266 })
5267 );
5268 }
5269
5270 #[test]
5271 fn annotation_locations_are_encoded() {
5272 let validator = validator_for(&json!({
5273 "$id": "https://example.com/root",
5274 "properties": {"s p": {"properties": {"a b": true}}}
5275 }))
5276 .expect("Should build validator");
5277 let evaluation = validator.evaluate(&json!({"s p": {"a b": 1}}));
5278 let locations: Vec<(&str, Option<String>, &str)> = evaluation
5279 .iter_annotations()
5280 .map(|entry| {
5281 (
5282 entry.schema_location,
5283 entry.absolute_keyword_location.map(ToString::to_string),
5284 entry.instance_location.as_str(),
5285 )
5286 })
5287 .collect();
5288 assert_eq!(
5289 locations,
5290 [
5291 (
5292 "https://example.com/root#/properties",
5293 Some("https://example.com/root#/properties".to_string()),
5294 ""
5295 ),
5296 (
5297 "https://example.com/root#/properties/s%20p/properties",
5298 Some("https://example.com/root#/properties/s%20p/properties".to_string()),
5299 "/s p"
5300 ),
5301 ]
5302 );
5303 assert_eq!(
5304 serde_json::to_value(evaluation.list()).expect("List output"),
5305 json!({
5306 "valid": true,
5307 "details": [
5308 {
5309 "valid": true,
5310 "evaluationPath": "",
5311 "schemaLocation": "https://example.com/root#",
5312 "instanceLocation": ""
5313 },
5314 {
5315 "valid": true,
5316 "evaluationPath": "/properties",
5317 "schemaLocation": "https://example.com/root#/properties",
5318 "instanceLocation": "",
5319 "annotations": ["s p"]
5320 },
5321 {
5322 "valid": true,
5323 "evaluationPath": "/properties/s p",
5324 "schemaLocation": "https://example.com/root#/properties/s%20p",
5325 "instanceLocation": "/s p"
5326 },
5327 {
5328 "valid": true,
5329 "evaluationPath": "/properties/s p/properties",
5330 "schemaLocation": "https://example.com/root#/properties/s%20p/properties",
5331 "instanceLocation": "/s p",
5332 "annotations": ["a b"]
5333 },
5334 {
5335 "valid": true,
5336 "evaluationPath": "/properties/s p/properties/a b",
5337 "schemaLocation": "https://example.com/root#/properties/s%20p/properties/a%20b",
5338 "instanceLocation": "/s p/a b"
5339 }
5340 ]
5341 })
5342 );
5343 }
5344
5345 #[test_case("s p", "s p", "s%20p")]
5346 #[test_case("a%b", "a%b", "a%25b")]
5347 #[test_case("q\"t", "q\"t", "q%22t")]
5348 #[test_case("c^d", "c^d", "c%5Ed")]
5349 #[test_case("[x]", "[x]", "%5Bx%5D")]
5350 #[test_case("{y}", "{y}", "%7By%7D")]
5351 #[test_case("p|q", "p|q", "p%7Cq")]
5352 #[test_case("b\\s", "b\\s", "b%5Cs")]
5353 #[test_case("<>", "<>", "%3C%3E")]
5354 #[test_case("`b", "`b", "%60b")]
5355 #[test_case("#h", "#h", "%23h")]
5356 #[test_case("é", "é", "%C3%A9")]
5357 #[test_case("t~e", "t~0e", "t~0e")]
5358 #[test_case("s/l", "s~1l", "s~1l")]
5359 fn error_locations_agree_on_encoding(name: &str, pointer: &str, encoded: &str) {
5360 let validator = validator_for(&json!({
5361 "$id": "https://example.com/root",
5362 "properties": {name: {"type": "string"}}
5363 }))
5364 .expect("Should build validator");
5365 let instance = json!({name: 1});
5366 let expected = vec![(
5367 format!("/properties/{pointer}/type"),
5368 format!("https://example.com/root#/properties/{encoded}/type"),
5369 format!("/{pointer}"),
5370 )];
5371
5372 let from_iter_errors: Vec<(String, String, String)> = validator
5373 .iter_errors(&instance)
5374 .map(|error| {
5375 (
5376 error.schema_path().to_string(),
5377 error
5378 .absolute_keyword_location()
5379 .expect("Absolute keyword location")
5380 .to_string(),
5381 error.instance_path().to_string(),
5382 )
5383 })
5384 .collect();
5385 assert_eq!(from_iter_errors, expected);
5386
5387 let evaluation = validator.evaluate(&instance);
5388 let from_entries: Vec<(String, Option<String>, String)> = evaluation
5389 .iter_errors()
5390 .map(|entry| {
5391 (
5392 entry.schema_location.to_string(),
5393 entry.absolute_keyword_location.map(ToString::to_string),
5394 entry.instance_location.to_string(),
5395 )
5396 })
5397 .collect();
5398 assert_eq!(
5399 from_entries,
5400 [(
5401 expected[0].1.clone(),
5402 Some(expected[0].1.clone()),
5403 expected[0].2.clone()
5404 )]
5405 );
5406
5407 let list = serde_json::to_value(evaluation.list()).expect("List output");
5408 let from_list: Vec<(String, String, String)> = list["details"]
5409 .as_array()
5410 .expect("List output details")
5411 .iter()
5412 .filter(|entry| entry.get("errors").is_some())
5413 .map(|entry| {
5414 let field = |key: &str| entry[key].as_str().expect("Location").to_string();
5415 (
5416 field("evaluationPath"),
5417 field("schemaLocation"),
5418 field("instanceLocation"),
5419 )
5420 })
5421 .collect();
5422 assert_eq!(from_list, expected);
5423 }
5424
5425 #[test]
5426 fn ref_target_location_is_encoded() {
5427 let validator = validator_for(&json!({
5428 "$id": "https://example.com/root",
5429 "$ref": "#/$defs/s%20p",
5430 "$defs": {"s p": {"type": "string"}}
5431 }))
5432 .expect("Should build validator");
5433 let evaluation = validator.evaluate(&json!(1));
5434 let list = serde_json::to_value(evaluation.list()).expect("List output");
5435 let locations: Vec<(&str, &str)> = list["details"]
5436 .as_array()
5437 .expect("List output details")
5438 .iter()
5439 .map(|entry| {
5440 (
5441 entry["evaluationPath"].as_str().expect("Evaluation path"),
5442 entry["schemaLocation"].as_str().expect("Schema location"),
5443 )
5444 })
5445 .collect();
5446 assert_eq!(
5447 locations,
5448 [
5449 ("", "https://example.com/root#"),
5450 ("/$ref", "https://example.com/root#/$defs/s%20p"),
5451 ("/$ref/type", "https://example.com/root#/$defs/s%20p/type"),
5452 ]
5453 );
5454 let absolute: Vec<Option<String>> = validator
5455 .iter_errors(&json!(1))
5456 .map(|error| error.absolute_keyword_location().map(ToString::to_string))
5457 .collect();
5458 assert_eq!(
5459 absolute,
5460 [Some(
5461 "https://example.com/root#/$defs/s%20p/type".to_string()
5462 )]
5463 );
5464 }
5465
5466 const DRAFT4: &str = "http://json-schema.org/draft-04/schema#";
5467 const DRAFT6: &str = "http://json-schema.org/draft-06/schema#";
5468 const DRAFT7: &str = "http://json-schema.org/draft-07/schema#";
5469 const DRAFT202012: &str = "https://json-schema.org/draft/2020-12/schema";
5470
5471 fn with_embedded_resource(root: &str, resource: &Value) -> Value {
5472 json!({"$schema": root, "properties": {"a": resource}})
5473 }
5474
5475 // An embedded resource is checked against the meta-schema of its own `$schema`, not the root's.
5476 #[test_case(DRAFT202012, &json!({"$id": "https://example.com/a", "$schema": DRAFT4, "items": [{"type": "string"}]}), &json!(["x"]), &json!([1]) ; "draft 4 tuple items under 2020-12")]
5477 #[test_case(DRAFT202012, &json!({"$id": "https://example.com/a", "$schema": DRAFT4, "minimum": 5, "exclusiveMinimum": true}), &json!(6), &json!(5) ; "draft 4 boolean exclusiveMinimum under 2020-12")]
5478 #[test_case(DRAFT6, &json!({"$id": "https://example.com/a", "$schema": DRAFT4, "minimum": 5, "exclusiveMinimum": true}), &json!(6), &json!(5) ; "draft 4 boolean exclusiveMinimum under draft 6")]
5479 #[test_case(DRAFT4, &json!({"$id": "https://example.com/a", "$schema": DRAFT202012, "exclusiveMinimum": 5}), &json!(6), &json!(5) ; "2020-12 numeric exclusiveMinimum under draft 4")]
5480 fn embedded_resource_follows_own_meta_schema(
5481 root: &str,
5482 resource: &Value,
5483 valid: &Value,
5484 invalid: &Value,
5485 ) {
5486 let validator =
5487 validator_for(&with_embedded_resource(root, resource)).expect("Should build validator");
5488 assert_eq!(
5489 (
5490 validator.is_valid(&json!({"a": valid})),
5491 validator.is_valid(&json!({"a": invalid}))
5492 ),
5493 (true, false)
5494 );
5495 }
5496
5497 // The error points at the offending keyword inside the embedded resource.
5498 #[test_case(DRAFT7, &json!({"$id": "https://example.com/a", "$schema": DRAFT202012, "minContains": -1}), "/properties/a/minContains" ; "2020-12 under draft 7")]
5499 #[test_case(DRAFT202012, &json!({"$id": "https://example.com/a", "$schema": DRAFT4, "minimum": 1, "exclusiveMinimum": 5}), "/properties/a/exclusiveMinimum" ; "draft 4 under 2020-12")]
5500 #[test_case(DRAFT202012, &json!({"$id": "https://example.com/a", "$schema": DRAFT4, "properties": {"b": {"$id": "https://example.com/b", "$schema": DRAFT202012, "minContains": -1}}}), "/properties/a/properties/b/minContains" ; "2020-12 under draft 4 under 2020-12")]
5501 fn embedded_resource_rejected_by_own_meta_schema(root: &str, resource: &Value, expected: &str) {
5502 let error = validator_for(&with_embedded_resource(root, resource))
5503 .expect_err("Should reject the embedded resource");
5504 assert_eq!(error.instance_path().as_str(), expected);
5505 }
5506
5507 // Without `$id` the subschema is not a resource root, so the root's meta-schema still applies.
5508 #[test]
5509 fn nested_schema_without_id_follows_root_meta_schema() {
5510 let error = validator_for(&with_embedded_resource(
5511 DRAFT202012,
5512 &json!({"$schema": DRAFT4, "items": [{"type": "string"}]}),
5513 ))
5514 .expect_err("Should reject array-form `items` under 2020-12");
5515 assert_eq!(error.instance_path().as_str(), "/properties/a/items");
5516 }
5517
5518 #[test_case(crate::is_valid ; "autodetect")]
5519 #[test_case(crate::draft4::is_valid ; "draft4")]
5520 #[test_case(crate::draft6::is_valid ; "draft6")]
5521 #[test_case(crate::draft7::is_valid ; "draft7")]
5522 #[test_case(crate::draft201909::is_valid ; "draft201909")]
5523 #[test_case(crate::draft202012::is_valid ; "draft202012")]
5524 fn test_is_valid(is_valid_fn: fn(&serde_json::Value, &serde_json::Value) -> bool) {
5525 let schema = json!({
5526 "type": "object",
5527 "properties": {
5528 "name": {"type": "string"},
5529 "age": {"type": "integer", "minimum": 0}
5530 },
5531 "required": ["name"]
5532 });
5533
5534 let valid_instance = json!({
5535 "name": "John Doe",
5536 "age": 30
5537 });
5538
5539 let invalid_instance = json!({
5540 "age": -5
5541 });
5542
5543 assert!(is_valid_fn(&schema, &valid_instance));
5544 assert!(!is_valid_fn(&schema, &invalid_instance));
5545 }
5546
5547 #[test_case(crate::validate ; "autodetect")]
5548 #[test_case(crate::draft4::validate ; "draft4")]
5549 #[test_case(crate::draft6::validate ; "draft6")]
5550 #[test_case(crate::draft7::validate ; "draft7")]
5551 #[test_case(crate::draft201909::validate ; "draft201909")]
5552 #[test_case(crate::draft202012::validate ; "draft202012")]
5553 fn test_validate(
5554 validate_fn: for<'i> fn(
5555 &serde_json::Value,
5556 &'i serde_json::Value,
5557 ) -> Result<(), ValidationError<'i>>,
5558 ) {
5559 let schema = json!({
5560 "type": "object",
5561 "properties": {
5562 "name": {"type": "string"},
5563 "age": {"type": "integer", "minimum": 0}
5564 },
5565 "required": ["name"]
5566 });
5567
5568 let valid_instance = json!({
5569 "name": "John Doe",
5570 "age": 30
5571 });
5572
5573 let invalid_instance = json!({
5574 "age": -5
5575 });
5576
5577 assert!(validate_fn(&schema, &valid_instance).is_ok());
5578 assert!(validate_fn(&schema, &invalid_instance).is_err());
5579 }
5580
5581 #[test]
5582 fn test_evaluate() {
5583 let schema = json!({
5584 "type": "object",
5585 "properties": {
5586 "name": {"type": "string"},
5587 "age": {"type": "integer", "minimum": 0}
5588 },
5589 "required": ["name"]
5590 });
5591
5592 let valid_instance = json!({
5593 "name": "John Doe",
5594 "age": 30
5595 });
5596
5597 let invalid_instance = json!({
5598 "age": -5
5599 });
5600
5601 let valid_eval = crate::evaluate(&schema, &valid_instance);
5602 assert!(valid_eval.flag().valid);
5603
5604 let invalid_eval = crate::evaluate(&schema, &invalid_instance);
5605 assert!(!invalid_eval.flag().valid);
5606 let errors: Vec<_> = invalid_eval.iter_errors().collect();
5607 assert!(!errors.is_empty());
5608 }
5609
5610 #[test_case(crate::meta::validate, crate::meta::is_valid ; "autodetect")]
5611 #[test_case(crate::draft4::meta::validate, crate::draft4::meta::is_valid ; "draft4")]
5612 #[test_case(crate::draft6::meta::validate, crate::draft6::meta::is_valid ; "draft6")]
5613 #[test_case(crate::draft7::meta::validate, crate::draft7::meta::is_valid ; "draft7")]
5614 #[test_case(crate::draft201909::meta::validate, crate::draft201909::meta::is_valid ; "draft201909")]
5615 #[test_case(crate::draft202012::meta::validate, crate::draft202012::meta::is_valid ; "draft202012")]
5616 fn test_meta_validation(
5617 validate_fn: fn(&serde_json::Value) -> Result<(), ValidationError>,
5618 is_valid_fn: fn(&serde_json::Value) -> bool,
5619 ) {
5620 let valid = json!({
5621 "type": "object",
5622 "properties": {
5623 "name": {"type": "string"},
5624 "age": {"type": "integer", "minimum": 0}
5625 },
5626 "required": ["name"]
5627 });
5628
5629 let invalid = json!({
5630 "type": "invalid_type",
5631 "minimum": "not_a_number",
5632 "required": true // should be an array
5633 });
5634
5635 assert!(validate_fn(&valid).is_ok());
5636 assert!(validate_fn(&invalid).is_err());
5637 assert!(is_valid_fn(&valid));
5638 assert!(!is_valid_fn(&invalid));
5639 }
5640
5641 #[test_case(&json!({"type": "object", "required": ["name"]}), true ; "valid")]
5642 #[test_case(&json!({"type": "invalid_type", "required": true}), false ; "invalid")]
5643 #[test_case(&json!(true), true ; "boolean schema")]
5644 #[test_case(&json!({"$schema": "http://json-schema.org/draft-04/schema#", "minimum": 5, "exclusiveMinimum": true}), true ; "draft4 boolean exclusive minimum")]
5645 #[test_case(&json!({"$schema": "http://json-schema.org/draft-04/schema#", "exclusiveMinimum": 5}), false ; "draft4 numeric exclusive minimum")]
5646 #[test_case(&json!({"$schema": "https://json-schema.org/draft/2020-12/schema", "exclusiveMinimum": 5}), true ; "draft2020-12 numeric exclusive minimum")]
5647 fn test_meta_validate_for(schema: &serde_json::Value, expected: bool) {
5648 let result = crate::meta::validate_for::<SerdeJson>(schema);
5649 assert_eq!(result.is_ok(), expected);
5650 assert_eq!(result.is_ok(), crate::meta::validate(schema).is_ok());
5651 assert_eq!(
5652 crate::meta::is_valid_for::<SerdeJson>(schema).expect("Meta-schema is resolvable"),
5653 expected
5654 );
5655 }
5656
5657 #[test]
5658 fn test_meta_validate_for_unresolvable_meta_schema() {
5659 let schema = json!({"$schema": "htt://json-schema.org/draft-07/schema"});
5660 let error = crate::meta::validate_for::<SerdeJson>(&schema)
5661 .expect_err("Unresolvable meta-schema should fail");
5662 assert!(matches!(
5663 error.kind(),
5664 crate::error::ValidationErrorKind::Referencing(_)
5665 ));
5666 let error = crate::meta::is_valid_for::<SerdeJson>(&schema)
5667 .expect_err("Unresolvable meta-schema should fail");
5668 assert!(matches!(
5669 error.kind(),
5670 crate::error::ValidationErrorKind::Referencing(_)
5671 ));
5672 }
5673
5674 #[test]
5675 fn test_exclusive_minimum_across_drafts() {
5676 // In Draft 4, exclusiveMinimum is a boolean modifier for minimum
5677 let draft4_schema = json!({
5678 "$schema": "http://json-schema.org/draft-04/schema#",
5679 "minimum": 5,
5680 "exclusiveMinimum": true
5681 });
5682 assert!(crate::meta::is_valid(&draft4_schema));
5683 assert!(crate::meta::validate(&draft4_schema).is_ok());
5684
5685 // This is invalid in Draft 4 (exclusiveMinimum must be boolean)
5686 let invalid_draft4 = json!({
5687 "$schema": "http://json-schema.org/draft-04/schema#",
5688 "exclusiveMinimum": 5
5689 });
5690 assert!(!crate::meta::is_valid(&invalid_draft4));
5691 assert!(crate::meta::validate(&invalid_draft4).is_err());
5692
5693 // In Draft 6 and later, exclusiveMinimum is a numeric value
5694 let drafts = [
5695 "http://json-schema.org/draft-06/schema#",
5696 "http://json-schema.org/draft-07/schema#",
5697 "https://json-schema.org/draft/2019-09/schema",
5698 "https://json-schema.org/draft/2020-12/schema",
5699 ];
5700
5701 for uri in drafts {
5702 // Valid in Draft 6+ (numeric exclusiveMinimum)
5703 let valid_schema = json!({
5704 "$schema": uri,
5705 "exclusiveMinimum": 5
5706 });
5707 assert!(
5708 crate::meta::is_valid(&valid_schema),
5709 "Schema should be valid for {uri}"
5710 );
5711 assert!(
5712 crate::meta::validate(&valid_schema).is_ok(),
5713 "Schema validation should succeed for {uri}",
5714 );
5715
5716 // Invalid in Draft 6+ (can't use boolean with minimum)
5717 let invalid_schema = json!({
5718 "$schema": uri,
5719 "minimum": 5,
5720 "exclusiveMinimum": true
5721 });
5722 assert!(
5723 !crate::meta::is_valid(&invalid_schema),
5724 "Schema should be invalid for {uri}",
5725 );
5726 assert!(
5727 crate::meta::validate(&invalid_schema).is_err(),
5728 "Schema validation should fail for {uri}",
5729 );
5730 }
5731 }
5732
5733 #[test_case(
5734 "http://json-schema.org/draft-04/schema#",
5735 true,
5736 5,
5737 true ; "draft4 valid"
5738 )]
5739 #[test_case(
5740 "http://json-schema.org/draft-04/schema#",
5741 5,
5742 true,
5743 false ; "draft4 invalid"
5744 )]
5745 #[test_case(
5746 "http://json-schema.org/draft-06/schema#",
5747 5,
5748 true,
5749 false ; "draft6 invalid"
5750 )]
5751 #[test_case(
5752 "http://json-schema.org/draft-07/schema#",
5753 5,
5754 true,
5755 false ; "draft7 invalid"
5756 )]
5757 #[test_case(
5758 "https://json-schema.org/draft/2019-09/schema",
5759 5,
5760 true,
5761 false ; "draft2019-09 invalid"
5762 )]
5763 #[test_case(
5764 "https://json-schema.org/draft/2020-12/schema",
5765 5,
5766 true,
5767 false ; "draft2020-12 invalid"
5768 )]
5769 fn test_exclusive_minimum_detection(
5770 schema_uri: &str,
5771 exclusive_minimum: impl Into<serde_json::Value>,
5772 minimum: impl Into<serde_json::Value>,
5773 expected: bool,
5774 ) {
5775 let schema = json!({
5776 "$schema": schema_uri,
5777 "minimum": minimum.into(),
5778 "exclusiveMinimum": exclusive_minimum.into()
5779 });
5780
5781 let is_valid_result = crate::meta::is_valid(&schema);
5782 assert_eq!(is_valid_result, expected);
5783
5784 let validate_result = crate::meta::validate(&schema);
5785 assert_eq!(validate_result.is_ok(), expected);
5786 }
5787
5788 #[test]
5789 fn test_invalid_schema_uri() {
5790 let schema = json!({
5791 "$schema": "invalid-uri",
5792 "type": "string"
5793 });
5794
5795 let result = crate::options().without_schema_validation().build(&schema);
5796
5797 assert!(result.is_err());
5798 let error = result.unwrap_err();
5799 assert!(error.to_string().contains("Unknown meta-schema"));
5800 assert!(error.to_string().contains("invalid-uri"));
5801 }
5802
5803 #[test]
5804 fn test_invalid_schema_keyword() {
5805 let schema = json!({
5806 // Note `htt`, not `http`
5807 "$schema": "htt://json-schema.org/draft-07/schema",
5808 "type": "string"
5809 });
5810
5811 // Without registering the meta-schema, this should fail
5812 let result = crate::options().without_schema_validation().build(&schema);
5813
5814 assert!(result.is_err());
5815 let error = result.unwrap_err();
5816 assert!(error.to_string().contains("Unknown meta-schema"));
5817 assert!(error
5818 .to_string()
5819 .contains("htt://json-schema.org/draft-07/schema"));
5820 }
5821
5822 #[test_case(Draft::Draft4)]
5823 #[test_case(Draft::Draft6)]
5824 #[test_case(Draft::Draft7)]
5825 fn meta_schemas(draft: Draft) {
5826 // See GH-258
5827 for schema in [json!({"enum": [0, 0.0]}), json!({"enum": []})] {
5828 assert!(crate::options().with_draft(draft).build(&schema).is_ok());
5829 }
5830 }
5831
5832 #[test]
5833 fn incomplete_escape_in_pattern() {
5834 // See GH-253
5835 let schema = json!({"pattern": "\\u"});
5836 assert!(crate::validator_for(&schema).is_err());
5837 }
5838
5839 #[test]
5840 fn validation_error_propagation() {
5841 fn foo() -> Result<(), Box<dyn std::error::Error>> {
5842 let schema = json!({});
5843 let validator = validator_for(&schema)?;
5844 let _ = validator.is_valid(&json!({}));
5845 Ok(())
5846 }
5847 let _ = foo();
5848 }
5849
5850 #[test]
5851 fn test_meta_validation_with_unknown_schema() {
5852 let schema = json!({
5853 "$schema": "json-schema:///custom",
5854 "type": "string"
5855 });
5856
5857 // Meta-validation now errors when the meta-schema is unknown/unregistered
5858 assert!(crate::meta::validate(&schema).is_err());
5859
5860 // Building a validator also fails without registration
5861 let result = crate::validator_for(&schema);
5862 assert!(result.is_err());
5863 }
5864
5865 #[test]
5866 #[cfg(all(not(target_arch = "wasm32"), feature = "resolve-file"))]
5867 fn test_meta_validation_respects_metaschema_draft() {
5868 use std::io::Write;
5869
5870 let mut temp_file = tempfile::NamedTempFile::new().expect("Failed to create temp file");
5871 let meta_schema_draft7 = json!({
5872 "$id": "http://example.com/meta/draft7",
5873 "$schema": "http://json-schema.org/draft-07/schema",
5874 "type": ["object", "boolean"],
5875 "properties": {
5876 "$schema": { "type": "string" },
5877 "type": {},
5878 "properties": { "type": "object" }
5879 },
5880 "additionalProperties": false
5881 });
5882 write!(temp_file, "{meta_schema_draft7}").expect("Failed to write to temp file");
5883
5884 let uri = crate::retriever::path_to_uri(temp_file.path());
5885
5886 let schema_using_draft7_meta = json!({
5887 "$schema": uri,
5888 "type": "object",
5889 "properties": {
5890 "name": { "type": "string" }
5891 },
5892 "unevaluatedProperties": false
5893 });
5894
5895 let schema_valid_for_draft7_meta = json!({
5896 "$schema": uri,
5897 "type": "object",
5898 "properties": {
5899 "name": { "type": "string" }
5900 }
5901 });
5902
5903 assert!(crate::meta::is_valid(&meta_schema_draft7));
5904 assert!(!crate::meta::is_valid(&schema_using_draft7_meta));
5905 assert!(crate::meta::is_valid(&schema_valid_for_draft7_meta));
5906 }
5907
5908 #[test]
5909 #[cfg(all(not(target_arch = "wasm32"), feature = "resolve-file"))]
5910 fn test_meta_schema_chain_resolution() {
5911 use std::io::Write;
5912
5913 // Create intermediate meta-schema pointing to Draft 2020-12
5914 let mut intermediate_file =
5915 tempfile::NamedTempFile::new().expect("Failed to create temp file");
5916 let intermediate_meta = json!({
5917 "$id": "http://example.com/meta/intermediate",
5918 "$schema": "https://json-schema.org/draft/2020-12/schema",
5919 "type": "object"
5920 });
5921 write!(intermediate_file, "{intermediate_meta}").expect("Failed to write to temp file");
5922 let intermediate_uri = crate::retriever::path_to_uri(intermediate_file.path());
5923
5924 // Create custom meta-schema with unknown draft that points to intermediate
5925 // This triggers the chain resolution code path in resolve_meta_schema_chain
5926 let mut custom_file = tempfile::NamedTempFile::new().expect("Failed to create temp file");
5927 let custom_meta = json!({
5928 "$id": "http://example.com/meta/custom",
5929 "$schema": intermediate_uri,
5930 "type": "object"
5931 });
5932 write!(custom_file, "{custom_meta}").expect("Failed to write to temp file");
5933 let custom_uri = crate::retriever::path_to_uri(custom_file.path());
5934
5935 let schema = json!({
5936 "$schema": custom_uri,
5937 "type": "string"
5938 });
5939
5940 // Should successfully resolve through the chain and detect Draft 2020-12
5941 assert!(crate::meta::is_valid(&schema));
5942 }
5943
5944 #[test]
5945 #[cfg(all(not(target_arch = "wasm32"), feature = "resolve-file"))]
5946 fn test_circular_meta_schema_reference() {
5947 use std::io::Write;
5948
5949 // Create meta-schema A pointing to meta-schema B
5950 let mut meta_a_file = tempfile::NamedTempFile::new().expect("Failed to create temp file");
5951 let meta_a_uri = crate::retriever::path_to_uri(meta_a_file.path());
5952
5953 // Create meta-schema B pointing back to meta-schema A
5954 let mut meta_b_file = tempfile::NamedTempFile::new().expect("Failed to create temp file");
5955 let meta_b_uri = crate::retriever::path_to_uri(meta_b_file.path());
5956
5957 let meta_a = json!({
5958 "$id": "http://example.com/meta/a",
5959 "$schema": &meta_b_uri,
5960 "type": "object"
5961 });
5962 write!(meta_a_file, "{meta_a}").expect("Failed to write to temp file");
5963
5964 let meta_b = json!({
5965 "$id": "http://example.com/meta/b",
5966 "$schema": &meta_a_uri,
5967 "type": "object"
5968 });
5969 write!(meta_b_file, "{meta_b}").expect("Failed to write to temp file");
5970
5971 let schema = json!({
5972 "$schema": meta_a_uri.clone(),
5973 "type": "string"
5974 });
5975
5976 // Should return a circular meta-schema error
5977 let result = crate::meta::options().validate(&schema);
5978 assert!(result.is_err());
5979 assert!(result
5980 .unwrap_err()
5981 .to_string()
5982 .contains("Circular meta-schema reference"));
5983 }
5984
5985 #[test]
5986 fn simple_schema_with_unknown_draft() {
5987 // Define a custom meta-schema
5988 let meta_schema = json!({
5989 "$schema": "https://json-schema.org/draft/2020-12/schema",
5990 "$id": "http://custom.example.com/schema",
5991 "$vocabulary": {
5992 "https://json-schema.org/draft/2020-12/vocab/core": true,
5993 "https://json-schema.org/draft/2020-12/vocab/applicator": true,
5994 "https://json-schema.org/draft/2020-12/vocab/validation": true,
5995 }
5996 });
5997
5998 // Schema using the custom meta-schema
5999 let schema = json!({
6000 "$schema": "http://custom.example.com/schema",
6001 "type": "object",
6002 "properties": {
6003 "name": { "type": "string" }
6004 }
6005 });
6006
6007 let registry = Registry::new()
6008 .add("http://custom.example.com/schema", meta_schema)
6009 .expect("Should accept meta-schema")
6010 .prepare()
6011 .expect("Should create registry");
6012 let validator = crate::options()
6013 .without_schema_validation()
6014 .with_registry(®istry)
6015 .build(&schema)
6016 .expect("Should build validator");
6017
6018 // Valid instance
6019 assert!(validator.is_valid(&json!({"name": "test"})));
6020
6021 // Invalid instance - name should be string, not number
6022 assert!(!validator.is_valid(&json!({"name": 123})));
6023
6024 // Also verify type validation works
6025 assert!(!validator.is_valid(&json!("not an object")));
6026 }
6027
6028 #[test]
6029 fn custom_meta_schema_support() {
6030 // Define a custom meta-schema that extends Draft 2020-12
6031 let meta_schema = json!({
6032 "$id": "http://example.com/meta/schema",
6033 "$schema": "https://json-schema.org/draft/2020-12/schema",
6034 "title": "Core schema definition",
6035 "type": "object",
6036 "allOf": [
6037 {
6038 "$ref": "#/$defs/editable"
6039 },
6040 {
6041 "$ref": "#/$defs/core"
6042 }
6043 ],
6044 "properties": {
6045 "properties": {
6046 "type": "object",
6047 "patternProperties": {
6048 ".*": {
6049 "type": "object",
6050 "properties": {
6051 "type": {
6052 "type": "string",
6053 "enum": [
6054 "array",
6055 "boolean",
6056 "integer",
6057 "number",
6058 "object",
6059 "string",
6060 "null"
6061 ]
6062 }
6063 }
6064 }
6065 },
6066 "propertyNames": {
6067 "type": "string",
6068 "pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
6069 }
6070 }
6071 },
6072 "unevaluatedProperties": false,
6073 "required": [
6074 "properties"
6075 ],
6076 "$defs": {
6077 "core": {
6078 "type": "object",
6079 "properties": {
6080 "$id": {
6081 "type": "string"
6082 },
6083 "$schema": {
6084 "type": "string"
6085 },
6086 "type": {
6087 "const": "object"
6088 },
6089 "title": {
6090 "type": "string"
6091 },
6092 "description": {
6093 "type": "string"
6094 },
6095 "additionalProperties": {
6096 "type": "boolean",
6097 "const": false
6098 }
6099 },
6100 "required": [
6101 "$id",
6102 "$schema",
6103 "type"
6104 ]
6105 },
6106 "editable": {
6107 "type": "object",
6108 "properties": {
6109 "creationDate": {
6110 "type": "string",
6111 "format": "date-time"
6112 },
6113 "updateDate": {
6114 "type": "string",
6115 "format": "date-time"
6116 }
6117 },
6118 "required": [
6119 "creationDate"
6120 ]
6121 }
6122 }
6123 });
6124
6125 // A schema that uses the custom meta-schema
6126 let element_schema = json!({
6127 "$schema": "http://example.com/meta/schema",
6128 "$id": "http://example.com/schemas/element",
6129 "title": "Element",
6130 "description": "An element",
6131 "creationDate": "2024-12-31T12:31:53+01:00",
6132 "properties": {
6133 "value": {
6134 "type": "string"
6135 }
6136 },
6137 "type": "object"
6138 });
6139
6140 let registry = Registry::new()
6141 .add("http://example.com/meta/schema", meta_schema)
6142 .expect("Should accept meta-schema")
6143 .prepare()
6144 .expect("Should create registry");
6145 let validator = crate::options()
6146 .without_schema_validation()
6147 .with_registry(®istry)
6148 .build(&element_schema)
6149 .expect("Should successfully build validator with custom meta-schema");
6150
6151 let valid_instance = json!({
6152 "value": "test string"
6153 });
6154 assert!(validator.is_valid(&valid_instance));
6155
6156 let invalid_instance = json!({
6157 "value": 123
6158 });
6159 assert!(!validator.is_valid(&invalid_instance));
6160 }
6161
6162 #[test]
6163 fn custom_meta_schema_with_fragment_finds_vocabularies() {
6164 // Custom meta-schema URIs with trailing # should be found in registry
6165 let custom_meta = json!({
6166 "$id": "http://example.com/custom-with-unevaluated",
6167 "$schema": "https://json-schema.org/draft/2020-12/schema",
6168 "$vocabulary": {
6169 "https://json-schema.org/draft/2020-12/vocab/core": true,
6170 "https://json-schema.org/draft/2020-12/vocab/applicator": true,
6171 "https://json-schema.org/draft/2020-12/vocab/validation": true,
6172 "https://json-schema.org/draft/2020-12/vocab/unevaluated": true
6173 }
6174 });
6175
6176 let registry = Registry::new()
6177 .add("http://example.com/custom-with-unevaluated", custom_meta)
6178 .expect("Should accept meta-schema")
6179 .prepare()
6180 .expect("Should create registry");
6181
6182 let schema = json!({
6183 "$id": "http://example.com/subject",
6184 "$schema": "http://example.com/custom-with-unevaluated#",
6185 "type": "object",
6186 "properties": {
6187 "foo": { "type": "string" }
6188 },
6189 "unevaluatedProperties": false
6190 });
6191
6192 let validator = crate::options()
6193 .without_schema_validation()
6194 .with_registry(®istry)
6195 .build(&schema)
6196 .expect("Should build validator");
6197
6198 assert!(validator.is_valid(&json!({"foo": "bar"})));
6199 assert!(!validator.is_valid(&json!({"foo": "bar", "extra": "value"})));
6200 }
6201
6202 fn dialect_registry(vocabularies: &Value) -> Registry<'static> {
6203 Registry::new()
6204 .add(
6205 "https://example.com/dialect",
6206 json!({
6207 "$schema": "https://json-schema.org/draft/2020-12/schema",
6208 "$id": "https://example.com/dialect",
6209 "$vocabulary": vocabularies,
6210 }),
6211 )
6212 .expect("Should accept meta-schema")
6213 .prepare()
6214 .expect("Should create registry")
6215 }
6216
6217 fn format_assertion_dialect(required: bool) -> Value {
6218 json!({
6219 "https://json-schema.org/draft/2020-12/vocab/core": true,
6220 "https://json-schema.org/draft/2020-12/vocab/validation": true,
6221 "https://json-schema.org/draft/2020-12/vocab/format-assertion": required,
6222 })
6223 }
6224
6225 // The boolean only steers implementations that do not know the vocabulary.
6226 #[test_case(true)]
6227 #[test_case(false)]
6228 fn format_assertion_vocabulary_asserts_without_opt_in(required: bool) {
6229 let registry = dialect_registry(&format_assertion_dialect(required));
6230 let validator = crate::options()
6231 .with_registry(®istry)
6232 .build(&json!({
6233 "$schema": "https://example.com/dialect",
6234 "type": "string",
6235 "format": "date-time"
6236 }))
6237 .expect("Should build validator");
6238
6239 assert!(validator.is_valid(&json!("2026-08-22T12:00:00Z")));
6240 assert!(!validator.is_valid(&json!("not-a-date")));
6241 }
6242
6243 #[test_case(false)]
6244 #[test_case(true)]
6245 fn format_assertion_vocabulary_rejects_unknown_format(ignore_unknown_formats: bool) {
6246 let registry = dialect_registry(&format_assertion_dialect(true));
6247 let error = crate::options()
6248 .with_registry(®istry)
6249 .should_ignore_unknown_formats(ignore_unknown_formats)
6250 .build(&json!({
6251 "$schema": "https://example.com/dialect",
6252 "format": "totally-made-up"
6253 }))
6254 .expect_err("Should reject unknown format");
6255
6256 assert_eq!(
6257 error.to_string(),
6258 "Unknown format: 'totally-made-up'. The meta-schema asserts formats, so unrecognized ones cannot be ignored. Register a check for it or disable format validation"
6259 );
6260 }
6261
6262 #[test]
6263 fn format_assertion_vocabulary_honors_explicit_opt_out() {
6264 let registry = dialect_registry(&format_assertion_dialect(true));
6265 let validator = crate::options()
6266 .with_registry(®istry)
6267 .should_validate_formats(false)
6268 .build(&json!({
6269 "$schema": "https://example.com/dialect",
6270 "format": "date-time"
6271 }))
6272 .expect("Should build validator");
6273
6274 assert!(validator.is_valid(&json!("not-a-date")));
6275 }
6276
6277 #[test_case(true, false ; "required")]
6278 #[test_case(false, true ; "optional")]
6279 fn draft201909_format_vocabulary_asserts_when_required(required: bool, expected: bool) {
6280 let registry = Registry::new()
6281 .add(
6282 "https://example.com/dialect",
6283 json!({
6284 "$schema": "https://json-schema.org/draft/2019-09/schema",
6285 "$id": "https://example.com/dialect",
6286 "$vocabulary": {
6287 "https://json-schema.org/draft/2019-09/vocab/core": true,
6288 "https://json-schema.org/draft/2019-09/vocab/validation": true,
6289 "https://json-schema.org/draft/2019-09/vocab/format": required,
6290 },
6291 }),
6292 )
6293 .expect("Should accept meta-schema")
6294 .prepare()
6295 .expect("Should create registry");
6296 let validator = crate::options()
6297 .with_registry(®istry)
6298 .build(&json!({
6299 "$schema": "https://example.com/dialect",
6300 "format": "ipv4"
6301 }))
6302 .expect("Should build validator");
6303
6304 assert_eq!(validator.is_valid(&json!("not-an-ipv4")), expected);
6305 }
6306
6307 #[test]
6308 fn dialect_without_id_declares_vocabularies() {
6309 let registry = Registry::new()
6310 .add(
6311 "https://example.com/dialect",
6312 json!({
6313 "$schema": "https://json-schema.org/draft/2020-12/schema",
6314 "$vocabulary": format_assertion_dialect(true),
6315 }),
6316 )
6317 .expect("Should accept meta-schema")
6318 .prepare()
6319 .expect("Should create registry");
6320 let validator = crate::options()
6321 .with_registry(®istry)
6322 .build(&json!({
6323 "$schema": "https://example.com/dialect",
6324 "format": "ipv4"
6325 }))
6326 .expect("Should build validator");
6327
6328 assert!(!validator.is_valid(&json!("not-an-ipv4")));
6329 }
6330
6331 #[test]
6332 fn nested_dialect_declares_vocabularies() {
6333 let registry = dialect_registry(&format_assertion_dialect(true));
6334 let validator = crate::options()
6335 .with_draft(Draft::Draft4)
6336 .with_registry(®istry)
6337 .build(&json!({
6338 "properties": {
6339 "address": {"$schema": "https://example.com/dialect", "format": "ipv4"}
6340 }
6341 }))
6342 .expect("Should build validator");
6343
6344 assert!(!validator.is_valid(&json!({"address": "not-an-ipv4"})));
6345 }
6346
6347 #[test_case("https://json-schema.org/draft/2020-12/meta/format-assertion" ; "2020-12 format-assertion")]
6348 #[test_case("https://json-schema.org/draft/2019-09/meta/format" ; "2019-09 format")]
6349 fn bundled_format_meta_schema_asserts(meta_schema: &str) {
6350 let validator = crate::options()
6351 .build(&json!({
6352 "$schema": meta_schema,
6353 "format": "ipv4"
6354 }))
6355 .expect("Should build validator");
6356
6357 assert!(validator.is_valid(&json!("127.0.0.1")));
6358 assert!(!validator.is_valid(&json!("not-an-ipv4")));
6359 }
6360
6361 #[test]
6362 fn format_annotation_vocabulary_does_not_assert() {
6363 let registry = dialect_registry(&json!({
6364 "https://json-schema.org/draft/2020-12/vocab/core": true,
6365 "https://json-schema.org/draft/2020-12/vocab/format-annotation": true,
6366 }));
6367 let validator = crate::options()
6368 .with_registry(®istry)
6369 .build(&json!({
6370 "$schema": "https://example.com/dialect",
6371 "format": "date-time"
6372 }))
6373 .expect("Should build validator");
6374
6375 assert!(validator.is_valid(&json!("not-a-date")));
6376 }
6377
6378 const UNKNOWN_VOCABULARY_ERROR: &str = "Unknown vocabulary: 'https://example.com/vocab/made-up' is required by the meta-schema. Adjust configuration to declare support for it";
6379
6380 #[test_case("https://example.com/vocab/made-up", true, false, Some(UNKNOWN_VOCABULARY_ERROR) ; "required undeclared")]
6381 #[test_case("https://example.com/vocab/made-up", false, false, None ; "optional undeclared")]
6382 #[test_case("https://example.com/vocab/made-up", true, true, None ; "required declared")]
6383 #[test_case("https://Example.COM/vocab/made-up", true, true, None ; "declared uppercase host")]
6384 #[test_case("https://example.com/vocab/./made-up", true, true, None ; "declared dot segment")]
6385 #[test_case("https://example.com/vocab/made%2Dup", true, true, None ; "declared percent-encoded unreserved")]
6386 fn unknown_vocabulary(uri: &str, required: bool, declared: bool, expected: Option<&str>) {
6387 let registry = dialect_registry(&json!({
6388 "https://json-schema.org/draft/2020-12/vocab/core": true,
6389 uri: required,
6390 }));
6391 let mut options = crate::options().with_registry(®istry);
6392 if declared {
6393 options = options.with_vocabulary(uri);
6394 }
6395
6396 // `Clone` keeps the declarations.
6397 let error = options
6398 .clone()
6399 .build(&json!({"$schema": "https://example.com/dialect"}))
6400 .err()
6401 .map(|error| error.to_string());
6402
6403 assert_eq!(error.as_deref(), expected);
6404 }
6405
6406 #[test_case("$ref", "https://example.com/inner" ; "ref to resource")]
6407 #[test_case("$ref", "https://example.com/inner#/$defs/leaf" ; "ref into resource")]
6408 #[test_case("$dynamicRef", "https://example.com/inner" ; "dynamic ref to resource")]
6409 fn required_unknown_vocabulary_is_rejected_behind_reference(keyword: &str, target: &str) {
6410 let registry = Registry::new()
6411 .add(
6412 "https://example.com/dialect",
6413 json!({
6414 "$schema": "https://json-schema.org/draft/2020-12/schema",
6415 "$id": "https://example.com/dialect",
6416 "$vocabulary": {
6417 "https://json-schema.org/draft/2020-12/vocab/core": true,
6418 "https://example.com/vocab/made-up": true,
6419 },
6420 }),
6421 )
6422 .expect("Should accept meta-schema")
6423 .add(
6424 "https://example.com/inner",
6425 json!({
6426 "$schema": "https://example.com/dialect",
6427 "$id": "https://example.com/inner",
6428 "type": "string",
6429 "$defs": {"leaf": {"type": "string"}},
6430 }),
6431 )
6432 .expect("Should accept inner schema")
6433 .prepare()
6434 .expect("Should create registry");
6435 let error = crate::options()
6436 .with_registry(®istry)
6437 .build(&json!({keyword: target}))
6438 .expect_err("Should reject required unknown vocabulary");
6439
6440 assert_eq!(error.to_string(), UNKNOWN_VOCABULARY_ERROR);
6441 }
6442
6443 #[test]
6444 fn strict_meta_schema_catches_typos() {
6445 // Issue #764: Use strict meta-schema with unevaluatedProperties: false
6446 // to catch typos in schema keywords
6447
6448 let strict_meta = json!({
6449 "$schema": "https://json-schema.org/draft/2020-12/schema",
6450 "$id": "https://json-schema.org/draft/2020-12/strict",
6451 "$dynamicAnchor": "meta",
6452 "$ref": "https://json-schema.org/draft/2020-12/schema",
6453 "unevaluatedProperties": false
6454 });
6455
6456 let registry = Registry::new()
6457 .add("https://json-schema.org/draft/2020-12/strict", strict_meta)
6458 .expect("Should accept strict meta-schema")
6459 .prepare()
6460 .expect("Should create registry");
6461
6462 // Valid schema - all keywords are recognized
6463 let valid_schema = json!({
6464 "$schema": "https://json-schema.org/draft/2020-12/strict",
6465 "type": "object",
6466 "properties": {
6467 "name": {"type": "string", "minLength": 1}
6468 }
6469 });
6470
6471 assert!(crate::meta::options()
6472 .with_registry(®istry)
6473 .is_valid(&valid_schema));
6474
6475 // Invalid schema - top-level typo "typ" instead of "type"
6476 let invalid_schema_top_level = json!({
6477 "$schema": "https://json-schema.org/draft/2020-12/strict",
6478 "typ": "string" // Typo
6479 });
6480
6481 assert!(!crate::meta::options()
6482 .with_registry(®istry)
6483 .is_valid(&invalid_schema_top_level));
6484
6485 // Invalid schema - nested invalid keyword "minSize" (not a real JSON Schema keyword)
6486 let invalid_schema_nested = json!({
6487 "$schema": "https://json-schema.org/draft/2020-12/strict",
6488 "type": "object",
6489 "properties": {
6490 "name": {"type": "string", "minSize": 1} // Invalid keyword in nested schema
6491 }
6492 });
6493
6494 assert!(!crate::meta::options()
6495 .with_registry(®istry)
6496 .is_valid(&invalid_schema_nested));
6497 }
6498
6499 #[test]
6500 fn custom_meta_schema_preserves_underlying_draft_behavior() {
6501 // Regression test: Custom meta-schemas should preserve the draft-specific
6502 // behavior of their underlying draft, not default to Draft 2020-12
6503 // Draft 7 specific behavior: $ref siblings are ignored
6504
6505 let custom_meta_draft7 = json!({
6506 "$id": "http://example.com/meta/draft7-custom",
6507 "$schema": "http://json-schema.org/draft-07/schema#",
6508 "type": "object",
6509 "properties": {
6510 "customKeyword": {"type": "string"}
6511 }
6512 });
6513
6514 let registry = Registry::new()
6515 .add("http://example.com/meta/draft7-custom", custom_meta_draft7)
6516 .expect("Should accept meta-schema")
6517 .prepare()
6518 .expect("Should create registry");
6519
6520 let schema = json!({
6521 "$id": "http://example.com/subject",
6522 "$schema": "http://example.com/meta/draft7-custom",
6523 "$ref": "#/$defs/positiveNumber",
6524 "maximum": 5,
6525 "$defs": {
6526 "positiveNumber": {
6527 "type": "number",
6528 "minimum": 0
6529 }
6530 }
6531 });
6532
6533 let validator = crate::options()
6534 .without_schema_validation()
6535 .with_registry(®istry)
6536 .build(&schema)
6537 .expect("Should build validator");
6538
6539 // In Draft 7: siblings of $ref are ignored, so maximum: 5 has no effect
6540 // In Draft 2020-12: siblings are evaluated, so maximum: 5 would apply
6541 assert!(validator.is_valid(&json!(10)));
6542 }
6543
6544 mod meta_options_tests {
6545 use super::*;
6546 use crate::Registry;
6547
6548 #[test]
6549 fn test_meta_options_with_registry_valid_schema() {
6550 let custom_meta = json!({
6551 "$schema": "https://json-schema.org/draft/2020-12/schema",
6552 "type": "object",
6553 "properties": {
6554 "$schema": { "type": "string" },
6555 "type": { "type": "string" },
6556 "maxLength": { "type": "integer" }
6557 },
6558 "additionalProperties": false
6559 });
6560
6561 let registry = Registry::new()
6562 .add("http://example.com/meta", custom_meta)
6563 .unwrap()
6564 .prepare()
6565 .unwrap();
6566
6567 let schema = json!({
6568 "$schema": "http://example.com/meta",
6569 "type": "string",
6570 "maxLength": 10
6571 });
6572
6573 assert!(crate::meta::options()
6574 .with_registry(®istry)
6575 .is_valid(&schema));
6576
6577 assert!(crate::meta::options()
6578 .with_registry(®istry)
6579 .validate(&schema)
6580 .is_ok());
6581 }
6582
6583 #[test]
6584 fn test_meta_options_with_registry_invalid_schema() {
6585 let custom_meta = json!({
6586 "$schema": "https://json-schema.org/draft/2020-12/schema",
6587 "type": "object",
6588 "properties": {
6589 "type": { "type": "string" }
6590 },
6591 "additionalProperties": false
6592 });
6593
6594 let registry = Registry::new()
6595 .add("http://example.com/meta", custom_meta)
6596 .unwrap()
6597 .prepare()
6598 .unwrap();
6599
6600 // Schema has disallowed property
6601 let schema = json!({
6602 "$schema": "http://example.com/meta",
6603 "type": "string",
6604 "maxLength": 10 // Not allowed by custom meta-schema
6605 });
6606
6607 assert!(!crate::meta::options()
6608 .with_registry(®istry)
6609 .is_valid(&schema));
6610
6611 assert!(crate::meta::options()
6612 .with_registry(®istry)
6613 .validate(&schema)
6614 .is_err());
6615 }
6616
6617 #[test]
6618 fn test_meta_options_with_registry_chain() {
6619 // Create a chain: custom-meta -> draft2020-12
6620 let custom_meta = json!({
6621 "$schema": "https://json-schema.org/draft/2020-12/schema",
6622 "type": "object"
6623 });
6624
6625 let registry = Registry::new()
6626 .add("http://example.com/custom", custom_meta)
6627 .unwrap()
6628 .prepare()
6629 .unwrap();
6630
6631 let schema = json!({
6632 "$schema": "http://example.com/custom",
6633 "type": "string"
6634 });
6635
6636 assert!(crate::meta::options()
6637 .with_registry(®istry)
6638 .is_valid(&schema));
6639 }
6640
6641 #[test]
6642 fn test_meta_options_with_registry_multi_level_chain() {
6643 // Create chain: schema -> meta-level-2 -> meta-level-1 -> draft2020-12
6644 let meta_level_1 = json!({
6645 "$id": "http://example.com/meta/level1",
6646 "$schema": "https://json-schema.org/draft/2020-12/schema",
6647 "type": "object",
6648 "properties": {
6649 "customProp": { "type": "boolean" }
6650 }
6651 });
6652
6653 let meta_level_2 = json!({
6654 "$id": "http://example.com/meta/level2",
6655 "$schema": "http://example.com/meta/level1",
6656 "type": "object",
6657 "customProp": true
6658 });
6659
6660 let registry = Registry::new()
6661 .extend([
6662 ("http://example.com/meta/level1", meta_level_1),
6663 ("http://example.com/meta/level2", meta_level_2),
6664 ])
6665 .unwrap()
6666 .prepare()
6667 .unwrap();
6668
6669 let schema = json!({
6670 "$schema": "http://example.com/meta/level2",
6671 "type": "string",
6672 "customProp": true
6673 });
6674
6675 assert!(crate::meta::options()
6676 .with_registry(®istry)
6677 .is_valid(&schema));
6678 }
6679
6680 #[test]
6681 fn test_meta_options_with_registry_multi_document_meta_schema() {
6682 let shared_constraints = json!({
6683 "$id": "http://example.com/meta/shared",
6684 "$schema": "https://json-schema.org/draft/2020-12/schema",
6685 "type": "object",
6686 "properties": {
6687 "maxLength": { "type": "integer", "minimum": 0 }
6688 }
6689 });
6690
6691 let root_meta = json!({
6692 "$id": "http://example.com/meta/root",
6693 "$schema": "https://json-schema.org/draft/2020-12/schema",
6694 "type": "object",
6695 "properties": {
6696 "$schema": { "type": "string" },
6697 "type": { "type": "string" }
6698 },
6699 "allOf": [
6700 { "$ref": "http://example.com/meta/shared" }
6701 ]
6702 });
6703
6704 let registry = Registry::new()
6705 .extend([
6706 ("http://example.com/meta/root", root_meta),
6707 ("http://example.com/meta/shared", shared_constraints),
6708 ])
6709 .unwrap()
6710 .prepare()
6711 .unwrap();
6712
6713 let schema = json!({
6714 "$schema": "http://example.com/meta/root",
6715 "type": "string",
6716 "maxLength": 5
6717 });
6718
6719 let result = crate::meta::options()
6720 .with_registry(®istry)
6721 .validate(&schema);
6722
6723 assert!(
6724 result.is_ok(),
6725 "meta validation failed even though registry contains all meta-schemas: {}",
6726 result.unwrap_err()
6727 );
6728
6729 assert!(crate::meta::options()
6730 .with_registry(®istry)
6731 .is_valid(&schema));
6732 }
6733
6734 #[test]
6735 fn test_meta_options_without_registry_unknown_metaschema() {
6736 let schema = json!({
6737 "$schema": "http://0.0.0.0/nonexistent",
6738 "type": "string"
6739 });
6740
6741 // Without registry, should fail to resolve
6742 let result = crate::meta::options().validate(&schema);
6743 assert!(result.is_err());
6744 }
6745
6746 #[test]
6747 #[should_panic(expected = "Failed to resolve meta-schema")]
6748 fn test_meta_options_is_valid_panics_on_missing_metaschema() {
6749 let schema = json!({
6750 "$schema": "http://0.0.0.0/nonexistent",
6751 "type": "string"
6752 });
6753
6754 // is_valid() should panic if meta-schema cannot be resolved
6755 let _ = crate::meta::options().is_valid(&schema);
6756 }
6757
6758 #[test]
6759 fn test_meta_options_with_registry_missing_metaschema() {
6760 let custom_meta = json!({
6761 "$schema": "https://json-schema.org/draft/2020-12/schema",
6762 "type": "object"
6763 });
6764
6765 let registry = Registry::new()
6766 .add("http://example.com/meta1", custom_meta)
6767 .unwrap()
6768 .prepare()
6769 .unwrap();
6770
6771 // Schema references a different meta-schema not in registry
6772 let schema = json!({
6773 "$schema": "http://example.com/meta2",
6774 "type": "string"
6775 });
6776
6777 let result = crate::meta::options()
6778 .with_registry(®istry)
6779 .validate(&schema);
6780
6781 assert!(result.is_err());
6782 }
6783
6784 #[test]
6785 fn test_meta_options_circular_reference_detection() {
6786 // Create a circular reference: meta1 -> meta2 -> meta1
6787 let meta1 = json!({
6788 "$id": "http://example.com/meta1",
6789 "$schema": "http://example.com/meta2",
6790 "type": "object"
6791 });
6792
6793 let meta2 = json!({
6794 "$id": "http://example.com/meta2",
6795 "$schema": "http://example.com/meta1",
6796 "type": "object"
6797 });
6798
6799 let registry = Registry::new()
6800 .extend([
6801 ("http://example.com/meta1", meta1),
6802 ("http://example.com/meta2", meta2),
6803 ])
6804 .unwrap()
6805 .prepare()
6806 .unwrap();
6807
6808 let schema = json!({
6809 "$schema": "http://example.com/meta1",
6810 "type": "string"
6811 });
6812
6813 let result = crate::meta::options()
6814 .with_registry(®istry)
6815 .validate(&schema);
6816
6817 assert!(result.is_err());
6818 // Check it's specifically a circular error
6819 let err = result.unwrap_err();
6820 assert!(err.to_string().contains("Circular"));
6821 }
6822
6823 #[test]
6824 fn test_meta_options_standard_drafts_without_registry() {
6825 // Standard drafts should work without registry
6826 let schemas = vec![
6827 json!({ "$schema": "http://json-schema.org/draft-04/schema#", "type": "string" }),
6828 json!({ "$schema": "http://json-schema.org/draft-06/schema#", "type": "string" }),
6829 json!({ "$schema": "http://json-schema.org/draft-07/schema#", "type": "string" }),
6830 json!({ "$schema": "https://json-schema.org/draft/2019-09/schema", "type": "string" }),
6831 json!({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "string" }),
6832 ];
6833
6834 for schema in schemas {
6835 assert!(
6836 crate::meta::options().is_valid(&schema),
6837 "Failed for schema: {schema}"
6838 );
6839 }
6840 }
6841
6842 #[test]
6843 fn test_meta_options_validate_returns_specific_errors() {
6844 let custom_meta = json!({
6845 "$schema": "https://json-schema.org/draft/2020-12/schema",
6846 "type": "object",
6847 "required": ["type"]
6848 });
6849
6850 let registry = Registry::new()
6851 .add("http://example.com/meta", custom_meta)
6852 .unwrap()
6853 .prepare()
6854 .unwrap();
6855
6856 // Schema missing required property
6857 let schema = json!({
6858 "$schema": "http://example.com/meta",
6859 "properties": {
6860 "name": { "type": "string" }
6861 }
6862 });
6863
6864 let result = crate::meta::options()
6865 .with_registry(®istry)
6866 .validate(&schema);
6867
6868 assert!(result.is_err());
6869 let err = result.unwrap_err();
6870 assert!(err.to_string().contains("required") || err.to_string().contains("type"));
6871 }
6872
6873 #[test]
6874 fn test_meta_options_builds_validator_with_resolved_draft() {
6875 let custom_meta = json!({
6876 "$id": "http://example.com/meta/draft7-based",
6877 "$schema": "http://json-schema.org/draft-07/schema#",
6878 "type": "object",
6879 "properties": {
6880 "$schema": { "type": "string" },
6881 "type": { "type": "string" },
6882 "minLength": { "type": "integer" }
6883 },
6884 "additionalProperties": false
6885 });
6886
6887 let registry = Registry::new()
6888 .add("http://example.com/meta/draft7-based", custom_meta)
6889 .unwrap()
6890 .prepare()
6891 .unwrap();
6892
6893 let schema = json!({
6894 "$schema": "http://example.com/meta/draft7-based",
6895 "type": "string",
6896 "minLength": 5
6897 });
6898
6899 let result = crate::meta::options()
6900 .with_registry(®istry)
6901 .validate(&schema);
6902
6903 assert!(result.is_ok());
6904 }
6905
6906 #[test]
6907 fn test_meta_options_validator_uses_correct_draft() {
6908 let custom_meta_draft6 = json!({
6909 "$id": "http://example.com/meta/draft6-based",
6910 "$schema": "http://json-schema.org/draft-06/schema#",
6911 "type": "object",
6912 "properties": {
6913 "$schema": { "type": "string" },
6914 "type": { "type": "string" },
6915 "exclusiveMinimum": { "type": "number" }
6916 },
6917 "additionalProperties": false
6918 });
6919
6920 let registry = Registry::new()
6921 .add("http://example.com/meta/draft6-based", custom_meta_draft6)
6922 .unwrap()
6923 .prepare()
6924 .unwrap();
6925
6926 let schema_valid_for_draft6 = json!({
6927 "$schema": "http://example.com/meta/draft6-based",
6928 "type": "number",
6929 "exclusiveMinimum": 0
6930 });
6931
6932 let result = crate::meta::options()
6933 .with_registry(®istry)
6934 .validate(&schema_valid_for_draft6);
6935
6936 assert!(result.is_ok());
6937 }
6938
6939 #[test]
6940 fn test_meta_options_without_schema_validation_in_built_validator() {
6941 let custom_meta = json!({
6942 "$id": "http://example.com/meta/custom",
6943 "$schema": "https://json-schema.org/draft/2020-12/schema",
6944 "type": "object",
6945 "properties": {
6946 "$schema": { "type": "string" },
6947 "type": { "type": "string" }
6948 },
6949 "additionalProperties": false
6950 });
6951
6952 let registry = Registry::new()
6953 .add("http://example.com/meta/custom", custom_meta)
6954 .unwrap()
6955 .prepare()
6956 .unwrap();
6957
6958 let schema = json!({
6959 "$schema": "http://example.com/meta/custom",
6960 "type": "string"
6961 });
6962
6963 let result = crate::meta::options()
6964 .with_registry(®istry)
6965 .validate(&schema);
6966
6967 assert!(result.is_ok());
6968 }
6969
6970 #[test]
6971 fn test_meta_validation_uses_resolved_draft_from_chain() {
6972 // Chain: user-schema -> custom-meta -> Draft 4
6973 // Validator should use Draft 4 rules to validate the schema
6974 let custom_meta = json!({
6975 "$id": "http://example.com/meta/draft4-based",
6976 "$schema": "http://json-schema.org/draft-04/schema#",
6977 "type": "object",
6978 "properties": {
6979 "$schema": { "type": "string" },
6980 "type": { "type": "string" },
6981 "enum": { "type": "array" },
6982 "const": { "type": "string" }
6983 },
6984 "additionalProperties": false
6985 });
6986
6987 let registry = Registry::new()
6988 .add("http://example.com/meta/draft4-based", custom_meta)
6989 .unwrap()
6990 .prepare()
6991 .unwrap();
6992
6993 let schema = json!({
6994 "$schema": "http://example.com/meta/draft4-based",
6995 "type": "string",
6996 "const": "foo"
6997 });
6998
6999 let result = crate::meta::options()
7000 .with_registry(®istry)
7001 .validate(&schema);
7002
7003 assert!(result.is_ok());
7004 }
7005
7006 #[test]
7007 fn test_meta_validation_multi_level_chain_uses_resolved_draft() {
7008 // Multi-level chain: user-schema -> meta-2 -> meta-1 -> Draft 4
7009 let meta_level_1 = json!({
7010 "$id": "http://example.com/meta/level1",
7011 "$schema": "http://json-schema.org/draft-04/schema#",
7012 "type": "object",
7013 "properties": {
7014 "customKeyword": { "type": "boolean" }
7015 }
7016 });
7017
7018 let meta_level_2 = json!({
7019 "$id": "http://example.com/meta/level2",
7020 "$schema": "http://example.com/meta/level1",
7021 "type": "object",
7022 "properties": {
7023 "$schema": { "type": "string" },
7024 "type": { "type": "string" },
7025 "minimum": { "type": "number" },
7026 "exclusiveMinimum": { "type": "boolean" }
7027 },
7028 "customKeyword": true,
7029 "additionalProperties": false
7030 });
7031
7032 let registry = Registry::new()
7033 .extend([
7034 ("http://example.com/meta/level1", meta_level_1),
7035 ("http://example.com/meta/level2", meta_level_2),
7036 ])
7037 .unwrap()
7038 .prepare()
7039 .unwrap();
7040
7041 let schema = json!({
7042 "$schema": "http://example.com/meta/level2",
7043 "type": "number",
7044 "minimum": 5,
7045 "exclusiveMinimum": true
7046 });
7047
7048 let result = crate::meta::options()
7049 .with_registry(®istry)
7050 .validate(&schema);
7051
7052 assert!(result.is_ok());
7053 }
7054 }
7055
7056 #[test]
7057 fn test_meta_validator_for_valid_schema() {
7058 let schema = json!({
7059 "type": "string",
7060 "maxLength": 5
7061 });
7062
7063 let validator = crate::meta::validator_for(&schema).expect("Valid meta-schema");
7064 assert!(validator.is_valid(&schema));
7065 }
7066
7067 #[test]
7068 fn test_meta_validator_for_invalid_schema() {
7069 let schema = json!({
7070 "type": "invalid_type"
7071 });
7072
7073 let validator = crate::meta::validator_for(&schema).expect("Valid meta-schema");
7074 assert!(!validator.is_valid(&schema));
7075 }
7076
7077 #[test]
7078 fn test_meta_validator_for_evaluate_api() {
7079 let schema = json!({
7080 "type": "string",
7081 "maxLength": 5
7082 });
7083
7084 let validator = crate::meta::validator_for(&schema).expect("Valid meta-schema");
7085 let evaluation = validator.evaluate(&schema);
7086
7087 let flag = evaluation.flag();
7088 assert!(flag.valid);
7089 }
7090
7091 #[test]
7092 fn test_meta_validator_for_evaluate_api_invalid() {
7093 let schema = json!({
7094 "type": "invalid_type",
7095 "minimum": "not a number"
7096 });
7097
7098 let validator = crate::meta::validator_for(&schema).expect("Valid meta-schema");
7099 let evaluation = validator.evaluate(&schema);
7100
7101 let flag = evaluation.flag();
7102 assert!(!flag.valid);
7103 }
7104
7105 #[test]
7106 fn test_meta_validator_for_all_drafts() {
7107 let schemas = vec![
7108 json!({ "$schema": "http://json-schema.org/draft-04/schema#", "type": "string" }),
7109 json!({ "$schema": "http://json-schema.org/draft-06/schema#", "type": "string" }),
7110 json!({ "$schema": "http://json-schema.org/draft-07/schema#", "type": "string" }),
7111 json!({ "$schema": "https://json-schema.org/draft/2019-09/schema", "type": "string" }),
7112 json!({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "string" }),
7113 ];
7114
7115 for schema in schemas {
7116 let validator = crate::meta::validator_for(&schema).unwrap();
7117 assert!(validator.is_valid(&schema));
7118 }
7119 }
7120
7121 #[test]
7122 fn test_meta_validator_for_iter_errors() {
7123 let schema = json!({
7124 "type": "invalid_type",
7125 "minimum": "not a number"
7126 });
7127
7128 let validator = crate::meta::validator_for(&schema).expect("Valid meta-schema");
7129 let errors: Vec<_> = validator.iter_errors(&schema).collect();
7130 assert!(!errors.is_empty());
7131 }
7132
7133 #[test_case(
7134 "http://json-schema.org/draft-04/schema#",
7135 "http://json-schema.org/draft-04/schema#/definitions/positiveInteger/minimum",
7136 "/definitions/positiveInteger/minimum";
7137 "draft 4"
7138 )]
7139 #[test_case(
7140 "http://json-schema.org/draft-06/schema#",
7141 "http://json-schema.org/draft-06/schema#/definitions/nonNegativeInteger/minimum",
7142 "/definitions/nonNegativeInteger/minimum";
7143 "draft 6"
7144 )]
7145 #[test_case(
7146 "http://json-schema.org/draft-07/schema#",
7147 "http://json-schema.org/draft-07/schema#/definitions/nonNegativeInteger/minimum",
7148 "/definitions/nonNegativeInteger/minimum";
7149 "draft 7"
7150 )]
7151 #[test_case(
7152 "https://json-schema.org/draft/2019-09/schema",
7153 "https://json-schema.org/draft/2019-09/meta/validation#/$defs/nonNegativeInteger/minimum",
7154 "/$defs/nonNegativeInteger/minimum";
7155 "draft 2019-09"
7156 )]
7157 #[test_case(
7158 "https://json-schema.org/draft/2020-12/schema",
7159 "https://json-schema.org/draft/2020-12/meta/validation#/$defs/nonNegativeInteger/minimum",
7160 "/$defs/nonNegativeInteger/minimum";
7161 "draft 2020-12"
7162 )]
7163 fn test_meta_validator_for_reports_negative_length(
7164 dialect: &str,
7165 schema_location: &str,
7166 schema_path: &str,
7167 ) {
7168 const NEGATIVE: &str = "-1 is less than the minimum of 0";
7169 let schema = json!({"$schema": dialect, "minLength": -1});
7170 let validator = crate::meta::validator_for(&schema).expect("Valid meta-schema");
7171
7172 let evaluation = validator.evaluate(&schema);
7173 let evaluated: Vec<_> = evaluation
7174 .iter_errors()
7175 .map(|entry| {
7176 (
7177 entry.instance_location.as_str().to_string(),
7178 entry.schema_location.to_string(),
7179 entry.error.to_string(),
7180 )
7181 })
7182 .collect();
7183 let errors: Vec<_> = validator
7184 .iter_errors(&schema)
7185 .map(|error| {
7186 (
7187 error.to_string(),
7188 error.instance_path().to_string(),
7189 error.schema_path().to_string(),
7190 )
7191 })
7192 .collect();
7193 assert_eq!(
7194 (evaluation.flag().valid, evaluated, errors),
7195 (
7196 false,
7197 vec![(
7198 "/minLength".to_string(),
7199 schema_location.to_string(),
7200 NEGATIVE.to_string()
7201 )],
7202 vec![(
7203 NEGATIVE.to_string(),
7204 "/minLength".to_string(),
7205 schema_path.to_string()
7206 )]
7207 )
7208 );
7209 }
7210}
7211
7212#[cfg(all(test, feature = "resolve-async", not(target_family = "wasm")))]
7213mod async_tests {
7214 use std::{collections::HashMap, sync::Arc};
7215
7216 use serde_json::json;
7217
7218 use crate::{AsyncRetrieve, Draft, Uri};
7219
7220 /// Mock async retriever for testing
7221 #[derive(Clone)]
7222 struct TestRetriever {
7223 schemas: HashMap<String, serde_json::Value>,
7224 }
7225
7226 impl TestRetriever {
7227 fn new() -> Self {
7228 let mut schemas = HashMap::new();
7229 schemas.insert(
7230 "https://example.com/user.json".to_string(),
7231 json!({
7232 "type": "object",
7233 "properties": {
7234 "name": {"type": "string"},
7235 "age": {"type": "integer", "minimum": 0}
7236 },
7237 "required": ["name"]
7238 }),
7239 );
7240 Self { schemas }
7241 }
7242 }
7243
7244 #[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
7245 #[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
7246 impl AsyncRetrieve for TestRetriever {
7247 async fn retrieve(
7248 &self,
7249 uri: &Uri<String>,
7250 ) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
7251 self.schemas
7252 .get(uri.as_str())
7253 .cloned()
7254 .ok_or_else(|| "Schema not found".into())
7255 }
7256 }
7257
7258 #[tokio::test]
7259 async fn test_async_validator_for() {
7260 let schema = json!({
7261 "$ref": "https://example.com/user.json"
7262 });
7263
7264 let validator = crate::async_options()
7265 .with_retriever(TestRetriever::new())
7266 .build(&schema)
7267 .await
7268 .unwrap();
7269
7270 // Valid instance
7271 assert!(validator.is_valid(&json!({
7272 "name": "John Doe",
7273 "age": 30
7274 })));
7275
7276 // Invalid instances
7277 assert!(!validator.is_valid(&json!({
7278 "age": -5
7279 })));
7280 assert!(!validator.is_valid(&json!({
7281 "name": 123,
7282 "age": 30
7283 })));
7284 }
7285
7286 #[tokio::test]
7287 async fn test_async_options_with_draft() {
7288 let schema = json!({
7289 "$ref": "https://example.com/user.json"
7290 });
7291
7292 let validator = crate::async_options()
7293 .with_draft(Draft::Draft202012)
7294 .with_retriever(TestRetriever::new())
7295 .build(&schema)
7296 .await
7297 .unwrap();
7298
7299 assert!(validator.is_valid(&json!({
7300 "name": "John Doe",
7301 "age": 30
7302 })));
7303 }
7304
7305 #[tokio::test]
7306 async fn test_async_retrieval_failure() {
7307 let schema = json!({
7308 "$ref": "https://example.com/nonexistent.json"
7309 });
7310
7311 let result = crate::async_options()
7312 .with_retriever(TestRetriever::new())
7313 .build(&schema)
7314 .await;
7315
7316 assert!(result.is_err());
7317 assert!(result.unwrap_err().to_string().contains("Schema not found"));
7318 }
7319
7320 #[tokio::test]
7321 async fn test_async_nested_references() {
7322 let mut retriever = TestRetriever::new();
7323 retriever.schemas.insert(
7324 "https://example.com/nested.json".to_string(),
7325 json!({
7326 "type": "object",
7327 "properties": {
7328 "user": { "$ref": "https://example.com/user.json" }
7329 }
7330 }),
7331 );
7332
7333 let schema = json!({
7334 "$ref": "https://example.com/nested.json"
7335 });
7336
7337 let validator = crate::async_options()
7338 .with_retriever(retriever)
7339 .build(&schema)
7340 .await
7341 .unwrap();
7342
7343 // Valid nested structure
7344 assert!(validator.is_valid(&json!({
7345 "user": {
7346 "name": "John Doe",
7347 "age": 30
7348 }
7349 })));
7350
7351 // Invalid nested structure
7352 assert!(!validator.is_valid(&json!({
7353 "user": {
7354 "age": -5
7355 }
7356 })));
7357 }
7358
7359 #[tokio::test]
7360 async fn test_async_with_registry_uses_async_retriever_for_inline_only_refs() {
7361 let registry = crate::Registry::new().prepare().unwrap();
7362 let schema = json!({
7363 "$ref": "https://example.com/user.json"
7364 });
7365
7366 let validator = crate::async_options()
7367 .with_registry(®istry)
7368 .with_retriever(TestRetriever::new())
7369 .build(&schema)
7370 .await
7371 .unwrap();
7372
7373 assert!(validator.is_valid(&json!({
7374 "name": "John Doe",
7375 "age": 30
7376 })));
7377 assert!(!validator.is_valid(&json!({
7378 "age": -5
7379 })));
7380 }
7381
7382 #[tokio::test]
7383 async fn test_async_validator_for_basic() {
7384 let schema = json!({"type": "integer"});
7385
7386 let validator = crate::async_validator_for(&schema).await.unwrap();
7387
7388 assert!(validator.is_valid(&json!(42)));
7389 assert!(!validator.is_valid(&json!("abc")));
7390 }
7391
7392 #[tokio::test]
7393 async fn test_async_build_future_is_send() {
7394 let schema = Arc::new(json!({
7395 "$ref": "https://example.com/user.json"
7396 }));
7397 let retriever = TestRetriever::new();
7398
7399 let handle = tokio::spawn({
7400 let schema = Arc::clone(&schema);
7401 let retriever = retriever.clone();
7402 async move {
7403 crate::async_options()
7404 .with_retriever(retriever)
7405 .build(&schema)
7406 .await
7407 }
7408 });
7409
7410 let validator = handle.await.unwrap().unwrap();
7411 assert!(validator.is_valid(&json!({
7412 "name": "John Doe",
7413 "age": 30
7414 })));
7415 }
7416}