Request Validator

The Validator capability checks a structured value — a parsed request body, a decoded config file, a message about to be enqueued — against a schema, and returns a full list of errors with machine-readable paths. Backends are pluggable: the same trait fronts JSON Schema, CUE, and (future) Zod / TypeBox bridges.

When To Use It

Use a ValidatorPlugin whenever you need:

  • Multi-error form validation where the UI shows every problem at once, not just the first failure.
  • Config-file validation at startup.
  • Message shape enforcement on a queue boundary before the payload reaches a handler.

If you only need a single type check, inline serde is simpler. Reach for Validator when the schema lives outside your Rust code (JSON files, admin UI, shared with other services).

Note

ValidatorError (plugin failure) is distinct from a ValidationReport with valid = false (successful evaluation of an invalid input). A SchemaNotFound error means you forgot to call register_schema; it is not a validation result.

The Trait

pub trait ValidatorPlugin: Send + Sync {
    fn name(&self) -> &str;
    fn schema_language(&self) -> &str;          // "json-schema", "cue", ...
    fn register_schema(&self, name: &str, source: &str) -> Result<(), ValidatorError>;
    fn validate(
        &self,
        schema: &str,
        value: &serde_json::Value,
    ) -> Result<ValidationReport, ValidatorError>;
}

The register-then-validate split amortises parse costs across many validation calls against the same schema. Re-registering a name replaces the previous entry.

Key Types

Type Purpose
ValidationReport valid: bool plus an ordered list of errors.
ValidationError path (JSON-pointer-style), message, kind.
ValidationErrorKind TypeMismatch, MissingField, OutOfRange, PatternMismatch, UnknownField, Custom.
ValidatorError Plugin-level failure: SchemaNotFound (400), MalformedSchema (400), Backend (500).

ValidatorError is distinct from ValidationReport with valid = false. The first means "the plugin couldn't do its job"; the second means "the plugin did its job and the input was wrong".

Reference Implementations

@bext/validate-jsonschema

Minimal, dependency-free JSON Schema subset covering the keywords that cover 95% of real-world use:

  • type (string, number, integer, boolean, object, array, null)
  • required
  • properties
  • minimum / maximum
  • minLength / maxLength
  • pattern
  • enum
  • items

Perfect for HTTP form validation and config files where you want portability across tools.

@bext/validate-cue (experimental)

Experimental — not for production. A stub that demonstrates the trait surface with a toy field: type grammar; it is not a real CUE evaluator. Full CUE evaluation requires either a shell-out to the cue binary or an embedded interpreter, both of which are future work. Use it only to experiment with the trait surface.

The server selects the validator with BEXT_VALIDATOR_PROVIDERjsonschema (the default, recommended) or cue (experimental). The @bext/validate-jsonschema backend is what every production deployment should use.

Example

use bext_plugin_api::validator::*;
use serde_json::json;

// Register at startup
let plugin: &dyn ValidatorPlugin = /* @bext/validate-jsonschema */;
plugin.register_schema(
    "create_user",
    r#"{
        "type": "object",
        "required": ["email", "age"],
        "properties": {
            "email": { "type": "string", "pattern": "^.+@.+\\..+$" },
            "age":   { "type": "integer", "minimum": 13, "maximum": 150 }
        }
    }"#,
)?;

// Validate on each request
let body = json!({ "email": "not-an-email", "age": 12 });
let report = plugin.validate("create_user", &body)?;

if !report.valid {
    for err in &report.errors {
        eprintln!("{}: {} ({:?})", err.path, err.message, err.kind);
    }
    // returns structured errors to the caller
}

Typical output for that input:

/email: value does not match pattern (PatternMismatch)
/age:   value 12 is less than minimum 13 (OutOfRange)

Note that both errors appear in one pass — the trait explicitly reports everything rather than bailing on the first mismatch.

Picking A Backend

Scenario Pick
Standard HTTP body validation, shared schemas across services @bext/validate-jsonschema (recommended default).
Config files with simple type constraints @bext/validate-jsonschema.
Experimenting with the CUE trait surface (toy grammar) @bext/validate-cue (experimental).
You already have Zod / TypeBox schemas in TypeScript A future @bext/validate-zod bridge.

You can install multiple validator plugins side-by-side and pick one at each call site — they share no global state beyond the plugin registry.

Feature Flag

None. The Validator trait and types live in bext-plugin-api and are always available; no cargo feature gates them.

See Also

  • @bext/validate-jsonschema — the minimal JSON Schema backend.
  • @bext/validate-cue — the CUE backend slot.
  • Search — wire a validator before index() to enforce document shape at the write boundary.
  • Webhook — validate decoded webhook payloads against a registered schema.
  • Capabilities overview — the full list of pluggable capabilities.