Documentation index
Russian translation
This guide answers the practical question:
Which part of
@modulify/validatorshould I use for taskX?
It is intentionally recipe-oriented. Use it when you already understand the project direction and want a fast path to a concrete implementation.
Use this quick rule of thumb:
- use
@modulify/validator/predicateswhen you only need runtime checks and type guards; - use built-in assertions such as
isString,isDefined,hasLength(...),oneOf(...)when you want machine-readable failures; - use combinators such as
shape(...),each(...),tuple(...),record(...),union(...),discriminatedUnion(...)when validation becomes structural; - use
meta(...)anddescribe(...)when another layer needs stable machine-readable descriptors; - use
toJsonSchema(...)only when you need an interoperability/export view, not as the source of runtime truth.
Use shape(...) plus leaf assertions.
import {
hasLength,
isDefined,
isString,
shape,
validate,
} from '@modulify/validator'
const createUser = shape({
email: [isDefined, isString],
password: [isString, hasLength({ min: 8 })],
}).strict()
const [ok, validated, violations] = validate.sync(input, createUser)Practical pattern:
- use
.strict()for request payloads when unknown keys should be rejected; - keep leaf checks small and composable;
- use the
validatedtuple item inside the success branch; - use
violationsas structured data for API responses, logs, or UI mapping.
Use matches.sync(...).
import {
isDefined,
isString,
matches,
} from '@modulify/validator'
const value: unknown = source()
if (matches.sync(value, [isDefined, isString])) {
value.toUpperCase()
}Use this when you want to narrow the original variable itself.
Do not use validate.sync(...) for this purpose if your main goal is narrowing the original variable. validate.sync(...) narrows the validated tuple item, not the original input binding.
Use the wrapper that matches your runtime meaning:
optional(x)meansundefinedis accepted;nullable(x)meansnullis accepted;nullish(x)means bothnullandundefinedare accepted.
const profile = shape({
nickname: optional(isString),
middleName: nullable(isString),
bio: nullish(isString),
})Practical rule:
- use
optional(...)for omitted or unset fields; - use
nullable(...)whennullis a meaningful explicit value; - use
nullish(...)only when both cases are intentionally allowed.
Build one base shape, then derive from it.
import {
exact,
isString,
optional,
shape,
} from '@modulify/validator'
const account = shape({
id: isString,
nickname: optional(isString),
role: exact('admin'),
}).strict()
const publicAccount = account.pick(['id', 'nickname'])
const editableAccount = account.partial()
const adminAccount = account.extend({ team: isString })Use this pattern when one domain object appears in:
- API payloads;
- form state;
- internal service boundaries;
- public views with a reduced field set.
Remember that structural derivations such as pick(), omit(), partial(), extend(), and merge() intentionally drop object-level rules.
Use meta(...) on any constraint.
import {
isString,
meta,
shape,
} from '@modulify/validator'
const registration = shape({
email: meta(isString, {
title: 'Email',
description: 'Primary login address',
}),
})This is useful when a separate layer needs:
- field titles;
- placeholders or display hints;
- domain-specific metadata for rendering;
- descriptor-driven tooling.
Only some metadata keys are later mapped into JSON Schema. Other keys remain library-specific.
Use collection(...) on top of violations.
import {
collection,
isString,
shape,
validate,
} from '@modulify/validator'
const schema = shape({
profile: shape({
email: isString,
}),
})
const [ok, validated, violations] = validate.sync(input, schema)
const errors = collection(violations)
const rootErrors = errors.at([])
const emailErrors = errors.at(['profile', 'email'])This is the recommended shape when you need exact path lookups instead of string parsing.
Use fieldsMatch(...) for common confirmation cases and refine(...) for everything else.
const registration = shape({
password: isString,
confirmPassword: isString,
}).fieldsMatch(['password', 'confirmPassword'])const registration = shape({
password: isString,
confirmPassword: isString,
}).refine(value => {
return value.password === value.confirmPassword
? []
: [{
path: ['confirmPassword'],
code: 'shape.fields.mismatch',
args: [['password', 'confirmPassword']],
}]
})Choose fieldsMatch(...) when the rule is exactly equality between two selectors.
Choose refine(...) when:
- more than two fields participate;
- the rule is domain-specific;
- the output path needs custom control;
- you want a custom descriptor for introspection.
Use custom(...) and provide describe() if the validator should participate in public introspection.
import { custom } from '@modulify/validator'
const isoDate = custom({
check(value: unknown): value is string {
return typeof value === 'string'
},
run() {
return []
},
describe() {
return {
kind: 'stringFormat' as const,
format: 'iso-date' as const,
}
},
})Without describe(), the validator remains intentionally opaque to describe(...) and toJsonSchema(...).
Use toJsonSchema(...) in two different modes depending on the consumer.
import { toJsonSchema } from '@modulify/validator/json-schema'
const schema = toJsonSchema(profile)
const strictSchema = toJsonSchema(profile, { mode: 'strict' })Use best-effort mode when:
- external consumers can tolerate permissive
{}nodes; - a partial export is better than no export;
- you want the broadest schema view.
Use strict mode when:
- lossy export would be misleading;
- JSON Schema is part of a contract;
- unsupported runtime semantics must fail loudly.
If you are new to the library, the shortest useful reading path is:
README.md01-shape-api.md03-violations.md02-metadata-and-introspection.md04-json-schema-export.md07-ai-reference.mdwhen you want the compact contract summary