Validation logic, as data.
Author schemas once in TypeScript, compile them to a portable JSON IR, and run the exact same rules in TypeScript and Rust.
Validation rules are usually written as code, locked inside one app. But the form they validate often isn't. It's defined in a form builder, configured per customer, stored in a database, submitted as JSON, and re-validated by a backend in another language. Code can't go where the form goes, so every surface reimplements the rules by hand and the copies drift out of sync.
The same rules have to hold everywhere the form travels:
- Form builders define fields at runtime; the rules live with the form, not hardcoded in your app.
- Web & mobile show inline field errors as the user types.
- PDFs & documents need fields validated before you fill or accept them.
- Any backend validates submitted JSON in Node, Rust, or a worker before trusting the shape.
- AI agents generate and inspect rules as data, not opaque closures.
wellformed compiles your validation to one portable JSON definition. Store it with the form, load it at runtime, and run the exact same contract in TypeScript and Rust.
- One schema, every runtime. Validation rules serialize to JSON instead of living in opaque JavaScript closures. The same IR validates identically in JavaScript, TypeScript, and Rust. No backend rewrite, no drift.
- Zod-like authoring. A familiar chained builder:
w.string().trim().minLen(1).email(), with full type inference viaInfer<typeof schema>. - 60+ domain predicates. SSN, EIN, ITIN, CUSIP, ABA routing, IBAN, ICD-10, dates, phones, and more. Purpose-built validators, not generic regex.
- Cross-field rules as data. Conditional requirements, mutual exclusion, and
comparisons:
.when("kind").equals("business").require("ein"). - Transform pipeline. Normalize before you validate (trim, digits-only, money-to-cents, date parsing). Transforms travel inside the schema, so every runtime cleans data the same way.
- Fast. Built-in predicates validate in tens of nanoseconds and stay competitive with the fastest validation libraries, while keeping schemas portable across TypeScript and Rust.
Steady-state validation, lower is better. The Rust numbers are dynamic JSON
validation with wellformed::validate(&schema, &mut serde_json::Value), not
generated typed Rust.
| Case | Rust dynamic JSON | TypeScript | Valibot | Zod |
|---|---|---|---|---|
| ~58 ns | ~61 ns | ~53 ns | ~2.73 us | |
| HTTP URL | ~58 ns | ~106 ns | ~1.31 us | ~4.41 us |
| UUID | ~76 ns | ~77 ns | ~82 ns | ~2.82 us |
| Object | ~544 ns | ~411 ns | ~517 ns | ~4.35 us |
See the full methodology and benchmark table.
npm install wellformed-ts # TypeScript / JavaScript
cargo add wellformed serde_json # Rustimport { validate, w, type Infer } from "wellformed-ts";
const taxpayer = w
.object({
kind: w.enum(["individual", "business"] as const),
name: w.string().trim().minLen(1),
ssn: w.string().digitsOnly().ssn().optional(),
ein: w.string().digitsOnly().ein().optional(),
})
.when("kind").equals("individual").require("ssn")
.when("kind").equals("business").require("ein")
.mutuallyExclusive("ssn", "ein");
type Taxpayer = Infer<typeof taxpayer>;
const result = validate(taxpayer.toSchema("1.0"), {
kind: "individual",
name: " Ada Lovelace ",
ssn: "123-45-6789",
});
if (result.valid) {
console.log((result.value as Taxpayer).name); // "Ada Lovelace" (trimmed)
}TypeScript builder ──► JSON IR ──┬──► TypeScript validate()
└──► Rust validate()
Schemas compile to a portable JSON intermediate representation. Store it in a
database, diff it in code review, send it over the wire, and evaluate it in any
runtime. The same JSON your frontend authored deserializes into the Rust
wellformed crate and validates with identical results.
Read the IR reference.
wellformed/is the Rust core crate: IR types, runtime validation, transforms, predicates, and codegen.wellformed-macros/provides Rust proc macros for schema-driven code generation.wellformed-validate/holds low-level validation primitives and benchmark helpers.typescript/packages/wellformed/is the TypeScript builder API, IR types, runtime, forms helpers, and tests.typescript/apps/docs/is the documentation site and playground.
wellformed is preparing for its first public production users. Treat it as
pre-1.0: pin exact versions, read the changelog before upgrading, and expect
compatibility notes for public API changes. Do not put sensitive personal data
in public issues or examples; report vulnerabilities privately per
SECURITY.md.
Prerequisites:
- Rust stable toolchain (published crates support a minimum of Rust 1.93.0)
cargo-audit(cargo install cargo-audit --locked)- Node.js 22 LTS (
nvm use) - pnpm 9+
Run the core checks:
bash scripts/release-preflight.shRun the docs site and playground locally:
cd typescript
pnpm --filter @wellformed/docs dev- Prefer stable machine-readable error
codevalues over parsing human-readablemessagetext. - Decide explicitly how objects handle unknown keys:
.strict(),.strip(),.passthrough(), or.catchall(...). - Keep custom predicates registered consistently in every runtime that evaluates the same IR.
- Store serialized schemas with a version and migration metadata.
- In Rust, use
wellformed!to embed a schema as a value for native validation, orform_schema!for a namespaced facade with typed values, field metadata, and framework-neutralFormState.
The TypeScript package ships skills.md, a compact agent guide for integrating
wellformed into applications. The docs site exposes
/llms-full.txt for full-documentation
context and /docs/<path>.md markdown twins for individual pages.
See CONTRIBUTING.md for local setup, pull request
expectations, and release note requirements.
MIT. See LICENSE.