Оглавление документации
English version
shape(...) — это переиспользуемый API объектных схем в @modulify/validator.
Он сохраняет тот же runtime validation kernel, что и остальная библиотека, но добавляет небольшой неизменяемый object-oriented слой для:
- валидации вложенных объектов;
- переиспользуемых object schema;
- производных схем вроде
pick(...),omit(...)иpartial(...); - object-level rules вроде
refine(...)иfieldsMatch(...); - descriptor-based introspection через
describe(...).
import {
exact,
isString,
optional,
shape,
validate,
} from '@modulify/validator'
const profile = shape({
id: isString,
nickname: optional(isString),
role: exact('admin'),
}).strict()
const [ok, validated, violations] = validate.sync({
id: 'u1',
nickname: 'neo',
role: 'admin',
}, profile)shape(...) возвращает обычный validator, поэтому его можно использовать везде, где принимается regular constraint.
Shape по-прежнему остаётся validator-ом. Дополнительный API — это тонкая обёртка поверх object descriptor и небольшого набора immutable helpers для derivation.
Каждое поле в descriptor может содержать:
- один constraint;
- массив constraint-ов, которые выполняются последовательно;
- другой structural validator вроде
shape(...),each(...),tuple(...),record(...),union(...)илиdiscriminatedUnion(...).
Поэтому object validation остаётся согласованной с остальной библиотекой:
- field-level checks переиспользуют те же assertions и combinators;
- violations сохраняют обычные вложенные
path; - introspection идёт через тот же контракт
describe(...); - metadata остаётся явной через
meta(...).
import {
hasLength,
isDefined,
isString,
shape,
} from '@modulify/validator'
const registration = shape({
email: [isDefined, isString],
password: [isString, hasLength({ min: 8 })],
})Runtime behavior:
- входное значение должно быть plain record-like object;
- каждое объявленное поле валидируется по своему slot;
- вложенные violations возвращаются на пути поля;
- unknown keys по умолчанию разрешены.
У shape есть два режима работы с неизвестными ключами:
'passthrough'— лишние ключи разрешены;'strict'— лишние ключи создаютshape.unknown-keyviolations на пути лишнего ключа.
shape(...) по умолчанию создаётся в режиме 'passthrough'.
const profile = shape({
id: isString,
})
const strictProfile = profile.strict()
const permissiveProfile = strictProfile.passthrough()Важно:
strict()иpassthrough()сохраняют тот же field descriptor;strict()иpassthrough()сохраняют существующие object-level rules;- меняется только поведение unknown keys.
У экземпляров shape есть:
descriptor— текущий object descriptor;unknownKeys— текущий режим unknown keys;- стандартное validator-поведение через
check(...)иrun(...).
Это удобно, когда одну и ту же shape нужно валидировать, описывать через describe(...) и затем выводить производные схемы без ручной пересборки.
Все helpers у shape immutable. Каждый вызов возвращает новую shape.
Используются, когда нужен поднабор текущей object schema.
const profile = shape({
id: isString,
nickname: optional(isString),
role: exact('admin'),
})
const publicProfile = profile.pick(['id', 'nickname'])
const internalProfile = profile.omit(['nickname'])partial() оборачивает каждое поле в optional(...).
const editableProfile = profile.partial()Это соответствует текущей модели библиотеки, в которой:
- отсутствующий ключ;
- и ключ со значением
undefined;
во время валидации трактуются одинаково.
extend(...) добавляет или переопределяет поля через обычный object descriptor.
const account = profile.extend({
team: isString,
})merge(...) объединяет текущую shape с другой shape.
const account = profile.merge(shape({
team: isString,
role: exact('editor'),
}))Поведение:
- unknown-key mode берётся у receiver shape;
- пересекающиеся ключи переопределяются правой shape;
- object-level rules из merge target не пытаются автоматически объединяться в новый общий набор.
Есть намеренное различие между переключением режима и структурным derivation.
strict() и passthrough() сохраняют object-level rules, потому что структура shape по сути остаётся той же.
pick(), omit(), partial(), extend() и merge() намеренно сбрасывают object-level rules, потому что generic refinements непрозрачны и могут зависеть от полей, которых больше нет или которые теперь ведут себя иначе.
Так derived schemas остаются предсказуемыми и библиотека не делает сомнительных предположений о валидности старых refinements.
Shapes позволяют выражать cross-field invariants без внедрения второй schema language.
refine(...) добавляет синхронное object-level rule, которое запускается только после того, как базовая shape уже успешно провалидировалась как объект.
const registration = shape({
password: isString,
confirmPassword: isString,
}).refine(value => {
return value.password === value.confirmPassword
? []
: [{
path: ['confirmPassword'],
code: 'shape.fields.mismatch',
args: [['password', 'confirmPassword']],
}]
})refine(...) специально остаётся тонким:
- он только sync;
- при успехе возвращает
[],nullилиundefined; - при ошибке возвращает один issue или массив issue;
pathзадаётся относительно текущей shape и по умолчанию равен[];valueнеобязателен и по умолчанию берётся из значения объекта по этому относительному пути;codeостаётся machine-readable.
Сгенерированные violations используют:
violates.kind === 'validator';violates.name === 'shape'.
Сами callbacks не сериализуются, но для rule можно добавить компактный machine-readable descriptor:
const registration = shape({
password: isString,
confirmPassword: isString,
}).refine(value => {
return value.password === value.confirmPassword
? []
: [{ path: ['confirmPassword'], code: 'shape.fields.mismatch' }]
}, {
kind: 'passwordConfirmation',
metadata: {
fields: ['password', 'confirmPassword'],
},
})Позже это попадает в describe(...) в массив rules.
fieldsMatch(...) — небольшой helper для частого случая с полем подтверждения.
const registration = shape({
password: isString,
confirmPassword: isString,
}).fieldsMatch(['password', 'confirmPassword'])Он также поддерживает nested selectors:
const registration = shape({
password: isString,
confirm: shape({
password: isString,
}),
}).fieldsMatch(['password', ['confirm', 'password']])На верхнем уровне валидация shape устроена так:
- Вход должен быть plain record-like object.
- Каждое объявленное поле валидируется рекурсивно.
- В режиме
'strict'выполняются проверки неизвестных ключей. - Object-level rules запускаются только если structural validation до этого полностью прошла.
Этот порядок важен, потому что object-level rules тогда работают уже с устойчивой провалидированной формой объекта.
Shape можно свободно вкладывать друг в друга, потому что это обычные validators.
const form = shape({
profile: shape({
email: isString,
nickname: optional(isString),
}).strict(),
})Вложенные ошибки сохраняют точные пути вроде ['profile', 'email'].
Shape участвуют в публичном descriptor tree, который возвращает describe(...).
import { describe } from '@modulify/validator'
const descriptor = describe(shape({
email: isString,
}).strict())Shape descriptors содержат:
unknownKeys;fields;rules;- опциональную
metadata.
Это полезно для adapters, tooling и производных export layers, например для JSON Schema.
meta(...) может аннотировать и shape целиком, и отдельные поля, не меняя validation semantics.
import {
describe,
meta,
} from '@modulify/validator'
const profile = meta(shape({
email: meta(isString, {
title: 'Email',
format: 'email',
}),
}).strict(), {
title: 'Profile',
})
const descriptor = describe(profile)Metadata остаётся полностью явной:
- никакого implicit inheritance по дереву нет;
- metadata shape и metadata полей появляются ровно там, где был вызван
meta(...).
shape(...)ориентирован на объекты, а не на schema-first подход;- он переиспользует те же constraints, что и остальная библиотека;
- derived helpers намеренно маленькие и предсказуемые;
- object-level rules выразительны, но остаются лёгкими;
- расхождения между runtime semantics и внешними форматами схем лучше отражать явно через adapter layers вроде
describe(...)илиtoJsonSchema(...).