Skip to main content
Background Image
  1. Projects/

Postmortem - Schema Validation for Rust

Author
Glen Baker
Building open source tooling
Table of Contents

Learn what went wrong—all at once

Crates.io
Documentation
GitHub

Links: GitHub | Crates.io | Documentation

Overview
#

Postmortem is a validation library that performs comprehensive validation and accumulates all validation errors instead of stopping at the first failure. Built on Stillwater’s Validation type, it enables you to see every problem with your data in a single pass—a “postmortem” that reveals the complete picture of what went wrong.

The Problem
#

Traditional validation libraries give developers and users a frustrating experience:

  • Sequential error discovery - Fix one validation error only to discover another, requiring multiple submission cycles
  • Poor user experience - Users fix one field, resubmit, then discover another error
  • Lost context - When validation fails deep in nested structures, it’s unclear which field caused the problem
  • Testing complexity - Hard to verify all validation rules are working when you only see one error at a time
  • Verbose code - Manually checking each field and accumulating errors is repetitive and error-prone

The Solution
#

Postmortem validates your entire data structure and reports all errors in one pass:

use postmortem::{Schema, JsonPath};
use serde_json::json;

// Build a validation schema
let user_schema = Schema::object()
    .required("email", Schema::string().min_len(1).max_len(255))
    .required("age", Schema::integer().min(18).max(120))
    .required("password", Schema::string().min_len(8));

// Validate data - accumulates ALL errors
let data = json!({
    "email": "",
    "age": 15,
    "password": "short"
});

match user_schema.validate(&data, &JsonPath::root()) {
    Ok(value) => println!("Valid: {:?}", value),
    Err(errors) => {
        // See every problem, not just the first
        eprintln!("Validation errors ({}):", errors.len());
        for error in errors.iter() {
            eprintln!("  {}: {}", error.path, error.message);
        }
    }
}

// Output:
// Validation errors (3):
//   $.email: string length must be >= 1
//   $.age: value must be >= 18
//   $.password: string length must be >= 8

Key Features
#

Composable Schemas
#

Build complex validation logic from simple primitives:

// Combine schemas with logical operators
let id = Schema::one_of(vec![
    Schema::string().pattern(r"^usr_[0-9a-f]{16}$"),
    Schema::integer().min(1),
]);

let password = Schema::all_of(vec![
    Schema::string().min_len(8),
    Schema::string().pattern(r"[A-Z]"),  // has uppercase
    Schema::string().pattern(r"[0-9]"),  // has digit
]);

JSON Path Tracking
#

Every error includes the exact path to the failing field:

let data = json!({
    "users": [
        {"email": "[email protected]"},
        {"email": ""},  // Invalid
        {"email": "[email protected]"},
        {"email": ""}   // Also invalid
    ]
});

// Errors report exact paths: users[1].email, users[3].email

Schema Registry
#

Define reusable schemas with references:

use postmortem::SchemaRegistry;

let mut registry = SchemaRegistry::new();

registry.define("address", Schema::object()
    .required("street", Schema::string())
    .required("city", Schema::string())
    .required("zip", Schema::string().pattern(r"^\d{5}$")));

let person = Schema::object()
    .required("name", Schema::string())
    .required("home", Schema::ref_schema("#/address"))
    .required("work", Schema::ref_schema("#/address"));

let result = registry.validate("person", &data);

Cross-Field Validation
#

Validate relationships between fields:

let schema = Schema::object()
    .required("password", Schema::string().min_len(8))
    .required("confirm_password", Schema::string())
    .custom(|value| {
        if value["password"] != value["confirm_password"] {
            Err(SchemaError::custom(
                JsonPath::new("confirm_password"),
                "passwords must match"
            ))
        } else {
            Ok(value.clone())
        }
    });

Effect Integration
#

For advanced use cases requiring dependency injection or async validation:

use postmortem::effect::{SchemaEnv, FileSystem, load_schemas_from_dir};

struct AppEnv {
    fs: RealFileSystem,
}

impl SchemaEnv for AppEnv {
    type Fs = RealFileSystem;

    fn filesystem(&self) -> &Self::Fs {
        &self.fs
    }
}

// Load schemas from directory
let env = AppEnv { fs: RealFileSystem };
let registry = load_schemas_from_dir(&env, Path::new("./schemas"))?;

// Validate with environment dependencies
let email_schema = Schema::string()
    .validate_with_env(|value, path, env: &DatabaseEnv| {
        // Check uniqueness against database
        if is_email_taken(env, value) {
            Validation::Failure(SchemaErrors::single(
                SchemaError::new(path.clone(), "email already exists")
            ))
        } else {
            Validation::Success(())
        }
    });

Technical Highlights
#

  • Language: Rust
  • Foundation: Built on Stillwater’s Validation type for error accumulation
  • Schema Types: String, integer, number, boolean, array, object, null
  • Combinators: one_of, all_of, exactly_one_of for complex logic
  • Format Support: Works with serde_json::Value for universal JSON validation
  • Testing: Mockable environment pattern for testable validation logic

Installation
#

# Add to Cargo.toml
cargo add postmortem

# Or manually
[dependencies]
postmortem = "0.1"

Quick Start
#

use postmortem::{Schema, JsonPath};
use serde_json::json;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let schema = Schema::object()
        .required("name", Schema::string().min_len(1))
        .required("age", Schema::integer().min(0).max(150))
        .optional("email", Schema::string().pattern(r"^[\w\.-]+@[\w\.-]+\.\w+$"));

    let data = json!({
        "name": "Alice",
        "age": 30,
        "email": "[email protected]"
    });

    match schema.validate(&data, &JsonPath::root()) {
        Ok(_) => println!("Valid!"),
        Err(errors) => {
            for error in errors.iter() {
                eprintln!("{}: {}", error.path, error.message);
            }
        }
    }

    Ok(())
}

Design Philosophy
#

Postmortem is built on functional programming principles from Stillwater:

  • Pure validation - Schemas are immutable, validation has no side effects
  • Composition - Build complex validators from simple primitives
  • Error accumulation - Uses applicative functors (Stillwater’s Validation type) to collect all errors
  • Explicit effects - I/O and side effects are opt-in via the environment pattern

This design makes schemas:

  • Easy to test in isolation
  • Safe to use concurrently (schemas are Send + Sync)
  • Simple to compose and reuse

Project Status
#

Active development, available on crates.io.

Links#

Related

Stillwater - Pure Core, Imperative Shell for Rust
Premortem - Configuration Validation for Rust
Debtmap - Rust Technical Debt Analyzer
Prodigy - AI Workflow Orchestration for Claude
Automating Documentation Maintenance with Prodigy: A Real-World Case Study
Transforming ripgrep's Documentation with AI Automation and MkDocs