Learn what went wrong—all at once
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
Validationtype for error accumulation - Schema Types: String, integer, number, boolean, array, object, null
- Combinators:
one_of,all_of,exactly_one_offor complex logic - Format Support: Works with
serde_json::Valuefor 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
Validationtype) 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#
- GitHub: github.com/iepathos/postmortem
- Documentation: docs.rs/postmortem
- Crates.io: Available via
cargo add postmortem