Skip to main content

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//! - ![Draft 2020-12](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Frust-jsonschema%2Fcompliance%2Fdraft2020-12.json)
22//! - ![Draft 2019-09](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Frust-jsonschema%2Fcompliance%2Fdraft2019-09.json)
23//! - ![Draft 7](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Frust-jsonschema%2Fcompliance%2Fdraft7.json)
24//! - ![Draft 6](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Frust-jsonschema%2Fcompliance%2Fdraft6.json)
25//! - ![Draft 4](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Frust-jsonschema%2Fcompliance%2Fdraft4.json)
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(&registry)
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(&registry)
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(&registry);
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(&current_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/// [![Draft 4](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Frust-jsonschema%2Fcompliance%2Fdraft4.json)](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/// [![Draft 6](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Frust-jsonschema%2Fcompliance%2Fdraft6.json)](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/// [![Draft 7](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Frust-jsonschema%2Fcompliance%2Fdraft7.json)](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/// [![Draft 2019-09](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Frust-jsonschema%2Fcompliance%2Fdraft2019-09.json)](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/// [![Draft 2020-12](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Frust-jsonschema%2Fcompliance%2Fdraft2020-12.json)](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(&registry)
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(&registry)
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(&registry)
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(&registry)
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(&registry)
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(&registry)
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(&registry)
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(&registry)
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(&registry)
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(&registry)
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(&registry)
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(&registry);
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(&registry)
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(&registry)
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(&registry)
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(&registry)
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(&registry)
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(&registry)
6575                .is_valid(&schema));
6576
6577            assert!(crate::meta::options()
6578                .with_registry(&registry)
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(&registry)
6609                .is_valid(&schema));
6610
6611            assert!(crate::meta::options()
6612                .with_registry(&registry)
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(&registry)
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(&registry)
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(&registry)
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(&registry)
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(&registry)
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(&registry)
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(&registry)
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(&registry)
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(&registry)
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(&registry)
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(&registry)
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(&registry)
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(&registry)
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}