Skip to content

Валідація ​

Вхідні дані (тіло HTTP-запиту, query, payload WS-повідомлення та сам конверт WS-повідомлення) перевіряються схемами ArkType (@arktype/type). Ядро сервера (vendor/) працює не з ArkType, а з нейтральним контрактом Validator<T>; конкретну бібліотеку підключає адаптер в app/validate. Загальний порядок обробки запиту описано в Життєвому циклі запиту, поля маршруту — в Маршрутизації та middleware. Перевірку вихідних даних за ResponseSchema описано в Перевірці відповідей.

Коротко на прикладі ​

Задача: маршрут POST /api/notes має приймати лише непорожній title до 200 символів і необов'язковий body, а все інше відхиляти.

  1. Опишіть схему в shared (з туторіалу Як додати фічу):

    ts
    export const CreateNoteInputSchema = type({
        title: '1 <= string <= 200',
        'body?': 'string <= 5000',
        '+': 'reject', // зайві поля — помилка
    })
  2. Передайте її маршруту як validator (для query — queryValidator):

    ts
    defineRoute({ url: '/', method: 'post', handler: ..., validator: CreateNoteInputSchema })
  3. У контролері візьміть уже перевірені дані: 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:

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 : unknown
  • validate синхронний і не кидає винятків — повертає результат-об'єднання.

  • 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:

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:

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 і викликається в onMessage ws-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):

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:

json
{
    "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).

Рецепт: валідація для нового маршруту ​

  1. Опишіть схему в packages/shared/src/schemas/<домен>/ через type({...}) з '+': 'reject' і експортуйте її разом з типом; додайте в index.ts пакета.
  2. Передайте схему в validator (і/або queryValidator) у defineRoute/defineWsRoute — не обгортайте вручну.
  3. Типізуйте handler: тип payload виводиться автоматично; для окремо оголошених обробників — InferQuery<typeof Schema> або тип зі схеми.
  4. Перевірте 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).

Автор: Олександр Блажейко · blazheiko@gmail.com