---
title: "Валідація даних"
description: "Архітектура валідації вхідних даних Ring Platform — схеми Zod, захист на межі маршруту, перевірка HMAC webhook-ів і безпека бізнес-даних для засновників та розробників"
locale: "uk"
---
# Валідація даних

> **Info**
> Використовуйте вкладки **Founder** / **Developer** на бічній панелі документації. Засновники: дізнайтеся, як валідація захищає ваші бізнес-дані. Розробники: отримайте повний довідник патернів Zod із реальними прикладами кодової бази.

Кожен API-виклик до вашого клону Ring — це контракт: клієнт обіцяє передати коректно сформовані дані, а сервер обіцяє безпечно їх обробити. **Валідація даних** — це спосіб, у який Ring Platform забезпечує виконання цього контракту: відхиляє некоректні вхідні дані до того, як вони зможуть пошкодити записи, створити фантомні замовлення або обійти перевірку платежу.

## Шари валідації на одному погляді

|| Шар | Що виявляє | Хто відповідає |
||-------|----------------|-------------|
|| **Межа маршруту** | Відсутні поля, неправильні типи, некоректний JSON | Схеми Zod в обробниках маршрутів `/app/api/**` |
|| **Межа сервісу** | Порушення бізнес-правил (неправильна роль, відсутнє право власності) | Доменно-орієнтовані сервіси у `features/*/services/` |
|| **Межа бази даних** | Порушення обмежень (NOT NULL, UNIQUE, CHECK) | Обмеження PostgreSQL у `data/schema.sql` |

Кожен шар додає вузьку гарантію. Жоден окремий шар не замінює інші.

### For founders

## Чому валідація захищає ваш бізнес

### Ціна поганих даних

Без валідації некоректний API-запит може:

- **Створити фантомні записи** — можливість без назви з’являється в результатах пошуку
- **Пошкодити платіжні записи** — webhook із підробленим підписом запускає повернення коштів
- **Порушити роботу користувацького інтерфейсу** — недійсні налаштування призводять до падіння сторінки параметрів
- **Розкрити конфіденційні дані** — відсутня перевірка видимості відкриває оголошення кімнати угоди

Тришарова валідація Ring Platform виявляє це до того, як дані потраплять у вашу базу.

### Що це означає для вашого клону

Виконуючи **ringize** розгортання, ви успадковуєте валідацію, яка:

- **Відхиляє неповні профілі сутностей** — ім’я, тип, розташування та видимість потрібні до створення запису
- **Перевіряє платіжні webhook-и** — HMAC-підписи WayForPay перевіряються до будь-якої зміни статусу замовлення
- **Захищає конфіденційні рівні** — валідація ролі відбувається до повернення конфіденційних даних сутності/можливості
- **Запобігає ін’єкції налаштувань** — параметри користувача перевіряються за відомою схемою, тому невідомі поля не можуть непомітно проникнути

### Бізнес-сценарії

  
- **[Підроблення платіжного webhook](/docs/architecture/payment-conductor.md)** — Зловмисник надсилає фальшивий webhook «платіж завершено». Ring спочатку перевіряє HMAC-підпис — якщо він не збігається, webhook відхиляється до будь-якої зміни статусу замовлення.

  
- **[Створення можливості](/docs/features/opportunities.md)** — Клієнт надсилає можливість без назви або категорії. Zod відхиляє її на межі маршруту з чітким повідомленням про помилку — фантомний запис не створюється.

  
- **[Налаштування користувача](/docs/features/authentication.md)** — Шкідливий клієнт намагається додати невідомі поля до налаштувань. Схема Zod вилучає невідомі ключі — дозволені лише locale, currency і theme.

  
- **[Конфіденційна кімната угоди](/docs/features/security.md)** — Підписник намагається отримати доступ до конфіденційних можливостей. Захист маршруту перевіряє роль до виконання запиту — база даних ніколи не бачить неавторизованого запиту.

  Валідація — це не лише питання для розробників: вона безпосередньо захищає ваші доходи, довіру користувачів і стан відповідності вимогам. Кожен клон Ring постачається з цими захисними механізмами, увімкненими за замовчуванням.

### For developers

## Архітектура схем Zod

