Introduction
Portable validation schemas for TypeScript and Rust. Author once, validate everywhere.
Validation logic should be data. Write a schema once in TypeScript. It compiles to JSON. 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
Zod hides 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.