Валідація
Вхідні дані (тіло HTTP-запиту, query, payload WS-повідомлення та сам конверт WS-повідомлення) перевіряються схемами ArkType (@arktype/type). Ядро сервера (vendor/) працює не з ArkType, а з нейтральним контрактом Validator<T>; конкретну бібліотеку підключає адаптер в app/validate. Загальний порядок обробки запиту описано в Життєвому циклі запиту, поля маршруту — в Маршрутизації та middleware. Перевірку вихідних даних за ResponseSchema описано в Перевірці відповідей.
Коротко на прикладі
Задача: маршрут POST /api/notes має приймати лише непорожній title до 200 символів і необов'язковий body, а все інше відхиляти.
Опишіть схему в
shared(з туторіалу Як додати фічу):tsexport const CreateNoteInputSchema = type({ title: '1 <= string <= 200', 'body?': 'string <= 5000', '+': 'reject', // зайві поля — помилка })Передайте її маршруту як
validator(для query —queryValidator):tsdefineRoute({ url: '/', method: 'post', handler: ..., validator: CreateNoteInputSchema })У контролері візьміть уже перевірені дані:
getTypedPayload(context)повертаєCreateNoteInput.
Результат: некоректний запит не доходить до контролера.
| Тіло запиту | Відповідь |
|---|---|
{"title":"Купити молоко"} | 200, нотатку створено |
{"title":""} | 422, "messages":["title must be non-empty"] |
{"title":"x","done":true} | 422, "messages":["done must be removed"] |
Перевірка форми даних (тип, довжина, зайві поля) — у схемі. Перевірка змісту (чи існує запис, чи можна користувачу) — у сервісі, див. Перший маршрут. Нижче описано, як це влаштовано всередині.
Контракт Validator<T>
vendor/contracts/validator.ts:
// One problem of a field; `path` is dot-separated (`nested.name`), `''` for the root.
export interface ValidationIssue {
path: string
message: string
}
export type ValidationResult<T> =
{ ok: true; value: T } | { ok: false; messages: string[]; issues?: ValidationIssue[] }
export interface Validator<T> {
validate(input: unknown): ValidationResult<T>
describe?(): unknown
}
export type InferPayload<V> = V extends Validator<infer T> ? T : unknownvalidateсинхронний і не кидає винятків — повертає результат-об'єднання.describeнеобов'язковий: повертає схему для документації маршрутів (GET /<prefix>/doc/routesі api-playground). serialize-routes.ts розуміє три форми результату:- тип ArkType — перетворює serialize-ark-schema.ts;
- готовий опис
{ expression: string, fields?: { name, type, required }[], jsonSchema? }— передається як є, некоректні елементиfieldsвідкидаються; - об'єкт JSON Schema (має
type,properties,anyOf,oneOf,allOf,$ref,enumабоconst) —fieldsбереться зpropertiesверхнього рівня іrequired,expression— зtype, сама схема йде вjsonSchema.
Інший результат або відсутній
describe— маршрут серіалізується безinputSchema/querySchema.
ArkType-адаптер
app/validate/adapters/arktype-adapter.ts — arkValidator(schema) викликає схему, і якщо результат — type.errors, повертає { ok: false, messages, issues }: issues — по запису { path, message } на кожну помилку ArkType (path — propString, наприклад nested.name; '' для кореня), messages — ті самі message рядками. Власний адаптер може не повертати issues, тоді в помилці буде лише messages. describe() повертає саму схему.
Активний движок фіксується одним рядком у app/validate/index.ts:
export { arkValidator as defaultValidator, arkValidator } from './adapters/arktype-adapter.js'Як маршрут отримує валідатор
У маршрутах передається схема, а не валідатор: defineRoute / defineWsRoute самі загортають її через defaultValidator (define-route.ts, define-ws-route.ts). Вручну обгортати arkValidator(...) у файлах маршрутів не потрібно.
validator— схема тіла (HTTP) або payload (WS). На HTTP він дозволений лише дляpost,putіpatch; наget/deleteдає помилку типів і помилку на старті, бо тіло там не читається (Методи і тіло запиту). БезContent-Typeі без тіла валідується{}, тож handler зvalidatorне отримуєpayload = null(крімoctet-маршрутів).queryValidator— схема query (лише HTTP). До неїdefineRouteзастосовуєonUndeclaredKey("reject")(зайві ключі → помилка) або, зqueryAllowExtra: true,onUndeclaredKey("delete")(зайві ключі видаляються).queryArraysперелічує ключі, які можуть повторюватися (збираються вstring[]); див. flatten-query.ts.
Приклад з http-routes.ts:
defineRoute({
url: '/login',
method: 'post',
handler: AuthController.login.bind(AuthController),
validator: LoginInputSchema,
ResponseSchema: ResponseSchemas.LoginResponseSchema,
description: 'Login a user by email or phone identifier',
})Виведення типів
defineRoute виводить тип payload з Type<T> схеми тіла, а тип query — зі схеми query (або null, якщо queryValidator не задано). Якщо задано ResponseSchema, результат хендлера звіряється з її вхідним типом (inferIn) — див. Тип відповіді хендлера. Для обробників, оголошених окремо (методи контролерів), є хелпер InferQuery<typeof Schema> в app/validate/index.ts; на рівні контракту — InferPayload<V>. У shared-схемах тип зазвичай експортується поруч зі схемою через typeof Schema.infer / ["inferIn"].
Де виконується валідація
Усі шляхи зводяться до validateRoutePayload: виклик validator.validate, а при ok: false — throw new ValidationError(messages, undefined, issues).
- HTTP query —
validateQueryу server.ts, до читання тіла. БезqueryValidator→httpData.query === null. - HTTP тіло —
readAndParseBodyу тому ж файлі: після парсингу, якщо єroute.validatorі payload неnull. Без валідатора payload стаєnull(див. Життєвий цикл). - WS конверт — ws-message-validator.ts (
{ payload, event, timestamp }) реєструється в app/start/index.ts черезregisterWsMessageValidatorі викликається вonMessagews-handlers.ts. - WS payload — у ws-api-dispatcher.ts після rate limit, до middleware. Деталі — WebSocket.
Схеми
| Місце | Призначення |
|---|---|
| packages/shared/src/schemas | основні схеми вводу (auth, main, support, push-subscription, query, ws…), спільні для frontend і backend |
Приклад схеми (shared/src/schemas/auth/register-email.ts):
import { type } from '@arktype/type'
export const RegisterEmailInputSchema = type({
name: 'string >= 1 & string <= 100',
email: 'string.email & string <= 255',
password: 'string >= 8 & string <= 32',
token: 'string <= 60',
'+': 'reject',
})
export type RegisterEmailInput = typeof RegisterEmailInputSchema.infer'+': 'reject' забороняє невідомі ключі. Query-схеми приходять рядками, тому використовують морфи-парсери, напр. type('string.integer.parse').pipe(type('number > 0')) у query/common.ts; значення за замовчуванням задає .default('50') (раніше для цього були касти кортежів [схема, '=', '50'], які @arktype/type 2.2.9 більше не потребує), а те, що схема виводить для обробника, зафіксовано в type-checks.ts і порівнюється під час компіляції.
Checkers: заголовки, cookies, параметри
app/validate/checkers — не схеми, а regex/довжина для «сирого» транспорту. Використовуються в http-request-handlers.ts:
| Checker | Правила | Поведінка при порушенні |
|---|---|---|
validateHeader | ім'я ^[a-zA-Z0-9\-_]+$, 1–256 символів; значення — друковані ASCII і таб, ≤ 3072 | заголовок мовчки відкидається (getHeaders) |
validateCookie | ім'я/значення за regex, ліміт appConfig.reasonableCookieLimit, коректне percent-кодування | cookie мовчки відкидається (parseCookies) |
validateParameter | ^[a-zA-Z0-9\-_./]*$, ≤ 256 символів | ParameterValidationError → 400 bad_request |
Формат помилок
ValidationError має code = "validation_failed", statusCode = 422, messages: string[] і issues?: ValidationIssue[].
HTTP-відповідь (обробник помилок у server.ts) — статус 422:
{
"status": "error",
"code": "validation_failed",
"message": "Validation failure",
"details": {
"kind": "validation",
"messages": [
"email must be an email address (was \"x\")",
"password must be at least length 8 (was 3)"
],
"issues": [
{ "path": "email", "message": "email must be an email address (was \"x\")" },
{ "path": "password", "message": "password must be at least length 8 (was 3)" }
]
}
}messages — по рядку на кожне поле (раніше — один рядок summary з усіма помилками); клієнти, що показують messages, працюють як і раніше. Для підсвічування полів у формі беріть issues[].path. Тип — ValidationErrorDetails у shared/src/errors/error-codes.ts.
Помилка параметра шляху — статус 400, code: "bad_request", details.kind: "parameter_validation", details.parameter.
У WS (і для конверта, і для payload) повертається status: "error" з конвертом error, створеним createWsErrorEnvelope("validation_failed", "Validation failure", { details: { kind: "validation", messages, issues } }) — ті самі messages і issues, що й у HTTP. HTTP-статусу немає. Формат конверта — див. WebSocket.
Схеми відповідей (ResponseSchema)
ResponseSchema (тип BaseType з ArkType у types.d.ts) використовується двічі. serialize-routes.ts перетворює його на outputSchema для doc-сторінки маршрутів. А router.ts при старті будує з нього responseCheck — перевірку результату контролера під час виконання; реакцію на порушення задає RESPONSE_VALIDATION_CONTRACT (strict / log). Перевіряється об'єкт до JSON-серіалізації; подробиці — Перевірка відповідей. Схеми відповідей беруться з shared/responses.
Що лишилося ArkType-специфічним
Рішення про абстракцію валідатора записане в ADR 0001 і реалізоване: vendor/start/server.ts і ws-api-dispatcher.ts працюють тільки з Validator<T>. Свідомо прив'язаними до ArkType лишаються:
ResponseSchema?: BaseTypeуvendor/types/types.d.ts;- перевірка відповідей під час виконання (validate-response.ts) викликає схему напряму й розпізнає помилки через
type.errors, а не черезValidator<T>; defineRoute/defineWsRouteіInferQueryтипізовані черезTypeз@arktype/type;- doc-серіалізація
ResponseSchema(outputSchema) — лише ArkType; схемиvalidator/queryValidatorприймаються й від інших движків черезdescribe()(див. контракт); - схема WS-конверта в
ws-message-validator.ts.
План
Не реалізовано з початкового плану (див. ADR 0001): експорт InputSchema з app/validate/index.ts (§3.6); демонстраційний nativeValidator(predicate) (§9); перенесення RouteConfig/ResponseSchema з vendor/types в app/routing/types.ts (§5.6, окремий PR).
Рецепт: валідація для нового маршруту
- Опишіть схему в
packages/shared/src/schemas/<домен>/черезtype({...})з'+': 'reject'і експортуйте її разом з типом; додайте вindex.tsпакета. - Передайте схему в
validator(і/абоqueryValidator) уdefineRoute/defineWsRoute— не обгортайте вручну. - Типізуйте handler: тип payload виводиться автоматично; для окремо оголошених обробників —
InferQuery<typeof Schema>або тип зі схеми. - Перевірте 422 з
validation_failedна невалідному тілі. Повний сценарій — Додавання фічі.
Заміна бібліотеки валідації
Код це підтримує лише як глобальну заміну: новий адаптер, що реалізує Validator<T>, і зміна експорту defaultValidator в app/validate/index.ts. Але доведеться також переписати всі схеми в shared, типи defineRoute/defineWsRoute/InferQuery (вони на Type ArkType), ws-message-validator.ts і doc-серіалізацію. Проєкт використовує один валідатор: маршрути передають сиру схему (validator: SomeSchema), обгортку робить defineRoute, а змішувати движки по маршрутах не можна (ADR 0001).