Introduction
Portable validation schemas for TypeScript and Rust. Author once, validate everywhere.
Validation logic, as data. Write a schema once in TypeScript. It compiles to JSON, and the same rules run in TypeScript and Rust, identically. No backend rewrite. No drift.
TypeScript builder -> JSON IR -> TypeScript runtime
-> Rust runtimenpm install wellformed-tspnpm add wellformed-tsyarn add wellformed-tsbun add wellformed-tscargo add wellformed wellformed-macros serde_jsonYour first schema, in 30 seconds
import { w, validate, type Infer } from "wellformed-ts";
const Contact = w.object({
firstName: w.string().trim().minLen(1),
lastName: w.string().trim().minLen(1),
email: w.string().trim().lower().email(),
phone: w.string().phoneUS().optional(),
preferredContact: w.enum(["email", "phone", "mail"] as const),
});
// Types come free. No duplicate definitions.
type Contact = Infer<typeof Contact>;
// { firstName: string; lastName: string; email: string;
// phone?: string; preferredContact: "email" | "phone" | "mail" }
const result = validate(Contact.toSchema("1.0"), input);
result.valid; // true | falseuse serde_json::json;
use wellformed_macros::wellformed;
fn main() -> wellformed::Result<()> {
let contact = wellformed!("schemas/contact.json");
let (result, value) = contact.validate_json(r#"{
"firstName": " Ada ",
"lastName": "Lovelace",
"email": "ADA@EXAMPLE.COM",
"preferredContact": "email"
}"#)?;
assert!(result.is_valid());
assert_eq!(value["firstName"], json!("Ada"));
Ok(())
}TypeScript gives you the fluent authoring surface and inferred application type. Rust uses wellformed! to embed the same JSON IR as a schema value with native validation helpers.
Why this is different
Most validation libraries hide your rules inside JavaScript functions. wellformed keeps them as JSON. That single decision is the whole product: the schema becomes portable.
import { w, schemaToJSON } from "wellformed-ts";
const schema = w
.object({
ssn: w.string().digitsOnly().ssn(),
})
.toSchema("1.0");
schemaToJSON(schema, true);{
"version": "1.0",
"root": {
"type": "object",
"properties": {
"ssn": {
"type": "string",
"transforms": [{ "fn": "digits_only" }],
"constraints": [
{
"pred": { "type": "call", "name": "is_ssn" },
"error": {
"code": "INVALID_SSN",
"message": "Invalid Social Security Number"
}
}
]
}
}
}
}Every transform, predicate, and error code is plain JSON. Store it in a database. Send it over the wire. Deserialize it in the Rust wellformed crate. Same results, no JavaScript required.
The whole idea, in one sentence
Validation that crosses a runtime boundary should be data, not a closure.
Where to go next
Getting Started
Install and validate your first schema in under a minute.
Playground
Edit a schema and watch a real form validate live.
Validate in Rust
Run the same IR natively in the Rust crate.
Domain validators
60+ built-in predicates: SSN, EIN, CUSIP, IBAN, ICD-10.
Cross-field rules
Conditional requirements and comparisons, as data.
vs Zod and JSON Schema
When to reach for wellformed, and when not to.