zmdbzero-maintenance data layer
Docs Benchmarks Anti-patterns OpenAPI
Docs / Validation and contracts

equals, random & other utilitiesSupported

The rest of @zmdb/validator. Each takes its shape from its type argument, which @zmdb/compiler replaces with emitted code at build time; an untransformed call has nothing to work from and throws. See AOT Setup.

equals / assertEquals — reject unknown keys#

import { equals, assertEquals } from '@zmdb/validator';

interface Config {
  host: string;
  port: number;
}

equals<Config>({ host: 'a', port: 1 }); // true
equals<Config>({ host: 'a', port: 1, prot: 2 }); // false — `prot` is not in the type
assertEquals<Config>(raw); // throws instead of returning false

is and assert ignore extra properties, which is right for an API request you do not control. equals is right when an unknown key means someone made a mistake you should surface — a config file, an internal message, a payload whose sender you own. The typo case is the strongest argument for it: prot: 8080 with is gives you a default port and a confusing afternoon.

random — a value that satisfies a type#

import { random } from '@zmdb/validator';

const user = random<User>();
const body = random<CreateUserRequest>();

Useful for tests and for property-style checks over your own code:

it('serialization round-trips', () => {
  for (let i = 0; i < 100; i++) {
    const u = random<User>();
    expect(parse<User>(stringify<User>(u))).toEqual({ success: true, data: u });
  }
});

The values satisfy the type and any recognised validate() rules. They are not realistic — see Seed Functions if you want data that looks like data.

📝 Note

random<T>() is not seeded — the transformer inlines it over Math.random, so two calls give two values and a failing generated case is not reproducible from the test output. Log the value on failure, or use seedRows / makeRng from @zmdb/orm/seeding, which drive the same sampler from a seed.

validate — errors without an exception#

import { validate } from '@zmdb/validator';

const result = validate<CreateUserRequest>(ctx.body);
if (!result.success) {
  throw new ValidationError('invalid payload', result.errors);
}

Each error carries the path (input.address.zip), the expected type and the value found — which is what makes a 400 response useful to whoever is calling you. Prefer this over catching an assert: a validation failure is an expected outcome of an untrusted input, not an exceptional one.

Choosing between them#

You wantUse
a booleanis
the value or an exceptionassert
every error, as datavalidate
a boolean, extra keys rejectedequals
the value or an exception, extra keys rejectedassertEquals
a value of the typerandom

The pattern at an HTTP boundary#

@Post('/users')
async create(ctx: Ctx<Record<never, string>, unknown>) {
  const result = validate<CreateDTO<User>>(ctx.body);
  if (!result.success) throw new ValidationError('invalid payload', result.errors);
  return this.repo.create(result.data);
}

Typing the body as unknown is deliberate: it makes the validate call the only way to get at it, so the check cannot be skipped by accident.

---

See also: is() · assert() · Seed Functions