The fastest schema with next-gen DX.
Describe your data once, then parse it, validate it, transform it, serialize it back, and turn it into JSON Schema β all from the same definition, all compiled into a single optimized function.
Formerly known as ReScript Schema. It's plain JavaScript β you don't need the ReScript compiler to use it. ReScript users, see the ReScript docs.
npm install suryimport * as S from "sury";
const playerSchema = S.schema({
username: S.string,
xp: S.number,
});
S.parser(playerSchema)({ username: "billie", xp: 100 });
// => { username: "billie", xp: 100 }
S.parser(playerSchema)({ username: "billie", xp: "not a number" });
// => throws S.Error: Failed at ["xp"]: Expected number, received "not a number"
type Player = S.Infer<typeof playerSchema>;
// ^? { username: string; xp: number }The API mirrors TypeScript types, so there's not much new syntax to learn.
Full API reference: JS/TS Β· ReScript Β· PPX
Declare the union, and get parsing, narrowing, and serialization from one definition:
const eventSchema = S.union([
{ type: "user.created", id: S.bigint },
{ type: "user.renamed", id: S.bigint, name: S.string },
{ type: "user.deleted", id: S.bigint },
]);
// Chain schemas to build a pipeline β no JSON.parse in your own code
const parseEvent = S.decoder(S.jsonString, eventSchema);
const event = parseEvent('{"type":"user.renamed","id":"42","name":"Dmitry"}');
// => { type: "user.renamed", id: 42n, name: "Dmitry" }
switch (event.type) {
case "user.renamed":
event.name; // string β TypeScript narrows it for you
break;
}
// The same schema serializes back out, no second definition needed
S.encoder(eventSchema, S.jsonString)(event);
// => '{"type":"user.renamed","id":"42","name":"Dmitry"}'Note that you write id: S.bigint β the type you want to work with. A bigint can't exist in JSON, so Sury infers the "42" β 42n coercion from the input side of the pipeline, in both directions. No as const, no coercion wrappers, no second schema for the wire format.
Errors point at the field inside the matched variant, not at the union as a whole:
parseEvent('{"type":"user.renamed","id":"42"}');
// => throws S.Error: Failed at ["name"]: Expected string, received undefinedparseEvent above isn't an interpreter walking a schema tree at runtime β it's a function Sury generated for exactly this shape. The union dispatches on the discriminant, the inferred bigint coercion is inlined as a bare BigInt() call, and the whole S.jsonString β union β field-level pipeline is fused into one pass:
(i) => {
let v0;
try {
v0 = JSON.parse(i);
} catch (t) {
e[0](i);
}
if (typeof v0 === "object" && v0 && !Array.isArray(v0)) {
if (v0["type"] === "user.created") {
let v2 = v0["id"];
typeof v2 === "string" || e[2](v2);
let v1;
try {
v1 = BigInt(v2);
} catch (_) {
e[1](v2);
}
v0 = { type: v0["type"], id: v1 };
} else if (v0["type"] === "user.renamed") {
// β¦one branch per variant, no loop over union members
} else {
e[8](v0);
}
} else {
e[9](v0);
}
return v0;
};This is why Sury will most likely outperform not only other libraries, but also your own hand-rolled validation logic.
Rename fields, coerce types, and reshape objects β then get the inverse for free:
const userSchema = S.schema({
USER_ID: S.string.with(S.to, S.bigint),
USER_NAME: S.string,
}).with(S.shape, (input) => ({
id: input.USER_ID,
name: input.USER_NAME,
}));
//? S.Schema<{ id: bigint; name: string }, { USER_ID: string; USER_NAME: string }>
S.parser(userSchema)({ USER_ID: "0", USER_NAME: "Dmitry" });
// => { id: 0n, name: "Dmitry" }
S.encoder(userSchema)({ id: 0n, name: "Dmitry" });
// => { USER_ID: "0", USER_NAME: "Dmitry" }Every schema is reversible, so S.reverse hands you a full-featured schema with Input and Output swapped β usable with every operation, not just a serialize shortcut.
S.jsonString above wasn't a special "parse JSON" mode β it's an ordinary schema used as a stage. So is S.json, S.uint8Array, S.date, and every schema you write. Instead of a fixed menu of parseJson / parseJsonString / convertToJson functions, you describe the shape of the data at each step and let Sury compile the path between them.
Stages nest, so any field can be its own pipeline:
const apiUser = S.schema({
// Arrives as JSON text, parsed and validated as an array of addresses
addresses: S.jsonString.with(S.to, S.array(addressSchema)),
// Arrives as a string, mapped to a Date
createdAt: S.string.with(S.to, S.date),
// Element-level transforms work the same way
ids: S.array(S.string.with(S.to, S.bigint)),
});The whole tree β top-level operation plus every nested S.to β still folds into one generated function, so deep pipelines cost nothing at runtime.
Once schemas are stages, layouts that usually need hand-written glue become a single definition. S.compactColumns maps columnar arrays to rows, in both directions:
const cityRow = S.schema({ id: S.bigint, city: S.string });
const rows = S.compactColumns(S.json).with(S.to, S.array(cityRow));
S.parser(rows)([["1", "2"], ["Tbilisi", "Batumi"]]);
// => [{ id: 1n, city: "Tbilisi" }, { id: 2n, city: "Batumi" }]
S.encoder(rows)([{ id: 1n, city: "Tbilisi" }, { id: 2n, city: "Batumi" }]);
// => [["1", "2"], ["Tbilisi", "Batumi"]]Sury's internal representation is JSON Schema-shaped, so conversion is native rather than bolted on β and it's exposed through the Standard JSON Schema extension of the Standard Schema spec, so tools consume it without special-casing Sury.
Because Sury tracks Input and Output separately, it describes both sides of a transformation:
S.enableStandardJSONSchema();
const productSchema = S.schema({
id: S.string,
price: S.string.with(S.to, S.number),
}).with(S.meta, {
description: "A product in the catalog",
examples: [{ id: "p_1", price: 9.99 }],
});
productSchema["~standard"].jsonSchema.input({ target: "draft-2020-12" });
// {
// $schema: "https://json-schema.org/draft/2020-12/schema",
// type: "object",
// properties: { id: { type: "string" }, price: { type: "string" } },
// required: ["id", "price"], β the wire format
// description: "A product in the catalog",
// examples: [{ id: "p_1", price: "9.99" }],
// }
productSchema["~standard"].jsonSchema.output({ target: "draft-2020-12" });
// { β¦ properties: { id: { type: "string" }, price: { type: "number" } }, β¦ }
// β what your code receivesS.meta attaches description, title, examples, and deprecated. You write examples in the Output format you actually work with (price: 9.99), and they're emitted in the Input format the wire uses (price: "9.99") β so a generated OpenAPI document describes the payload a client really sends.
"draft-07", "draft-2020-12", and "openapi-3.0" are all supported targets, and S.toJSONSchema(schema, options) is available directly if you'd rather not go through ~standard.
It reads JSON Schema back in, too:
S.assert(S.fromJSONSchema({ type: "string", format: "email" }), "example.com");
// => throws S.Error: Expected email, received "example.com"Hover any schema and you see the data, not the library's internals:
S.schema({ foo: S.string });
//? S.Schema<{ foo: string }, { foo: string }>Compare that with v.ObjectSchema<{readonly foo: v.StringSchema<undefined>}, undefined>. Both Input and Output are right there, which is what makes a transformation's two sides obvious at a glance rather than something you reconstruct in your head.
S.parser(S.schema({ a: S.array(S.schema({ b: S.string })) }))({
a: [{ b: "x" }, { b: 1 }],
});
// => throws S.Error: Failed at ["a"]["1"]["b"]: Expected string, received 1Every error is an S.Error (err instanceof S.Error), and S.safe / S.safeAsync wrap any block into a typed result if you'd rather not catch:
const result = S.safe(() => S.parser(playerSchema)(data));
if (result.success) result.value;
else result.error;- Works with plain JavaScript, TypeScript, and ReScript β no compiler required
- The fastest parsing and validation library in the JavaScript ecosystem (benchmarks)
- Small JS footprint & tree-shakable API
- Async transformations, recursive schemas, and custom schemas
Use Sury anywhere a schema is accepted:
- tRPC, TanStack Form, TanStack Router, Hono, and 19+ more via the Standard Schema spec
- Anything that speaks JSON Schema, via
S.toJSONSchema/S.fromJSONSchema
- HyperIndex β Envio's blockchain indexing framework, which uses Sury to power native high-performance external calls
- rescript-rest β RPC-like client, contract, and server implementation for a pure REST API
- rescript-envsafe β makes sure you don't accidentally deploy apps with missing or invalid environment variables
- rescript-stripe β describe and manage Stripe billing in a declarative way with code
- Internal form library at Carla
Building something with Sury? Let me know and I'll add it here.
Instead of a few large classes with many methods, Sury's API and source are built from many small, independent functions, each with a single task. A bundler can follow your import statements and drop everything you don't use, which can cut the shipped size by up to 2Γ compared to Zod. (The approach is borrowed from Valibot, which pioneered it.)
At the same time Sury is the fastest composable validation library in the ecosystem, because schemas are compiled to specialized code with new Function rather than interpreted.
Measured against sury@11.0.0-alpha.11, zod@4.4.3, typebox@0.34.52, valibot@1.4.2, arktype@2.2.3:
| Sury | Zod | TypeBox | Valibot | ArkType | |
|---|---|---|---|---|---|
| Total size (min + gzip) | 20.8 kB | 64.7 kB | 31.2 kB | 15.2 kB | 47.9 kB |
| Benchmark size (min + gzip) | 9.88 kB | 19.6 kB | 22.6 kB | 1.30 kB | 47.8 kB |
| Parse with the same schema | 160,549 ops/ms | 8,463 ops/ms | 120,684 ops/ms (no transforms) | 1,328 ops/ms | 77,405 ops/ms |
| Create schema & parse once | 54 ops/ms | 7 ops/ms | 82 ops/ms (no transforms) | 198 ops/ms | 9 ops/ms |
Independent benchmarks and conformance suites that include Sury:
- typescript-runtime-type-benchmarks β throughput across the ecosystem
- schemabenchmarks.dev β per-step breakdown: download, initialization, validation, parsing, Standard Schema, codec
- json-schema-compliance-suite β JSON Schema validation, semantics, and round-trip fidelity
| Sury | Zod | TypeBox | Valibot | ArkType | |
|---|---|---|---|---|---|
| Inferred TS type (what you hover) | S.Schema<{foo: string}, {foo: string}> |
z.ZodObject<{foo: z.ZodString}, $strip> |
TObject<{foo: TString}> |
v.ObjectSchema<{readonly foo: v.StringSchema<undefined>}, undefined> |
Type<{foo: string}, {}> |
| JSON Schema | S.toJSONSchema + S.fromJSONSchema |
z.toJSONSchema |
π | @valibot/to-json-schema |
myType.toJsonSchema() |
| Standard Schema | β | β | β | β | β |
| Codegen-free (doesn't need compiler) | β | β | β | β | β |
| Eval-free | β | β opt-out | β opt-in | β | β opt-out |
| Ecosystem | βοΈβοΈ | βοΈβοΈβοΈβοΈβοΈ | βοΈβοΈβοΈβοΈβοΈ | βοΈβοΈβοΈ | βοΈβοΈ |
Sury's own ecosystem is young, but implementing Standard Schema means the 32+ libraries that support the spec already work with it today.
Yes β that's where the speed comes from. The approach is battle-tested and has no known security issues; it's also how TypeBox, Zod v4, and ArkType work. Even Cloudflare Workers added support for it.
There's currently no eval-free mode, so Sury won't run in environments that forbid dynamic code evaluation, such as pages under a strict CSP without 'unsafe-eval', some browser extension contexts, and a few restricted edge runtimes. If that's your environment, Valibot is the honest recommendation today.
It's short, it's pronounceable, and the 𧬠fits: a schema is the DNA of your data β one definition that everything else is generated from.
- Welcome Sury - The fastest schema with next-gen DX (Dev.to)
- ReScript Schema unique features (Dev.to)
- Building and consuming REST API in ReScript with rescript-rest and Fastify (YouTube)
Bug reports, ideas, and pull requests are all welcome β open an issue to get started.
If you're enjoying Sury and want to give back, that would be rad!
The free ways help a lot too: star the repo, write about it, or tell someone who's picking a validation library this week.
If you'd like to donate, GitHub Sponsors isn't available in my country, so USDT is the easiest route:
- ERC20:
0x509fCF7C24A94a776eb92B56B9DA4aA145615529 - TRC20:
TFg5hKgkdcrFnPHNgYqfbp9yMyx25uaWrF
Your sponsorship doesn't go towards anything specific β it's simply a wonderful way to say "thank you" and make me happy. π
DM me on X/Twitter if you want to be featured or just to say hi! This would mean so much to me. β¨