There are too many JavaScript schema libraries, so support only one

goodoldneon1 pts0 comments

There are too many JavaScript schema libraries, so support only one - Inngest Blog

Open Source6.9KOpen Source6.9K<br>Sign inStart free

Blog ArticleThere are too many JavaScript schema libraries, so support only one<br>Aaron Harper•6/10/2026•10 min read

TypeScript developers have no shortage of schema libraries: Zod, Valibot, ArkType, Yup, Effect Schema, Superstruct, io-ts, Runtypes, TypeBox, Joi, and more.

For library authors who accept user-defined schemas, that abundance used to collapse into three bad options:

Pick one (usually Zod), frustrating everyone who picked something else.

Don't support runtime validation, frustrating everyone who expected it.

Roll your own validation interface and adapters, frustrating yourself.

Now there's a better way: support one schema interface that many popular schema libraries already implement. This is Standard Schema.

We switched to Standard Schema in our TypeScript SDK v4.0.

Is this just another competing standard?

Source: xkcd "Standards".

Standard Schema is not a new schema language, validation library, or syntax users have to learn. Users still write Zod, Valibot, ArkType, Yup, or whatever else they already chose. Standard Schema is only the small compatibility interface those schema objects expose so other libraries can validate unknown input and recover input/output types without knowing which schema library produced them.

That narrowness is important. Standard Schema does not standardize error formatting, metadata, defaults, JSON Schema generation, or schema introspection. If you need those, you still have library-specific work to do. It helped us because our SDK needed the narrow part: validation plus type inference.

What Standard Schema is

At its core, Standard Schema is one property on a schema object:

interface StandardSchemaV1Input, Output = Input> {<br>"~standard": {<br>version: 1;<br>vendor: string;<br>validate(<br>value: unknown,<br>options?: { libraryOptions?: Recordstring, unknown> },<br>):<br>| { value: Output; issues?: undefined }<br>| { issues: readonly Issue[] }<br>| Promise<br>| { value: Output; issues?: undefined }<br>| { issues: readonly Issue[] }<br>>;<br>types?: { input: Input; output: Output };<br>};

That small surface area is the point. Even the property name is designed to stay out of the way: the ~ prefix sorts last in autocomplete and signals "don't touch this directly."

Schema libraries implement it, libraries like ours consume it, and users never see it. Zod, Valibot, ArkType, Yup, Joi, and others already support ~standard. If your library accepts a StandardSchemaV1, you support every schema library that implements the spec without writing per-library adapters.

What our users' code looks like

From the user's perspective, nothing special is happening. They write schemas with their library of choice.

With Zod:

import { eventType } from "inngest";<br>import { z } from "zod";

const userCreated = eventType("user.created", {<br>schema: z.object({ userId: z.string() }),<br>});

With Valibot:

import { eventType } from "inngest";<br>import { object, string } from "valibot";

const userCreated = eventType("user.created", {<br>schema: object({ userId: string() }),<br>});

With ArkType:

import { eventType } from "inngest";<br>import { type } from "arktype";

const userCreated = eventType("user.created", {<br>schema: type({ userId: "string" }),<br>});

In our library code, we just accept StandardSchemaV1. Users keep their schema library, and we avoid maintaining a growing adapter layer.

What we learned

1. Some users want static types without runtime validation

A schema library is overkill if you only want compile-time types. Standard Schema makes this case trivial: use a no-op validator.

export function staticSchemaT extends Recordstring, unknown>>(): StandardSchemaV1T> {<br>return {<br>"~standard": {<br>version: 1,<br>vendor: "inngest",<br>validate: (value) => ({ value: value as T }),<br>},<br>};

Users get static types with no runtime validation cost and no new dependencies:

const userCreated = eventType("user.created", {<br>schema: staticSchema userId: string }>(),<br>});

2. Transforms can make validation non-repeatable

Event-driven systems often need double validation: producers validate before sending (so bad data never enters the system) and consumers validate when receiving (so bad data is not processed). Producers and consumers are often different processes, sometimes different codebases, so each side owns its validation against the same schema.

That works as long as validation leaves the payload unchanged. However, some schema libraries support "transforms", which break the "double validation is safe" assumption. This means that the producer's validation could return modified data that will fail consumer-side validation.

For example, your schema might transform a string into a number. After the first validation, the payload has a number instead of a string, so the second validation fails. Here's a runnable example that calls ~standard directly only to isolate the failure mode. In real Inngest usage, the SDK...

schema validation standard library libraries support

Related Articles