Генерация OpenAPI спецификаций

Zod представляет декларативный способ описания структур данных, где схема одновременно выступает валидатором и источником типов TypeScript. Такой подход позволяет использовать единое определение данных как на этапе выполнения, так и на этапе компиляции.

Базовая идея заключается в том, что каждая схема описывает форму объекта, примитив или композицию, а затем может быть использована для вывода типов и валидации входящих данных.

import { z } from "zod";

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  age: z.number().int().positive(),
});

Схемы Zod становятся фундаментом для последующей генерации OpenAPI, так как они уже содержат полное описание структуры данных.


Базовые типы и композиция

В Zod поддерживаются примитивные типы, а также их композиции. На уровне OpenAPI это соответствует схемам компонентов (components/schemas), которые могут быть переиспользованы.

Основные примитивы:

z.string();
z.number();
z.boolean();
z.null();
z.undefined();

Композиционные конструкции:

  • z.array(schema) — массив
  • z.object({...}) — объект
  • z.union([a, b]) — объединение типов
  • z.intersection(a, b) — пересечение
  • z.optional() — необязательность
  • z.nullable() — допускается null

Пример композиции:

const ResponseSchema = z.object({
  data: z.union([
    z.string(),
    z.array(z.string())
  ]),
  error: z.nullable(z.string()),
});

При генерации OpenAPI такие конструкции трансформируются в oneOf, anyOf, nullable и массивы схем.


Интеграция с OpenAPI через zod-to-openapi

Для преобразования схем используется экосистема вроде zod-to-openapi, которая расширяет Zod возможностью аннотирования схем метаданными OpenAPI.

Подключение расширения:

import { extendZodWithOpenApi } from "zod-to-openapi";
import { z } from "zod";

extendZodWithOpenApi(z);

После расширения каждая схема может быть описана с дополнительной информацией:

const UserSchema = z.object({
  id: z.string().openapi({ example: "123" }),
  name: z.string().openapi({ example: "Alex" }),
}).openapi("User");

Метод .openapi() задаёт имя компонента, которое затем используется в спецификации OpenAPI как ссылка $ref.


Регистрация схем и компонентов

Генерация OpenAPI требует централизованного реестра схем. Он используется для формирования секции components/schemas.

import { OpenAPIRegistry } from "zod-to-openapi";

const registry = new OpenAPIRegistry();

registry.register("User", UserSchema);

Каждая зарегистрированная схема становится доступной для маршрутов и может быть переиспользована в разных частях API.

Регистрация позволяет:

  • избегать дублирования схем
  • формировать корректные $ref
  • строить граф зависимостей типов

Описание маршрутов (paths)

OpenAPI спецификация строится вокруг HTTP-операций. Для их описания используется регистрация путей:

registry.registerPath({
  method: "get",
  path: "/users/{id}",
  responses: {
    200: {
      description: "User found",
      content: {
        "application/json": {
          schema: UserSchema,
        },
      },
    },
  },
});

Каждый маршрут содержит:

  • HTTP-метод
  • путь
  • параметры
  • тело запроса
  • ответы

Схемы Zod используются для всех этих элементов.


Параметры запроса и ответы

Параметры пути и query-string также описываются через Zod:

const ParamsSchema = z.object({
  id: z.string(),
});

registry.registerPath({
  method: "get",
  path: "/users/{id}",
  request: {
    params: ParamsSchema,
  },
  responses: {
    200: {
      description: "OK",
      content: {
        "application/json": {
          schema: UserSchema,
        },
      },
    },
  },
});

Преобразование:

  • z.string()type: string
  • z.number()type: number
  • z.object()type: object

Query-параметры автоматически отражаются как in: query.


Генерация спецификации

После регистрации всех схем и маршрутов формируется финальная OpenAPI спецификация.

import { OpenApiGeneratorV3 } from "zod-to-openapi";

const generator = new OpenApiGeneratorV3(registry.definitions);

const document = generator.generateDocument({
  openapi: "3.0.0",
  info: {
    title: "API",
    version: "1.0.0",
  },
});

Результатом является JSON-документ, соответствующий OpenAPI 3.x, включающий:

  • paths
  • components/schemas
  • parameters
  • responses

Расширенные возможности: discriminated unions, refs

Zod поддерживает дискриминируемые объединения, которые напрямую транслируются в OpenAPI oneOf с discriminator.

const ShapeSchema = z.discriminatedUnion("type", [
  z.object({
    type: z.literal("circle"),
    radius: z.number(),
  }),
  z.object({
    type: z.literal("square"),
    size: z.number(),
  }),
]);

В OpenAPI это превращается в:

  • oneOf
  • discriminator.propertyName = "type"

Также активно используются ссылки:

const A = z.object({
  b: z.lazy(() => BSchema),
});

Ленивые ссылки позволяют описывать циклические зависимости между схемами, что важно для сложных доменных моделей.


Версионирование и повторное использование

При генерации OpenAPI на основе Zod-схем критично управление версионированием:

  • изменение схемы приводит к изменению контракта API
  • переиспользуемые схемы уменьшают дублирование
  • components/schemas выступает единым источником правды

Структура обычно организуется через доменные модули:

  • UserSchema
  • AuthSchema
  • PaymentSchema

Каждая сущность становится независимым компонентом спецификации.


Практики для сложных API

При масштабировании API на основе Zod и OpenAPI важны следующие подходы:

  • разделение схем и маршрутов по модулям
  • явное именование схем через .openapi(name)
  • избегание inline-описаний в маршрутах
  • централизованный registry для всех компонентов
  • использование discriminated unions вместо ручных oneOf

Сложные структуры данных (например, вложенные DTO) описываются через композицию Zod-схем, что позволяет сохранять согласованность между runtime-валидацией и контрактом API.

Особое значение имеет строгая типизация на уровне схем, так как она напрямую определяет корректность OpenAPI-документа и последующую генерацию клиентских SDK.