foundation_jsonschema 0.0.1

Self-contained JSON Schema validation for ewe_platform
Documentation

foundation_jsonschema

A self-contained, no_std-compatible JSON Schema validation library for Rust. Supports Draft 4, 6, 7, 2019-09, and 2020-12 with a fluent scheme builder API.

Features

  • Five draft versions — Draft 4, 6, 7, 2019-09, 2020-12 with automatic detection
  • Fluent builder API — type-safe scheme::* builders that produce valid schemas
  • Format validation — email, uri, uuid, date-time, hostname, ipv4, ipv6, base64
  • Compound schemasanyOf, oneOf, allOf, not, if/then/else
  • Custom keywords — register your own validation logic
  • Custom formats — override or extend the built-in format checkers
  • No external dependencies — all meta-schemas resolved in-memory
  • no_std compatible — works with alloc, regex via fancy-regex feature

Quick Start

use foundation_jsonschema::{validator_for, Draft};
use serde_json::json;

// Validate a schema directly
let schema = json!({
    "type": "object",
    "required": ["name", "age"],
    "properties": {
        "name": {"type": "string", "minLength": 1},
        "age": {"type": "integer", "minimum": 0, "maximum": 150}
    }
});

let validator = validator_for(&schema).unwrap();
assert!(validator.is_valid(&json!({"name": "Alice", "age": 30})));
assert!(!validator.is_valid(&json!({"name": "Alice"}))); // missing required

The Scheme Builder API

The scheme module provides type-safe, fluent builders for constructing JSON Schema documents. Each builder only exposes methods that make sense for its type, catching configuration errors at compile time rather than runtime.

Primitives

Strings with length, pattern, and format constraints:

use foundation_jsonschema::scheme;
use serde_json::json;

let validator = scheme::string()
    .min_len(3)
    .max_len(32)
    .pattern(r"^[a-zA-Z0-9_]+$")
    .build()
    .compile()
    .unwrap();

assert!(validator.is_valid(&json!("darkvo1d")));
assert!(!validator.is_valid(&json!("ab")));

Format shorthands for common string patterns:

let validator = scheme::string()
    .email()
    .build()
    .assert_format(true)  // enforce format validation
    .compile()
    .unwrap();

assert!(validator.is_valid(&json!("user@example.com")));
assert!(!validator.is_valid(&json!("not-an-email")));

Available format shorthands: .email(), .uri(), .uuid(), .date_time(), .hostname(), .ipv4(), .ipv6(), .base64().

Integers with range and divisibility:

let validator = scheme::integer()
    .min(1)
    .max(65535)
    .description("Network port")
    .build()
    .compile()
    .unwrap();

assert!(validator.is_valid(&json!(8080)));
assert!(!validator.is_valid(&json!(0)));
assert!(!validator.is_valid(&json!(70000)));

Convenience shorthands: .positive(), .nonnegative(), .negative(), .nonpositive(), .multiple_of(5), .exclusive_min(0), .exclusive_max(100).

Numbers (floats):

let validator = scheme::number()
    .min(0.0)
    .max(100.0)
    .build()
    .compile()
    .unwrap();

assert!(validator.is_valid(&json!(50.5)));
assert!(!validator.is_valid(&json!(-1.0)));
assert!(!validator.is_valid(&json!(101.0)));

Booleans and null:

let validator = scheme::boolean()
    .default(json!(true))
    .build()
    .compile()
    .unwrap();

// Null schema — matches only null
let null_validator = scheme::null()
    .description("Explicitly null")
    .build()
    .compile()
    .unwrap();

Objects

Required and optional properties:

let validator = scheme::object()
    .required("username", scheme::string().min_len(3).max_len(32))
    .required("email", scheme::string().email())
    .required("age", scheme::integer().min(0).max(150))
    .optional("bio", scheme::string().max_len(500))
    .build()
    .assert_format(true)
    .compile()
    .unwrap();

assert!(validator.is_valid(&json!({
    "username": "darkvo1d",
    "email": "darkvoid@example.com",
    "age": 28
})));

assert!(validator.is_valid(&json!({
    "username": "darkvo1d",
    "email": "darkvoid@example.com",
    "age": 28,
    "bio": "A short bio"
})));

Strict mode — reject unknown properties:

let validator = scheme::object()
    .required("name", scheme::string())
    .strict()  // additionalProperties: false
    .build()
    .compile()
    .unwrap();

assert!(validator.is_valid(&json!({"name": "Alice"})));
assert!(!validator.is_valid(&json!({"name": "Alice", "extra": true})));

Loose mode — allow any extra properties (default, but explicit):

let schema = scheme::object()
    .required("id", scheme::integer())
    .loose()  // additionalProperties: true
    .build_schema();

Catch-all schema — extra properties must match a schema:

let schema = scheme::object()
    .required("name", scheme::string())
    .catchall(scheme::string())  // all extra fields must be strings
    .build_schema();