Ring Platform використовує [Zod](https://zod.dev) як єдину бібліотеку валідації в усіх API-маршрутах, Server Actions і webhook-обробниках. Схеми слугують і runtime-валідаторами, і генераторами типів TypeScript через `z.infer<>`.

### Розташування схем (SSOT)

|| Розташування | Призначення | Приклади |
||----------|---------|---------|
|| `lib/zod/` | Спільні схеми між функціями | `credit-schemas.ts`, `store-product.ts`, `desk-schemas.ts`, `airdrop-schemas.ts` |
|| `features/*/types/` | Схеми, специфічні для функції | `features/opportunities/types/`, `features/news/types/` |
|| `app/api/**/route.ts` | Локальні схеми маршруту | Вбудовані схеми для MCP- і webhook-маршрутів |

### Канонічний патерн межі маршруту

Кожен API-маршрут дотримується цього патерну — розібрати, перевірити, а потім використати типізовані дані:

{`import { z } from 'zod'

const createEntitySchema = z.object({
  name: z.string().min(1, 'name is required'),
  shortDescription: z.string().min(1, 'shortDescription is required'),
  type: z.string().min(1, 'entity type is required'),
  location: z.string().min(1, 'location is required'),
  visibility: z.enum(['public', 'subscriber', 'member', 'confidential']),
  isConfidential: z.boolean(),
}).passthrough()

export async function POST(request: NextRequest) {
  const raw = await request.json()
  const parsed = createEntitySchema.safeParse(raw)

  if (!parsed.success) {
    return NextResponse.json(
      { error: parsed.error.issues[0]?.message ?? 'Invalid request body' },
      { status: 400 },
    )
  }

  // parsed.data is now fully typed — no 'as any' needed
  const entity = await createEntity(parsed.data)
  return NextResponse.json(entity, { status: 201 })
}`}

  Ніколи не використовуйте `body as any`, щоб обійти TypeScript під час виклику сервісних функцій. Якщо сервіс очікує певний тип, створіть схему Zod, яка перевіряє форму, а потім приведіть перевірений результат: `parsed.data as Parameters[0]`. Валідація Zod гарантує безпечність такого приведення.

### z.preprocess для нормалізації webhook

Платіжні webhook-и (WayForPay, Stripe) надходять у різних форматах. `z.preprocess` нормалізує необроблене тіло перед валідацією — але для webhook-ів із перевіреним HMAC **ніколи не перетворюйте значення** (HMAC використовує `Object.values()` для необробленого payload):

{`function normalizeWayforPayBody(raw: unknown): unknown {
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
    throw new Error('WayForPay webhook payload must be a JSON object')
  }
  return raw  // NO value transformation — HMAC integrity depends on raw values
}

const wayforpayWebhookSchema = z.preprocess(
  normalizeWayforPayBody,
  z.object({
    orderReference: z.string().min(1),
    merchantSignature: z.string().min(1),
    merchantAccount: z.string().optional(),
    transactionStatus: z.string().optional(),
    amount: z.union([z.number(), z.string()]).optional(),
  }).passthrough()
)`}

Для webhook-ів із кількома дротовими форматами (наприклад, метрики Web Vitals) `z.preprocess` може нормалізувати дані до канонічної форми:

{`function normalizeWebVitalsInput(raw: unknown): unknown {
  if (!raw || typeof raw !== 'object') return raw
  const body = raw as Record<string, unknown>

  // Single-metric wrapper
  if (body.type === 'single-metric' && body.metric) {
    return { sessionId: String(body.metric.sessionId ?? ''), metrics: [body.metric] }
  }
  // Batch-report wrapper
  if (body.type === 'batch-report' && body.report) {
    return { sessionId: String(body.report.sessionId ?? ''), metrics: body.report.metrics }
  }
  // Raw metric (no wrapper)
  if (typeof body.name === 'string' && typeof body.value === 'number') {
    return { sessionId: String(body.sessionId ?? ''), metrics: [body] }
  }
  return body  // pass-through, schema catches mismatches
}

const schema = z.preprocess(normalizeWebVitalsInput, webVitalsPayloadSchema)`}

### Умовна валідація з superRefine

Коли правила валідації залежать від значень полів (наприклад, вимоги до метаданих різняться залежно від типу розмови), використовуйте `superRefine`:

{`const createConversationSchema = z.object({
  type: z.enum(['direct', 'entity', 'opportunity', 'product', 'group']),
  participantIds: z.array(z.string()).min(1),
  metadata: z.object({
    entityId: z.string().optional(),
    opportunityId: z.string().optional(),
    productId: z.string().optional(),
    /** Optional subtype (e.g. generative_gallery); open string today — see /docs/features/messaging */
    kind: z.string().optional(),
    hiddenFromInbox: z.boolean().optional(),
  }).optional(),
}).passthrough().superRefine((data, ctx) => {
  if (data.type !== 'direct' && !data.metadata) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: \`metadata is required for \${data.type} conversations\`,
      path: ['metadata'],
    })
  }
  if (data.type === 'entity' && !data.metadata?.entityId) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: 'entityId is required for entity conversations',
      path: ['metadata', 'entityId'],
    })
  }
})`}

### Витягування enum із TypeScript enum

Для великих TypeScript enum (наприклад, `NotificationType` із 27 значеннями) витягніть значення під час виконання для Zod:

{`import { NotificationType, NotificationPriority } from '@/features/notifications/types'

const typeValues = Object.values(NotificationType) as [string, ...string[]]
const priorityValues = Object.values(NotificationPriority) as [string, ...string[]]

const createNotificationSchema = z.object({
  title: z.string().min(1),
  body: z.string().min(1),
  type: z.enum(typeValues),
  priority: z.enum(priorityValues).default(NotificationPriority.NORMAL),
})`}

### after() для неблокувальних записів аналітики

Маршрути аналітики використовують `after()` Next.js 16, щоб відповідати одразу, поки записи до БД виконуються у фоновому режимі. Це усуває близько 50 мс блокувальної затримки для кожного аналітичного виклику:

{`import { after } from 'next/server'

export async function POST(request: NextRequest) {
  const parsed = analyticsSchema.safeParse(await request.json())
  if (!parsed.success) return errorResponse(parsed.error)

  const storageDisabled = isAnalyticsStorageDisabled()

  if (!storageDisabled) {
    after(async () => {
      const session = await auth().catch(() => null)
      await insertAnalyticsBatch(parsed.data, session?.user?.id)
        .catch(err => console.error('background write failed:', err))
    })
  }

  return NextResponse.json({ success: true, storageSkipped: storageDisabled })
}`}

### HMAC + Zod для платіжних webhook-ів

Обробка платіжного webhook поєднує перевірку форми за допомогою Zod із перевіркою HMAC-підпису:

```mermaid
flowchart LR
  A[Webhook POST] --> B[Zod: validate shape]
  B -->|invalid| C[400 Bad Request]
  B -->|valid| D[HMAC: verify signature]
  D -->|mismatch| E[401 Unauthorized]
  D -->|match| F[Dispatch to handler]
  F --> G[Idempotent order update]
```

### Розібрати й перевірити форму

Zod гарантує, що payload має обов’язкові поля (`orderReference`, `merchantSignature`, `amount` тощо) до початку будь-якої обробки.

### Перевірити HMAC-підпис

Повторно обчисліть HMAC-MD5 зі значень payload, використовуючи секретний ключ продавця. Порівняйте результат із `merchantSignature`. У разі невідповідності відхиліть запит.

### Направити за посиланням на замовлення

Розберіть префікс `orderReference`, щоб визначити тип замовлення (store, membership, news promotion) і передати його правильному обробнику.

### Ідемпотентне оновлення

Обробники перевіряють наявний статус замовлення перед оновленням — дублікати webhook безпечно ігноруються.

### Валідація MCP-маршрутів

Усі маршрути `/api/mcp/v1/*` використовують схеми Zod із `.passthrough()` для прямої сумісності. Це дозволяє MCP-клієнтам надсилати додаткові поля без помилок, водночас обов’язкові поля все одно перевіряються:

{`// Every MCP route follows this pattern:
const schema = z.object({
  // Required fields with clear error messages
  title: z.string().min(1, 'title is required'),
  // Optional fields with type constraints
  status: z.enum(['draft', 'active', 'archived']).optional(),
}).passthrough()  // Allow unknown fields for forward compatibility

export const POST = withMcpGuard(async (request) => {
  const parsed = schema.safeParse(await readJsonBody(request))
  if (!parsed.success) {
    return mcpError(parsed.error.issues[0]?.message ?? 'Invalid body', 400)
  }
  return mcpOk(await service.create(parsed.data), 201)
})`}

### Устранені антипатерни

|| Антипатерн | Чому це небезпечно | Заміна |
||-------------|-------------------|-------------|
|| `body as any` | Обходить усі перевірки типів | Zod `safeParse` + типізований результат |
|| Ручні ланцюжки `if` | Легко пропустити крайні випадки | Один виклик `schema.safeParse()` |
|| Мутація розібраного body | Побічні ефекти порушують передбачуваність | Розгорнути в новий об’єкт |
|| Відсутність валідації маршруту | Сервіс отримує сміття | Zod на межі маршруту |
|| `as Record<string, unknown>` для читання з БД | Приховує невідповідності типів | `schema.safeParse(dbResult.data)` |

## Пов’язана документація

  
- **[Модель безпеки](/docs/architecture/security.md)** — RBAC, конфіденційні рівні, чекліст зміцнення API та захист автентифікації на рівні макета.

  
- **[Модель даних](/docs/architecture/data-model.md)** — Контракт документа JSONB, SSOT schema.sql і патерни DatabaseService.

  
- **[PaymentConductor](/docs/architecture/payment-conductor.md)** — Перевірка HMAC webhook-ів, ідемпотентні посилання на замовлення та потоки розрахунків.

  
- **[Найкращі практики](/docs/development/best-practices.md)** — Угоди Server Action, патерни доступу до бази даних і обробка помилок.

  
- **[Автентифікація](/docs/architecture/authentication.md)** — Сесії Auth.js v5, SSOT enum ролей і налаштування кількох провайдерів.
