WebSocket-сообщения в прикладных системах представляют собой непрерывный поток структурированных данных, передаваемых между клиентом и сервером в режиме реального времени. При росте сложности протоколов обмена возрастает необходимость строгой формализации формата сообщений, где ключевую роль играет валидация входящих и исходящих структур. Библиотека Ajv (Another JSON Schema Validator) обеспечивает компиляцию JSON Schema в высокопроизводительные функции проверки, что делает её одним из базовых инструментов для контроля WebSocket-протоколов в JavaScript-среде.
WebSocket сам по себе не накладывает ограничений на структуру сообщений. Типичная практика — использование “конверта” сообщения, в котором задаются метаданные и полезная нагрузка:
{
"type": "message",
"event": "chat.send",
"payload": {
"text": "Hello"
},
"requestId": "abc-123"
}
Такая структура требует строгого описания, чтобы исключить некорректные данные на границе системы. JSON Schema позволяет описать подобные сообщения декларативно.
Базовая схема для подобного сообщения:
const messageSchema = {
type: "object",
additionalProperties: false,
required: ["type", "event", "payload"],
properties: {
type: { type: "string", enum: ["message", "error", "response"] },
event: { type: "string" },
requestId: { type: "string" },
payload: { type: "object" }
}
};
Ajv компилирует эту схему в функцию, выполняющую проверку без повторного разбора JSON Schema, что критично для потоковой обработки WebSocket-сообщений.
Создание экземпляра валидатора:
import Ajv from "ajv";
const ajv = new Ajv({
allErrors: true,
strict: true
});
const validateMessage = ajv.compile(messageSchema);
Компиляция выполняется один раз. Результирующая функция
validateMessage используется для каждого входящего
сообщения.
При получении данных через WebSocket важно учитывать, что данные приходят в виде строки. Типичный поток обработки включает парсинг и валидацию:
ws.on("message", (raw) => {
let data;
try {
data = JSON.parse(raw);
} catch (e) {
ws.send(JSON.stringify({
type: "error",
error: "INVALID_JSON"
}));
return;
}
const valid = validateMessage(data);
if (!valid) {
ws.send(JSON.stringify({
type: "error",
error: "SCHEMA_VALIDATION_FAILED",
details: validateMessage.errors
}));
return;
}
handleMessage(data);
});
Ajv возвращает структурированный массив ошибок, содержащий путь к полю, ожидаемый тип и фактическое значение, что позволяет строить детализированные диагностические ответы.
В WebSocket-протоколах часто используются разные типы сообщений:
события, ответы, ошибки, команды. Ajv поддерживает композицию схем через
oneOf, что позволяет формализовать строгую типизацию
сообщений:
const schema = {
type: "object",
oneOf: [
{
required: ["type", "event", "payload"],
properties: {
type: { const: "event" },
event: { type: "string" },
payload: { type: "object" }
}
},
{
required: ["type", "requestId", "result"],
properties: {
type: { const: "response" },
requestId: { type: "string" },
result: {}
}
},
{
required: ["type", "error"],
properties: {
type: { const: "error" },
error: { type: "string" },
message: { type: "string" }
}
}
]
};
Такая конструкция формирует строгий дискриминируемый союз типов
сообщений, где поле type выступает дискриминатором.
WebSocket-системы часто развиваются без возможности остановки сервера. Добавление версии протокола позволяет поддерживать обратную совместимость:
const versionedSchema = {
type: "object",
required: ["v", "type", "payload"],
properties: {
v: { type: "integer", enum: [1, 2] },
type: { type: "string" },
payload: { type: "object" }
},
oneOf: [
{
properties: {
v: { const: 1 },
type: { const: "message" }
}
},
{
properties: {
v: { const: 2 },
type: { enum: ["message", "event"] }
}
}
]
};
Ajv позволяет строить такие условные ветвления без потери производительности за счёт предварительной компиляции.
В контексте WebSocket производительность валидации критична, поскольку обработка сообщений происходит в реальном времени. Ajv решает эту задачу за счёт генерации оптимизированного JavaScript-кода на этапе компиляции схемы.
Ключевые практики оптимизации:
removeAdditional для очистки входных
данныхstrict режима для раннего обнаружения ошибок
схемыПример настройки:
const ajv = new Ajv({
allErrors: true,
removeAdditional: "all",
coerceTypes: true,
strict: true
});
WebSocket-протоколы часто используют сложные вложенные структуры:
const chatSchema = {
type: "object",
required: ["type", "payload"],
properties: {
type: { const: "chat" },
payload: {
type: "object",
required: ["user", "message"],
properties: {
user: {
type: "object",
required: ["id", "name"],
properties: {
id: { type: "string" },
name: { type: "string" }
}
},
message: { type: "string", maxLength: 500 }
}
}
}
};
Ajv рекурсивно валидирует вложенные структуры без дополнительного кода со стороны приложения.
Ajv формирует массив ошибок с детальной структурой:
validateMessage.errors = [
{
instancePath: "/payload/message",
message: "must be string",
keyword: "type",
params: {
type: "string"
}
}
];
Эти данные позволяют формировать диагностические события для клиента или логировать ошибки с точной привязкой к полю сообщения.
Ajv поддерживает расширение через форматы и кастомные ключевые слова, что полезно для WebSocket-протоколов с идентификаторами, токенами и временными метками:
import addFormats from "ajv-formats";
addFormats(ajv);
const schema = {
type: "object",
required: ["timestamp"],
properties: {
timestamp: { type: "string", format: "date-time" }
}
};
Кастомные проверки:
ajv.addKeyword({
keyword: "isUserId",
validate: (schema, data) => typeof data === "string" && data.startsWith("user_")
});
В типичной архитектуре Ajv используется как часть промежуточного слоя между транспортом и бизнес-логикой:
event или typeТакая схема обеспечивает симметричность протокола и контроль на обоих направлениях передачи данных.
Ajv применяется не только для входящих данных, но и для исходящих сообщений, что предотвращает утечки некорректных структур в клиент:
const response = {
type: "response",
requestId: "abc-123",
result: { ok: true }
};
if (!validateResponse(response)) {
throw new Error("Invalid server message structure");
}
ws.send(JSON.stringify(response));
В сложных системах схемы могут собираться динамически из модулей. Ajv поддерживает добавление схем в реестр:
ajv.addSchema(userSchema, "User");
ajv.addSchema(chatSchema, "ChatMessage");
const schema = {
$ref: "ChatMessage"
};
Это позволяет строить модульный WebSocket-протокол с переиспользованием компонентов.
Валидация WebSocket-сообщений через Ajv выполняет функцию первой линии защиты:
Комбинация additionalProperties: false и строгих типов
обеспечивает жёсткую фиксацию контракта между клиентом и сервером.
Хотя Ajv работает на уровне runtime, он может использоваться совместно с типизацией, где JSON Schema становится источником истины для структуры WebSocket-сообщений. Это снижает расхождение между документацией и фактической реализацией протокола.
В высоконагруженных WebSocket-системах валидатор становится частью pipeline обработки потока. Ajv позволяет быстро отклонять некорректные сообщения до попадания в бизнес-очередь, уменьшая нагрузку на последующие стадии обработки.
Каждый вызов compile создаёт отдельную функцию. В
WebSocket-сервере валидаторы должны быть созданы один раз и
использоваться повторно:
const validators = {
event: ajv.compile(eventSchema),
response: ajv.compile(responseSchema)
};
Такой подход исключает накладные расходы на повторную компиляцию при каждом сообщении.
Ajv поддерживает if/then/else, что позволяет
моделировать сложные протокольные правила:
const schema = {
type: "object",
if: {
properties: { type: { const: "auth" } }
},
then: {
required: ["token"]
},
else: {
required: ["requestId"]
}
};
В WebSocket-системах это используется для разделения аутентифицированных и неаутентифицированных сообщений.