postmortem 0.1.0

A validation library that accumulates all errors for comprehensive feedback
Documentation

postmortem

Learn what went wrong—all at once.

Crates.io Documentation License

A validation library that accumulates all validation errors instead of short-circuiting on the first failure.

Why "postmortem"?

In software, a postmortem is what you do after something breaks—gathering all the evidence to understand what went wrong. Traditional validation libraries give you the frustrating experience of fixing one error only to discover another:

Validation failed: email is required

# fix email...

Validation failed: age must be >= 18

# fix age...

Validation failed: password too short

postmortem gives you the complete picture upfront:

Validation errors (3):
  $.email: missing required field
  $.age: value 15 must be >= 18
  $.password: length 5 is less than minimum 8

One validation run. All errors. Complete feedback for fixing what went wrong.

Features

  • Accumulate all errors — Never stop at the first problem
  • Composable schemas — Build complex validators from simple primitives
  • Type-safe — Leverage Rust's type system
  • JSON path tracking — Know exactly which field failed (e.g., users[0].email)
  • Schema registry — Define reusable schemas with references
  • Cross-field validation — Validate relationships between fields
  • Recursive schemas — Support for self-referential data structures

Quick Start

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"
});

let result = user_schema.validate(&data, &JsonPath::root());

// Handle accumulated errors
match result {
    Ok(value) => println!("Valid: {:?}", value),
    Err(errors) => {
        eprintln!("Validation errors ({}):", errors.len());
        for error in errors.iter() {
            eprintln!("  {}: {}", error.path, error.message);
        }
    }
}

Installation

[dependencies]
postmortem = "0.1"

Core Concepts

Schema Types

Build validation schemas using a fluent API:

// String validation
let name = Schema::string()
    .min_len(1)
    .max_len(100)
    .pattern(r"^[a-zA-Z\s]+$");

// Integer validation
let age = Schema::integer()
    .min(0)
    .max(150);

// Array validation
let tags = Schema::array()
    .min_items(1)
    .max_items(10)
    .items(Schema::string());

// Object validation
let user = Schema::object()
    .required("name", Schema::string())
    .optional("bio", Schema::string().max_len(500));

Schema Combinators

Combine schemas for complex validation logic:

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

// All schemas must pass
let strict_string = 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
]);

// Exactly one schema must pass (XOR)
let payment = Schema::exactly_one_of(vec![
    Schema::object().required("card_number", Schema::string()),
    Schema::object().required("bank_account", Schema::string()),
]);

Schema Registry and References

Define reusable schemas with references:

use postmortem::SchemaRegistry;

let mut registry = SchemaRegistry::new();

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

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

// Validate with the registry
let result = registry.validate("person", &data);

JSON Path Tracking

Every error includes the exact path to the failing field:

let data = json!({
    "users": [
        {"email": "valid@example.com"},
        {"email": ""},  // Invalid
        {"email": "also-valid@example.com"},
        {"email": ""}   // Also invalid
    ]
});

let schema = Schema::array().items(
    Schema::object().required("email", Schema::string().min_len(1))
);

let result = schema.validate(&data, &JsonPath::root());
// Errors at: users[1].email and users[3].email

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())
        }
    });

Design Philosophy

postmortem is built on functional programming principles:

  • 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

This design makes schemas:

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

Documentation

Full API documentation is available at docs.rs/postmortem.

License

MIT — Glen Baker iepathos@gmail.com