Pattern properties — validate properties matching a regex:

let schema = scheme::object()
    .pattern_property(r"^x-", scheme::string())
    .build_schema();
// Properties like "x-custom", "x-header" must be strings

Nested objects:

let validator = scheme::object()
    .required("name", scheme::string())
    .required("address", scheme::object()
        .required("street", scheme::string())
        .required("city", scheme::string())
        .required("zip", scheme::string().len(5).pattern(r"^\d{5}$"))
        .strict()
    )
    .build()
    .compile()
    .unwrap();

Arrays

Items with constraints:

let validator = scheme::array()
    .items(scheme::string().min_len(1).max_len(20))
    .min_items(1)
    .max_items(5)
    .unique()
    .build()
    .compile()
    .unwrap();

assert!(validator.is_valid(&json!(["rust", "json", "schema"])));
assert!(!validator.is_valid(&json!([])));              // min_items: 1
assert!(!validator.is_valid(&json!(["rust", "rust"]))); // unique
assert!(!validator.is_valid(&json!(["a", "b", "c", "d", "e", "f"]))); // max_items: 5

Tuple validation (Draft 2020-12 prefixItems):

let validator = scheme::array()
    .prefix_items(vec![
        scheme::number(),  // latitude
        scheme::number(),  // longitude
        scheme::number(),  // altitude
    ])
    .len(3)
    .build()
    .compile()
    .unwrap();

assert!(validator.is_valid(&json!([1.0, 2.0, 3.0])));
assert!(!validator.is_valid(&json!([1.0, 2.0])));
assert!(!validator.is_valid(&json!([1.0, 2.0, 3.0, 4.0])));

Additional items for tuple + rest patterns:

let schema = scheme::array()
    .prefix_items(vec![scheme::string(), scheme::integer()])
    .additional_items(scheme::boolean())
    .build_schema();
// First item: string, second: integer, rest: boolean

Contains, min_contains, max_contains (Draft 6+):

let schema = scheme::array()
    .contains(scheme::integer().min(0))
    .min_contains(2)
    .max_contains(5)
    .build_schema();

Nullable and Optional

Nullable — accepts the type or null:

let validator = scheme::object()
    .required("name", scheme::string().nullable())
    .build()
    .compile()
    .unwrap();

assert!(validator.is_valid(&json!({"name": "Alice"})));
assert!(validator.is_valid(&json!({"name": null})));
assert!(!validator.is_valid(&json!({"name": 42})));

Optional — wraps in anyOf with null:

let validator = scheme::object()
    .required("name", scheme::string())
    .required("middle_name", scheme::string().optional())
    .build()
    .compile()
    .unwrap();

assert!(validator.is_valid(&json!({"name": "Alice", "middle_name": "Marie"})));
assert!(validator.is_valid(&json!({"name": "Alice", "middle_name": null})));

Note: .optional() on a field schema (used with required()) makes the field value nullable. Use .optional(name, schema) on the object builder to add a property that is not in the required array.

Enum and Literal Values

Enum constraint on strings:

let validator = scheme::object()
    .required("task", scheme::string())
    .required("priority", scheme::string()
        .enum_values(vec!["low".into(), "medium".into(), "high".into()])
    )
    .build()
    .compile()
    .unwrap();

assert!(validator.is_valid(&json!({"task": "deploy", "priority": "high"})));
assert!(!validator.is_valid(&json!({"task": "deploy", "priority": "critical"})));

Standalone enum:

use foundation_jsonschema::{scheme, validator_for};
use serde_json::json;

let schema = scheme::r#enum(vec![json!("red"), json!("green"), json!("blue")]);
let v = validator_for(&schema).unwrap();
assert!(v.is_valid(&json!("red")));
assert!(!v.is_valid(&json!("purple")));

Literal / const:

use foundation_jsonschema::validator_for;
use serde_json::json;

let schema = scheme::literal(json!("exact"));
let v = validator_for(&schema).unwrap();
assert!(v.is_valid(&json!("exact")));
assert!(!v.is_valid(&json!("not exact")));

Compound Schemas

anyOf — must match at least one:

use foundation_jsonschema::validator_for;
use serde_json::json;

let schema = scheme::any_of(vec![
    scheme::string().build_schema(),
    scheme::integer().build_schema(),
]);
let v = validator_for(&schema).unwrap();
assert!(v.is_valid(&json!("hello")));
assert!(v.is_valid(&json!(42)));
assert!(!v.is_valid(&json!(true)));

oneOf — must match exactly one:

let schema = scheme::one_of(vec![
    scheme::string().build_schema(),
    scheme::integer().build_schema(),
]);

allOf — must match all (intersection):

use foundation_jsonschema::validator_for;
use serde_json::json;

