Skip to content

desirecore/jsonschema-rs

 
 

Repository files navigation

jsonschema

ci codecov Crates.io docs.rs gitter

A JSON Schema validator implementation. It compiles schema into a validation tree to have validation as fast as possible.

Supported drafts:

  • Draft 7 (except optional idn-hostname.json test case)
  • Draft 6
  • Draft 4 (except optional bignum.json test case)

Partially supported drafts (some keywords are not implemented):

  • Draft 2019-09 (requires the draft201909 feature enabled)
  • Draft 2020-12 (requires the draft202012 feature enabled)
# Cargo.toml
jsonschema = "0.18"

To validate documents against some schema and get validation errors (if any):

use jsonschema::JSONSchema;
use serde_json::json;

fn main() {
    let schema = json!({"maxLength": 5});
    let instance = json!("foo");
    let compiled = JSONSchema::compile(&schema)
        .expect("A valid schema");
    let result = compiled.validate(&instance);
    if let Err(errors) = result {
        for error in errors {
            println!("Validation error: {}", error);
            println!(
                "Instance path: {}", error.instance_path
            );
        }
    }
}

Each error has an instance_path attribute that indicates the path to the erroneous part within the validated instance. It could be transformed to JSON Pointer via .to_string() or to Vec<String> via .into_vec().

If you only need to know whether document is valid or not (which is faster):

use jsonschema::is_valid;
use serde_json::json;

fn main() {
    let schema = json!({"maxLength": 5});
    let instance = json!("foo");
    assert!(is_valid(&schema, &instance));
}

Or use a compiled schema (preferred):

use jsonschema::JSONSchema;
use serde_json::json;

fn main() {
    let schema = json!({"maxLength": 5});
    let instance = json!("foo");
    // Draft is detected automatically
    // with fallback to Draft7
    let compiled = JSONSchema::compile(&schema)
        .expect("A valid schema");
    assert!(compiled.is_valid(&instance));
}

Output styles

jsonschema supports basic & flag output styles from Draft 2019-09, so you can serialize the validation results with serde:

use jsonschema::{Output, BasicOutput, JSONSchema};
use serde_json::json;

fn main() {
    let schema_json = json!({
        "title": "string value",
        "type": "string"
    });
    let instance = json!("some string");
    let schema = JSONSchema::compile(&schema_json)
        .expect("A valid schema");
    
    let output: BasicOutput = schema.apply(&instance).basic();
    let output_json = serde_json::to_value(output)
        .expect("Failed to serialize output");
    
    assert_eq!(
        output_json, 
        json!({
            "valid": true,
            "annotations": [
                {
                    "keywordLocation": "",
                    "instanceLocation": "",
                    "annotations": {
                        "title": "string value"
                    }
                }
            ]
        })
    );
}

Custom keywords

jsonschema allows you to implement custom validation logic by defining custom keywords. To use your own keyword, you need to implement the Keyword trait and add it to the JSONSchema instance via the with_keyword method:

use jsonschema::{
    paths::{JSONPointer, JsonPointerNode},
    ErrorIterator, JSONSchema, Keyword, ValidationError,
};
use serde_json::{json, Map, Value};
use std::iter::once;

struct MyCustomValidator;

impl Keyword for MyCustomValidator {
    fn validate<'instance>(
        &self,
        instance: &'instance Value,
        instance_path: &JsonPointerNode,
    ) -> ErrorIterator<'instance> {
        // ... validate instance ...
        if !instance.is_object() {
            let error = ValidationError::custom(
                JSONPointer::default(),
                instance_path.into(),
                instance,
                "Boom!",
            );
            Box::new(once(error))
        } else {
            Box::new(None.into_iter())
        }
    }
    fn is_valid(&self, instance: &Value) -> bool {
        // ... determine if instance is valid ...
        true
    }
}

// You can create a factory function, or use a closure to create new validator instances.
fn custom_validator_factory<'a>(
    // Parent object where your keyword is defined
    parent: &'a Map<String, Value>,
    // Your keyword value
    value: &'a Value,
    // JSON Pointer to your keyword within the schema
    path: JSONPointer,
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
    // You may return validation error if the keyword is misused for some reason
    Ok(Box::new(MyCustomValidator))
}

fn main() {
    let schema = json!({"my-type": "my-schema"});
    let instance = json!({"a": "b"});
    let compiled = JSONSchema::options()
        // Register your keyword via a factory function
        .with_keyword("my-type", custom_validator_factory)
        // Or use a closure
        .with_keyword("my-type-with-closure", |_, _, _| Ok(Box::new(MyCustomValidator)))
        .compile(&schema)
        .expect("A valid schema");
    assert!(compiled.is_valid(instance));
}

Reference resolving and TLS

By default, jsonschema resolves HTTP references via reqwest without TLS support. If you'd like to resolve HTTPS, you need to enable TLS support in reqwest:

reqwest = { version = "*", features = [ "rustls-tls" ] }

Otherwise, you might get validation errors like invalid URL, scheme is not http.

Status

This library is functional and ready for use, but its API is still evolving to the 1.0 API.

Bindings

  • Python - See the ./bindings/python directory
  • Ruby - a crate by @driv3r
  • NodeJS - a package by @ahungrynoob

Running tests

The tests in jsonschema/ depend on the JSON Schema Test Suite. Before calling cargo test, download the suite:

$ git submodule init
$ git submodule update

These commands clone the suite to jsonschema/tests/suite/.

Now, enter the jsonschema directory and run cargo test.

$ cd jsonschema
$ cargo test

Performance

There is a comparison with other JSON Schema validators written in Rust - jsonschema_valid==0.5.2, valico==4.0.0, and boon==0.5.0.

Test machine i8700K (12 cores), 32GB RAM.

Input values and schemas:

Case Schema size Instance size
OpenAPI 18 KB 4.5 MB
Swagger 25 KB 3.0 MB
Canada 4.8 KB 2.1 MB
CITM catalog 2.3 KB 501 KB
Fast (valid) 595 B 55 B
Fast (invalid) 595 B 60 B

Here is the average time for each contender to validate. Ratios are given against compiled JSONSchema using its validate method. The is_valid method is faster, but gives only a boolean return value:

Case jsonschema_valid valico boon jsonschema (validate) jsonschema (is_valid)
OpenAPI - (1) - (1) 11.71 ms (x3.34) 3.500 ms 3.147 ms (x0.89)
Swagger - (2) 180.65 ms (x32.12) 16.01 ms (x2.84) 5.623 ms 3.634 ms (x0.64)
Canada 40.363 ms (x33.13) 427.40 ms (x350.90) 25.50 ms (x20.93) 1.218 ms 1.217 ms (x0.99)
CITM catalog 5.357 ms (x2.51) 39.215 ms (x18.44) 1.58 ms (x0.74) 2.126 ms 569.23 us (x0.26)
Fast (valid) 2.27 us (x4.87) 6.55 us (x14.05) 542.2 us (x1.16) 465.89 ns 113.94 ns (x0.24)
Fast (invalid) 412.21 ns (x0.46) 6.69 us (x7.61) 787.12 us (x0.89) 878.23 ns 4.21ns (x0.004)

Notes:

  1. jsonschema_valid and valico do not handle valid path instances matching the ^\\/ regex.

  2. jsonschema_valid fails to resolve local references (e.g. #/definitions/definitions).

You can find benchmark code in benches/jsonschema.rs, Rust version is 1.78.

Support

If you have anything to discuss regarding this library, please, join our gitter!

Packages

No packages published

Languages

  • Rust 97.3%
  • Python 2.6%
  • Shell 0.1%