В GraphQL входные данные резолверов представляют собой строго
структурированный набор аргументов, которые приходят из клиентского
запроса и проходят через слой схемы до выполнения бизнес-логики. Каждый
резолвер получает четыре базовых параметра: parent,
args, context, info. Основной
интерес для валидации представляет объект args, так как
именно он содержит пользовательские входные данные.
Типичная проблема при работе с GraphQL заключается в том, что система типизации схемы не всегда достаточна для сложной доменной логики. GraphQL обеспечивает базовую проверку типов (scalar, enum, non-null, list), однако не покрывает:
Эта зона ответственности часто переносится в резолвер или сервисный слой, где использование JSON Schema и Ajv становится практичным решением.
Ajv представляет собой высокопроизводительный валидатор JSON Schema, ориентированный на строгую проверку структур данных. Его использование в GraphQL-резолверах позволяет формализовать проверку входных аргументов в виде декларативных схем.
Ключевая особенность заключается в том, что схема становится единым источником правил:
В отличие от встроенной системы GraphQL, Ajv позволяет описывать гораздо более сложные правила без разрастания логики внутри резолверов.
Типичная архитектура интеграции строится вокруг промежуточной функции-валидатора, которая вызывается перед основной логикой резолвера.
import Ajv fr om "ajv";
const ajv = new Ajv({ allErrors: true });
const schema = {
type: "object",
required: ["id", "limit"],
properties: {
id: { type: "string", minLength: 1 },
lim it: { type: "integer", minimum: 1, maximum: 100 }
},
additionalProperties: false
};
const validate = ajv.compile(schema);
function validateArgs(args) {
const valid = validate(args);
if (!valid) {
throw new Error(JSON.stringify(validate.errors));
}
}
Использование в резолвере:
const resolvers = {
Query: {
users: (_, args, context) => {
validateArgs(args);
return context.db.users.findMany(args);
}
}
};
Такая схема позволяет отделить валидацию от бизнес-логики и избежать разрастания проверок внутри каждого резолвера.
Входные аргументы GraphQL часто имеют вложенную структуру:
type Query {
searchUsers(filter: UserFilter!, pagination: Pagination): [User]
}
Соответствующая схема Ajv:
const schema = {
type: "object",
required: ["filter"],
properties: {
filter: {
type: "object",
required: ["status"],
properties: {
status: { type: "string", enum: ["active", "blocked"] },
age: {
type: "object",
properties: {
min: { type: "integer", minimum: 0 },
max: { type: "integer", minimum: 0 }
},
additionalProperties: false
}
},
additionalProperties: false
},
pagination: {
type: "object",
properties: {
limit: { type: "integer", minimum: 1, maximum: 50 },
offset: { type: "integer", minimum: 0 }
},
additionalProperties: false
}
},
additionalProperties: false
};
Такое описание отражает реальную структуру GraphQL-API, но добавляет более строгие ограничения, чем сама схема GraphQL.
GraphQL и JSON Schema используют разные модели типизации:
Несовпадения проявляются в нескольких аспектах:
Nullability
String!type: ["string"] + отсутствие
nullМассивы
[Int!]type: "array", items: { type: "integer" }Optional fields
requiredAjv требует явного описания всех ограничений, что делает поведение более предсказуемым, но требует дисциплины при синхронизации схем.
В сложных GraphQL API резолверы часто вызывают друг друга через сервисный слой. В таких случаях входные данные могут проходить несколько уровней трансформации.
Пример цепочки:
Ajv может использоваться только на первом уровне, но при необходимости может применяться и на промежуточных слоях для защиты доменной модели.
function serviceSearchUsers(input) {
validateArgs(input);
return repository.search(input.filter, input.pagination);
}
Во многих случаях входные данные требуют приведения к нормализованному виду до проверки:
Ajv поддерживает подобное поведение через опции:
const ajv = new Ajv({
coerceTypes: true,
useDefaults: true,
removeAdditional: true
});
Это позволяет автоматически:
"10" в 10GraphQL часто требует бизнес-ограничений, которые невозможно выразить стандартной схемой.
Пример: проверка зависимости полей
ajv.addKeyword({
keyword: "mutuallyExclusive",
type: "object",
validate: function (schema, data) {
if (schema.includes("email") && schema.includes("phone")) {
return !(data.email && data.phone);
}
return true;
}
});
Использование:
const schema = {
type: "object",
properties: {
email: { type: "string" },
phone: { type: "string" }
},
mutuallyExclusive: ["email", "phone"]
};
Такие конструкции позволяют переносить сложную бизнес-логику из резолверов в слой схем.
В GraphQL важно сохранять предсказуемую структуру ошибок. Ajv возвращает массив ошибок, который необходимо адаптировать под формат GraphQL.
Пример преобразования:
function formatAjvErrors(errors) {
return errors.map(err => ({
message: `${err.instancePath} ${err.message}`,
path: err.instancePath,
keyword: err.keyword
}));
}
Далее ошибки могут быть возвращены через стандартный GraphQL error mechanism.
Ajv оптимизирован через компиляцию схем в функции JavaScript. Это критически важно для GraphQL, где резолверы могут вызываться тысячи раз в секунду.
Особенности производительности:
Рекомендованная практика — избегать компиляции схем внутри резолвера:
// плохо
function resolver(args) {
const validate = ajv.compile(schema);
validate(args);
}
// правильно
const validate = ajv.compile(schema);
function resolver(args) {
validate(args);
}
В некоторых API структура аргументов зависит от входных параметров запроса. Это приводит к необходимости динамической генерации схем Ajv.
Пример:
function getSchema(type) {
if (type === "admin") {
return {
type: "object",
required: ["id", "role"],
properties: {
id: { type: "string" },
role: { type: "string", enum: ["admin"] }
}
};
}
return {
type: "object",
required: ["id"],
properties: {
id: { type: "string" }
}
};
}
Такой подход увеличивает гибкость API, но требует аккуратного кеширования скомпилированных схем.
При большом количестве резолверов и схем возникает необходимость кеширования:
const schemaCache = new Map();
function getValidator(schemaKey, schema) {
if (!schemaCache.has(schemaKey)) {
schemaCache.set(schemaKey, ajv.compile(schema));
}
return schemaCache.get(schemaKey);
}
Это снижает нагрузку на CPU и ускоряет обработку GraphQL-запросов.
Несмотря на пересечение ответственности, схемы GraphQL и Ajv выполняют разные функции:
В зрелых системах они используются совместно:
Такая многослойная проверка снижает вероятность неконсистентных данных в системе и упрощает сопровождение сложных API.