let schema = scheme::all_of(vec![
    scheme::object().required("a", scheme::string()).build_schema(),
    scheme::object().required("b", scheme::integer()).build_schema(),
]);
let v = validator_for(&schema).unwrap();
assert!(v.is_valid(&json!({"a": "hi", "b": 1})));
assert!(!v.is_valid(&json!({"a": "hi"})));

not — inverted validation:

use foundation_jsonschema::validator_for;
use serde_json::json;

let schema = scheme::not(scheme::null());
let v = validator_for(&schema).unwrap();
assert!(v.is_valid(&json!("hello")));
assert!(!v.is_valid(&json!(null)));

if/then — conditional validation:

use foundation_jsonschema::validator_for;
use serde_json::json;

let schema = scheme::if_then(
    scheme::object().required("role", scheme::literal(json!("admin"))).build_schema(),
    scheme::object().required("permissions", scheme::array().items(scheme::string())).build_schema(),
);
let v = validator_for(&schema).unwrap();
// Non-admin: no permissions required
assert!(v.is_valid(&json!({"role": "user"})));
// Admin with permissions: valid
assert!(v.is_valid(&json!({"role": "admin", "permissions": ["read", "write"]})));

if/then/else — discriminated union:

use foundation_jsonschema::validator_for;
use serde_json::json;

let schema = scheme::if_then_else(
    scheme::object()
        .required("kind", scheme::literal(json!("circle")))
        .build_schema(),
    scheme::object()
        .required("kind", scheme::literal(json!("circle")))
        .required("radius", scheme::number().positive())
        .build_schema(),
    scheme::object()
        .required("kind", scheme::literal(json!("rect")))
        .required("width", scheme::number().positive())
        .required("height", scheme::number().positive())
        .build_schema(),
);
let v = validator_for(&schema).unwrap();
assert!(v.is_valid(&json!({"kind": "circle", "radius": 5.0})));
assert!(v.is_valid(&json!({"kind": "rect", "width": 10.0, "height": 20.0})));

References

// Internal reference
let schema = scheme::ref_("#/$defs/user");

// Dynamic reference (Draft 2020-12)
let schema = scheme::dynamic_ref("#node");

Schema Inspection

Build a schema without compiling it, for inspection or serialization:

use serde_json::json;

let schema = scheme::object()
    .required("name", scheme::string())
    .required("age", scheme::integer().min(0))
    .optional("email", scheme::string().email())
    .strict()
    .build_schema();

// schema is a serde_json::Value — inspect, serialize, or pass to other tools
println!("{}", serde_json::to_string_pretty(&schema).unwrap());

build_schema vs build

  • build_schema(&self) — returns a serde_json::Value. Non-consuming, can be called multiple times. Useful for inspection.
  • build(self) — returns a ValidationOptions. Consuming, chains into .compile() for the full build-compile-validate pipeline.
use serde_json::json;

let builder = scheme::string().min_len(1);

// Inspect multiple times
let s1 = builder.build_schema();
let s2 = builder.build_schema();

// Compile and validate (consumes builder)
let validator = builder.build().compile().unwrap();

ValidationOptions

Fine-grained control over schema compilation:

use foundation_jsonschema::{scheme, Draft};

let validator = scheme::object()
    .required("email", scheme::string().email())
    .build()
    .with_draft(Draft::Draft7)
    .assert_format(true)
    .compile()
    .unwrap();

Schema Accessors

When using build(), the schema is embedded in ValidationOptions:

use serde_json::json;

let opts = scheme::string().min_len(1).max_len(255).build();

// Reference without consuming
let schema_ref = opts.schema();
println!("{}", schema_ref);

// Clone the schema
let cloned = opts.clone_schema();

// Take ownership
let owned = opts.into_schema();

Custom Draft Selection

use foundation_jsonschema::{scheme, Draft};

let validator = scheme::object()
    .required("value", scheme::number())
    .build()
    .with_draft(Draft::Draft202012)
    .compile()
    .unwrap();

Custom Resolvers

Provide your own resource resolver for external references:

use foundation_jsonschema::InMemoryFetcher;
use foundation_jsonschema::scheme;

let resolver = InMemoryFetcher::builtin();
let validator = scheme::object()
    .build()
    .with_resolver(resolver)
    .compile()
    .unwrap();

Direct Validation

For cases where you already have a serde_json::Value schema:

use foundation_jsonschema::{validate, is_valid, validator_for};
use serde_json::json;

let schema = json!({"type": "string", "minLength": 1});

// Quick boolean check
assert!(is_valid(&schema, &json!("hello")));

// Full validation with error details
let result = validate(&schema, &json!(""));
assert!(result.is_err());

// Reusable validator for repeated checks
let validator = validator_for(&schema).unwrap();
assert!(validator.is_valid(&json!("test")));

no_std Support

Enable the fancy-regex feature to use fancy-regex instead of the regex crate, allowing pattern validation without the standard library:

[dependencies]
foundation_jsonschema = { version = "0.0.1", default-features = false, features = ["fancy-regex"] }