I'm developing a Rust library that implements JSONSchema validation (Drafts 7 fully works at the moment). I have Rust-specific questions and concerns about my implementation.
It works by compiling the input schema first and validating the data after it:
use jsonschema::{JSONSchema, Draft};
use serde_json::json;
let schema = json!({"maxLength": 5});
let instance = json!("foo");
let compiled = JSONSchema::compile(&schema, Some(Draft::Draft7));
let result = compiled.validate(&instance);
Main points:
- Input schema is parsed into a tree of structs that implement a common trait -
Validate
that providesvalidate
method (code); - Each struct in this tree have different members that may refer to the original schema (e.g. holding a vector of strings for "required" keyword) or have some computed values, e.g. regex (example);
- During validation, the input value is passed to this tree and it is validated by specific nodes (code);
-
validate
returnsResult<(), ValidationError>
, where the error case contains data needed for displaying errors. This data is copied from schema or instance or constructed dynamically somewhere in the tree; - When resolving a reference, a
Cow
is returned.Borrowed
for cases when the referenced document exists ininstance
andOwned
when it is loaded from a remote location (e.g.{"$ref": "http://localhost/schema.json"}
); - A new validation tree is generated for resolved schemas and validation goes further with this new tree;
My concerns:
-
Validate
trait. Is it a good approach to build a validation tree? What are the alternatives for such a case? - Is it possible to avoid copying to
ValidationError
and use references when possible? In some cases, objects are generated / loaded from a remote location, during validation and have different lifetime than schema or instance. In this case, probably the data should be owned - is it possible to haveValidationError
working for both cases?
Code
Trait (actual code):
pub trait Validate<'a>: Send + Sync + 'a {
fn validate(&self, schema: &JSONSchema, instance: &Value) -> ValidationResult;
fn name(&self) -> String;
}
impl<'a> Debug for dyn Validate<'a> + Send + Sync + 'a {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
f.write_str(&self.name())
}
}
pub type ValidationResult = Result<(), error::ValidationError>;
pub type CompilationResult<'a> = Result<BoxedValidator<'a>, error::CompilationError>;
pub type BoxedValidator<'a> = Box<dyn Validate<'a> + Send + Sync + 'a>;
pub type Validators<'a> = Vec<BoxedValidator<'a>>;
ValidationError
(actual code):
#[derive(Debug)]
pub struct ValidationError {
kind: ValidationErrorKind,
}
#[derive(Debug)]
pub enum ValidationErrorKind {
FalseSchema(Value),
// ...
}
Validation node example ([actual code][3]):
pub struct PropertiesValidator<'a> {
properties: Vec<(&'a String, Validators<'a>)>,
}
impl<'a> PropertiesValidator<'a> {
pub(crate) fn compile(
schema: &'a Value,
context: &CompilationContext,
) -> CompilationResult<'a> {
match schema {
Value::Object(map) => {
let mut properties = Vec::with_capacity(map.len());
for (key, subschema) in map {
properties.push((key, compile_validators(subschema, context)?));
}
Ok(Box::new(PropertiesValidator { properties }))
}
_ => Err(CompilationError::SchemaError),
}
}
}
impl<'a> Validate<'a> for PropertiesValidator<'a> {
fn validate(&self, schema: &JSONSchema, instance: &Value) -> ValidationResult {
if let Value::Object(item) = instance {
for (name, validators) in self.properties.iter() {
if let Some(item) = item.get(*name) {
for validator in validators {
validator.validate(schema, item)?
}
}
}
}
Ok(())
}
// ...
}
Playground for a smaller version;
Rust version: 1.42.0