zmdbzero-maintenance data layer
Docs Benchmarks Anti-patterns OpenAPI
Docs / Ecosystem integrations

ValibotSupported

Valibot's pitch is bundle size: a pipeline of tree-shakeable functions instead of a class-based schema. It ends up architecturally close to Zod, and the comparison with zmdb is the same one — schema-as-value versus type-as-schema.

// Valibot
const User = v.object({ id: v.number(), email: v.pipe(v.string(), v.email()) });
type User = v.InferOutput<typeof User>;
const parsed = v.parse(User, body);

// zmdb
interface User {
  id: number;
  email: string;
}
const parsed = assert<User>(body);

Bundle size#

Valibot's advantage over Zod is real: you import the validators you use, so a small schema pulls in a small amount of code.

zmdb's position is different rather than strictly better:

If bundle size is the deciding factor, measure your actual types rather than trusting either claim. The benchmarks cover throughput, not bytes.

Where zmdb wins outright#

Valibot still needs the schema written twice in effect — once as a pipeline, once as the type you get back from InferOutput. It reads as one declaration, but any type you already have (from an OpenAPI generator, a shared package, a database schema) must be re-expressed as a pipeline to validate it. assert<T> takes the type you have.

Where Valibot wins#

Using both#

The sensible split is the same as with Zod: Valibot for anything defined at runtime, zmdb for your own fixed DTOs.

const dto = assert<CreatePostDto>(body); // fixed shape
const custom = v.parse(buildPipeline(tenantConfig), extra); // tenant-defined shape

Mapping the API#

Valibotzmdb
v.parse(S, x)assert<T>(x)
v.safeParse(S, x)validate<T>(x)
v.is(S, x)is<T>(x)
v.pipe(S, v.check(fn))refine
v.pipe(S, v.transform(fn))transform
v.union([...])union
v.variant('kind', [...])discriminated
v.strictObject / v.looseObject`validateObject(x, 'strict' \'passthrough')`
v.optional(S)`T \undefined` in the type

validateObject also has a 'strip' mode, which drops unknown keys instead of accepting or rejecting them — the right choice for a public API where extra fields should not be persisted. See Object Modes.

Migrating#

Same three steps as the Zod migration: keep the inferred type as a real interface, swap parse/safeParse for assert/validate, and add the transformer canary first — without it the swap replaces working validation with unconditional success.

it('the transformer is running', () => {
  expect(is<{ id: number }>({ id: 'x' })).toBe(false);
});

---

See also: Zod · Unions · Object Modes