Validation logic should be data.
Write the schema once. It compiles to JSON, then validates identically in JavaScript, TypeScript, and Rust: same transforms, same rules, same error codes. One source of truth, no drift across runtimes.
Inspired by JSON Schema's portable data model, with Zod-like authoring and first-class support for transforms, domain predicates, and cross-field rules.
MIT · TS & Rust · 60+ validators
import { w, validate, type Infer } from "wellformed-ts";
// Domain validators + cross-field rules, all built in.
const Account = w.object({
email: w.string().email(),
ssn: w.string().ssn().optional(),
ein: w.string().ein().optional(),
type: w.enum(["individual", "business"] as const),
})
.when("type").equals("individual").require("ssn")
.when("type").equals("business").require("ein");
type Account = Infer<typeof Account>;
// Compile to portable IR, then validate.
const result = validate(Account.toSchema("1.0"), input);The problem
Your validation rules are trapped in JavaScript closures.
Zod and Yup hide constraints, transforms, and cross-field logic inside functions. The moment validation crosses a boundary (a Rust service, a stored record, a review tool) you rewrite it by hand. Now you maintain two copies. They drift. Bugs ship.
wellformed makes the schema serializable data: one IR that any runtime reads, stores, diffs, and runs identically.
wellformed-ts
wellformed
Try it live
See validation as data, live.
Edit a schema's IR on the left. A real form validates on the right: predicates, transforms, cross-field rules, error codes, all in your browser. Seeing the wire format do the work converts skeptics faster than any pitch.
Open the Playgroundrequired when type is business
Batteries included
Everything you'd otherwise re-implement on both sides.
Portable IR
schemaToJSON(schema)
Author in TypeScript, ship as JSON. Store it, diff it in code review, version it like any other artifact.
TypeScript + Rust
validate(&schema, &input)
One schema, two first-class native runtimes. The same rules produce the same results on both sides of the wire.
60+ domain validators
is_ssn · is_ein · is_cusip · is_icd10
TINs, financial identifiers, healthcare and aviation codes, contact fields. Built in. No regex archaeology.
Cross-field rules
.when("type").equals("business")
Conditional requirements, mutual exclusion, field comparisons. Real business logic, expressed as data.
Transform pipeline
trim · digits_only · money→cents
Normalize before you validate. Transforms travel inside the schema, so every runtime cleans data identically.
Full type inference
type T = Infer<typeof schema>
Exact TypeScript types straight from the builder. Optional-aware, enum-aware, zero duplicate definitions.
Performance
Purpose-built beats regex.
Most validators lean on generic regex. wellformed's built-in predicates are purpose-built, so an email or SSN checks in tens of nanoseconds instead of microseconds. That is 10 to 40x faster than Zod in the same V8 runtime, and the Rust runtime goes further with SIMD-friendly byte scanners.
Validation latency, lower is faster. Same V8 runtime, Apple Silicon.
Stop maintaining two copies of the truth.
Define your validation once. Ship it as data. Run it everywhere.