TypeScript → JSON IR → Rust

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.

$npm i wellformed-ts

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);
authorshipvalidateone schema · two runtimes

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.

w.object({ … }).when(…).require(…)author · ts
account.schema.jsonportable ir
runtime boundary
TypeScript
valid

wellformed-ts

Rust
valid

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 Playground
live previewvalidating
Email
ada@example.com
SSN
•••-••-1234
EIN
empty

required 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.

wellformed-tsZod
Email45x faster
61 ns
2.73 µs
URL42x faster
106 ns
4.41 µs
SSN30x faster
85 ns
2.56 µs
Object11x faster
411 ns
4.35 µs

Validation latency, lower is faster. Same V8 runtime, Apple Silicon.

See the full benchmark

Stop maintaining two copies of the truth.

Define your validation once. Ship it as data. Run it everywhere.

$npm i wellformed-ts