Генерация типов из схем

Одним из ключевых преимуществ использования JSON Schema в TypeScript-проектах является возможность автоматической генерации типов на основе схем валидации. Это устраняет дублирование описаний данных, снижает вероятность рассинхронизации между рантайм-валидацией и типами на этапе компиляции и делает контракт данных единым источником истины.

Ajv сам по себе является валидатором JSON Schema, но в экосистеме вокруг него существует несколько подходов к генерации TypeScript-типов из схем, а также интеграции с типами на этапе сборки и выполнения.


Базовая идея синхронизации схем и типов

JSON Schema описывает структуру данных на уровне выполнения, тогда как TypeScript-типизация работает на этапе компиляции. Основная проблема возникает, когда схема и тип описываются вручную:

type User = {
  id: number;
  name: string;
};

и отдельно:

const schema = {
  type: "object",
  properties: {
    id: { type: "number" },
    name: { type: "string" }
  },
  required: ["id", "name"]
};

Любое расхождение между ними приводит к логическим ошибкам, которые TypeScript не всегда способен обнаружить.

Решение заключается в генерации типов из схем или использовании схемы как первичного источника.


Подход 1: JSON Schema → TypeScript через генераторы

Схема остаётся источником истины, а типы генерируются автоматически.

Установка инструмента генерации

Наиболее распространённый инструмент — json-schema-to-typescript:

npm install -D json-schema-to-typescript

Пример схемы

{
  "$id": "User",
  "type": "object",
  "properties": {
    "id": { "type": "integer" },
    "email": { "type": "string", "format": "email" },
    "isActive": { "type": "boolean" }
  },
  "required": ["id", "email"]
}

Генерация типов через CLI

json2ts -i user.schema.json -o user.types.ts

Результат:

export interface User {
  id: number;
  email: string;
  isActive?: boolean;
}

Интеграция с Ajv в рантайме

Сгенерированные типы не заменяют Ajv, а работают вместе с ним:

import Ajv from "ajv";
import schema from "./user.schema.json";

const ajv = new Ajv();

const validate = ajv.compile(schema);

const data = {
  id: 1,
  email: "test@mail.com"
};

if (validate(data)) {
  // data имеет тип User (в TypeScript)
}

Типизация и валидация становятся согласованными через общую схему.


Подход 2: TypeScript-first схемы с последующей генерацией Ajv-валидатора

В более современных подходах схема не пишется вручную, а выводится из TypeScript.

Использование библиотек типа Zod/TypeBox-подхода

Хотя это не чистый Ajv-подход, он часто применяется вместе.

Пример с TypeBox:

import { Type } from "@sinclair/typebox";

export const User = Type.Object({
  id: Type.Number(),
  email: Type.String({ format: "email" })
});

Далее схема может быть использована напрямую в Ajv:

import Ajv from "ajv";
import { User } from "./schema";

const ajv = new Ajv();

const validate = ajv.compile(User);

TypeScript тип выводится автоматически:

type UserType = Static<typeof User>;

Подход 3: Генерация типов из Ajv-валидированных схем

Ajv не является генератором типов напрямую, но предоставляет runtime-структуру, которую можно использовать совместно с дополнительными инструментами.

Использование ajv + ajv-types

В некоторых проектах применяется генерация типов через промежуточное AST-представление схем.

Пример:

import Ajv from "ajv";
import addFormats from "ajv-formats";

const ajv = new Ajv();
addFormats(ajv);

const schema = {
  type: "object",
  properties: {
    age: { type: "number", minimum: 0 }
  },
  required: ["age"]
};

const validate = ajv.compile(schema);

Далее тип может быть выведен вручную или через генератор схем.


Подход 4: Использование CLI Ajv для валидации и генерации артефактов

Ajv CLI позволяет интегрировать схемы в сборочный процесс.

npm install -D ajv-cli

Проверка данных:

ajv validate -s schema.json -d data.json

Хотя CLI не генерирует TypeScript напрямую, он часто используется в пайплайне, где параллельно работает генератор типов.


Типизация сложных конструкций JSON Schema

OneOf / AnyOf

{
  "oneOf": [
    { "type": "string" },
    { "type": "number" }
  ]
}

Генерация типов:

type Value = string | number;

Nested objects

{
  "type": "object",
  "properties": {
    "profile": {
      "type": "object",
      "properties": {
        "nickname": { "type": "string" }
      },
      "required": ["nickname"]
    }
  }
}

TypeScript:

type User = {
  profile: {
    nickname: string;
  };
};

Arrays

{
  "type": "array",
  "items": {
    "type": "number"
  }
}
type Numbers = number[];

Синхронизация изменений схем и типов

Основная проблема при генерации типов — рассинхронизация build-процессов.

Типовой подход:

  1. Схемы хранятся в /schemas
  2. Перед сборкой запускается генерация типов
  3. TypeScript компилируется уже с актуальными типами

Пример скрипта:

{
  "scripts": {
    "generate:types": "json2ts -i schemas -o types",
    "build": "npm run generate:types && tsc"
  }
}

Использование строгого режима Ajv для согласованности типов

Ajv может усиливать соответствие схем:

const ajv = new Ajv({
  allErrors: true,
  strict: true,
  strictTypes: true
});

Это помогает избежать ситуаций, когда схема допускает типы, не отражённые в TypeScript-генерации.


Проблемы генерации типов из схем

1. Неоднозначные конструкции

{
  "type": ["string", "null"]
}

TypeScript:

string | null

Но при сложных композициях возможны неточные выводы.


2. Динамические ключи

{
  "type": "object",
  "additionalProperties": {
    "type": "string"
  }
}

TypeScript:

Record<string, string>

При сложных ограничениях теряется детализация.


3. Format и custom keywords

Ajv поддерживает кастомные ключевые слова:

ajv.addKeyword("x-custom-rule");

Но генераторы типов их часто игнорируют, что приводит к расхождению логики.


Комбинированная архитектура (Ajv + генерация типов)

Наиболее устойчивый подход:

  • JSON Schema — источник истины
  • Ajv — runtime-валидация
  • json-schema-to-typescript — генерация типов
  • CI — проверка синхронности

Структура:

schemas/
  user.schema.json

types/
  user.types.ts

validators/
  user.validator.ts

Вывод типов через кодогенерацию Ajv-валидаторов

Некоторые сборки используют генерацию валидаторов:

const validateUser = ajv.compile<User>(schema);

Хотя Ajv не всегда выводит типы автоматически, через обёртки можно добиться строгой типизации.


Заключительные технические наблюдения

Генерация типов из JSON Schema в экосистеме Ajv — это не встроенная функция библиотеки, а архитектурный паттерн, который строится вокруг неё. Основная ценность Ajv в этом контексте заключается в том, что он обеспечивает строгую и быструю runtime-валидацию, а типы формируются либо внешними генераторами, либо через schema-first подход.

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