Skip to content

Schema libraries

amqp-contract validates through Standard Schema v1, so any conforming library works. This page compares the three common choices; for how to use schemas, see define a contract.

Comparison

ZodValibotArkType
API styleChainableFunctional, modularType-syntax strings
Bundle sizeLargestSmallest (tree-shakeable)Middle
Validation speedGoodFastestGood
EcosystemLargestGrowingGrowing
Learning curveLowLowMedium

Bundle size rarely matters here — contracts run on a server, not in a browser. Validation speed matters only if profiling puts schema validation on your hot path, which for typical message sizes it will not.

The practical advice: use Zod unless you have a specific reason not to. It has the largest ecosystem, the most examples, and the best-supported AsyncAPI converter. Reach for Valibot when you have measured validation cost and it matters, or when a shared contract package genuinely ships to a browser. Reach for ArkType if you prefer its syntax.

Usage

All three work identically with defineMessage:

typescript
import { defineMessage } from "@amqp-contract/contract";
import { z } from "zod";

defineMessage(z.object({ orderId: z.string(), amount: z.number().positive() }));
typescript
import { defineMessage } from "@amqp-contract/contract";
import * as v from "valibot";

defineMessage(v.object({ orderId: v.string(), amount: v.pipe(v.number(), v.minValue(0)) }));
typescript
import { defineMessage } from "@amqp-contract/contract";
import { type } from "arktype";

defineMessage(type({ orderId: "string", amount: "number>0" }));

Payload types are inferred from whichever you pick; handlers are unaffected by the choice.

Mixing libraries

Nothing stops you using different libraries for different messages in one contract — validation is per message. It is legal but rarely a good idea, since readers then need to know all of them.

AsyncAPI conversion

Generating an AsyncAPI document needs a converter that turns your schema into JSON Schema:

typescript
import { ZodToJsonSchemaConverter } from "@orpc/zod/zod4";

const generator = new AsyncAPIGenerator({
  schemaConverters: [new ZodToJsonSchemaConverter()],
});

Zod's converter is the best supported. Without a converter matching your library, generation fails — failOnMissingConverter defaults to true. Set it to false to generate anyway, degrading payload schemas to a generic { type: "object" } placeholder whose message shapes carry no information. See generate AsyncAPI.

This is the strongest practical argument for Zod: if you want a useful AsyncAPI document, its conversion path is the most complete.

Validation is stricter than types

Worth stating plainly, whichever library you choose. TypeScript checks the shape; the schema checks the values. z.string().email() is a string to the compiler, so an invalid address compiles and then fails validation at runtime.

That gap is deliberate — it is why validation exists in addition to types. See core concepts.

Switching libraries

Because validation is confined to defineMessage, switching is a contract-level change. Handlers, publishers and consumers are untouched as long as the inferred type is the same.

Migrate one message at a time and let the compiler find the drift: if the new schema infers a different type, every affected handler stops compiling.

Where next

Released under the MIT License.