Интеграция с TypeScript строится вокруг идеи синхронизации JSON Schema и статических типов. Основная проблема заключается в том, что JSON Schema описывает структуру данных во время выполнения, тогда как TypeScript работает на этапе компиляции. При неправильной организации возникает расхождение между схемой валидации и типами, что приводит к ошибкам, обнаруживаемым только в рантайме.
В экосистеме Ajv ключевым механизмом типизации становится
использование генерации типов из схем и строгая проверка соответствия
схем типам через JSONSchemaType.
TypeScript-ориентированный подход в Ajv предполагает, что схема становится первичной моделью данных.
import Ajv, { JSONSchemaType } from "ajv";
const ajv = new Ajv();
interface User {
id: number;
name: string;
email?: string;
}
const schema: JSONSchemaType<User> = {
type: "object",
properties: {
id: { type: "integer" },
name: { type: "string" },
email: { type: "string", nullable: true },
},
required: ["id", "name"],
additionalProperties: false,
};
const validate = ajv.compile(schema);
Здесь JSONSchemaType<User> выполняет роль строгого
контракта. Любое несоответствие между интерфейсом и схемой приводит к
ошибке компиляции.
Главная особенность интеграции заключается в том, что TypeScript не проверяет JSON Schema сам по себе. Проверка происходит через дженерики и строгую типизацию полей схемы.
Ошибки, которые предотвращаются:
type и TypeScript-типаnullable,
required, enumПример ошибки:
const schema: JSONSchemaType<User> = {
type: "object",
properties: {
id: { type: "string" }, // ошибка: должен быть integer
name: { type: "string" },
},
required: ["id", "name"],
additionalProperties: false,
};
TypeScript остановит компиляцию, так как id не
соответствует типу number.
В современных подходах Ajv допускает обратную стратегию — получение TypeScript-типа из схемы.
Это особенно полезно, когда схема является источником правды в API или при работе с внешними контрактами.
import { FromSchema } from "json-schema-to-ts";
const userSchema = {
type: "object",
properties: {
id: { type: "integer" },
name: { type: "string" },
},
required: ["id", "name"],
additionalProperties: false,
} as const;
type User = FromSchema<typeof userSchema>;
Такой подход снижает дублирование и исключает рассинхронизацию между моделью данных и схемой.
Ajv предоставляет механизм компиляции схемы в функцию-валидатор. В
TypeScript важно сохранять типизацию результата
compile.
const validate = ajv.compile<User>(schema);
const data: User = {
id: 1,
name: "Alex",
};
if (validate(data)) {
// data имеет тип User
} else {
console.log(validate.errors);
}
Здесь происходит связка:
TypeScript и JSON Schema имеют различную семантику для отсутствующих
и null значений.
В Ajv важно явно различать:
required — обязательное наличие ключаnullable: true — допустимость nullrequired — опциональностьinterface Profile {
bio?: string;
age: number | null;
}
const schema: JSONSchemaType<Profile> = {
type: "object",
properties: {
bio: { type: "string", nullable: true },
age: { type: "integer", nullable: true },
},
required: ["age"],
additionalProperties: false,
};
Ошибки чаще всего возникают при попытке совместить
undefined и null без явного отражения в
схеме.
Типизация в Ajv позволяет строить универсальные фабрики схем.
function createValidator<T>(schema: JSONSchemaType<T>) {
return ajv.compile<T>(schema);
}
Это обеспечивает:
TypeScript и JSON Schema расходятся в описании объединений типов, но
Ajv корректно поддерживает их через oneOf,
enum и const.
interface Role {
role: "admin" | "user";
}
const schema: JSONSchemaType<Role> = {
type: "object",
properties: {
role: { type: "string", enum: ["admin", "user"] },
},
required: ["role"],
additionalProperties: false,
};
type Shape =
| { type: "circle"; radius: number }
| { type: "square"; size: number };
const schema: JSONSchemaType<Shape> = {
oneOf: [
{
type: "object",
properties: {
type: { const: "circle" },
radius: { type: "number" },
},
required: ["type", "radius"],
},
{
type: "object",
properties: {
type: { const: "square" },
size: { type: "number" },
},
required: ["type", "size"],
},
],
};
Ключевой архитектурный принцип при работе с Ajv и TypeScript заключается в разделении ответственности:
Такое разделение позволяет:
Типовые расхождения:
number vs integerundefined vs отсутствие поляreadonly свойства TypeScript не отражаются в JSON
SchemanullОсобенно критично учитывать, что TypeScript допускает структурную совместимость, тогда как JSON Schema требует явного описания всех вариантов.
В связке с TypeScript Ajv часто используется как слой валидации входящих данных API:
app.post("/user", (req, res) => {
if (!validate(req.body)) {
return res.status(400).json(validate.errors);
}
const user: User = req.body;
});
Такой подход минимизирует риск некорректных данных на уровне бизнес-логики.
Сложные структуры, такие как record-объекты, требуют аккуратного описания:
interface Dictionary {
[key: string]: number;
}
const schema: JSONSchemaType<Dictionary> = {
type: "object",
additionalProperties: { type: "number" },
};
Ajv требует явного указания additionalProperties, иначе
структура считается неопределённой.
В зрелых проектах интеграция TypeScript и Ajv используется как часть цепочки обработки данных:
Такой подход устраняет необходимость ручного парсинга и проверки структуры на разных уровнях приложения.