Валидация данных редко ограничивается статическими JSON-схемами. В реальных приложениях структура данных часто зависит от:
Библиотека Ajv позволяет не только валидировать данные по заранее подготовленным схемам, но и динамически создавать схемы во время выполнения программы.
Динамическая генерация схем особенно полезна в:
Схема создаётся обычным JavaScript-кодом. Поскольку JSON Schema — это объект JavaScript, его можно формировать динамически.
Пример:
function createUserSchema(requiredFields = []) {
return {
type: "object",
properties: {
name: {
type: "string"
},
age: {
type: "integer"
}
},
required: requiredFields,
additionalProperties: false
}
}
Использование:
const schema = createUserSchema(["name"])
const validate = ajv.compile(schema)
console.log(validate({
name: "Alex",
age: 25
}))
Частая задача — различная валидация для администратора и обычного пользователя.
function createProfileSchema(role) {
const properties = {
username: {
type: "string"
}
}
const required = ["username"]
if (role === "admin") {
properties.permissions = {
type: "array",
items: {
type: "string"
}
}
required.push("permissions")
}
return {
type: "object",
properties,
required,
additionalProperties: false
}
}
Использование:
const adminSchema = createProfileSchema("admin")
const validate = ajv.compile(adminSchema)
function createSchema(fields) {
const properties = {}
for (const field of fields) {
properties[field.name] = {
type: field.type
}
}
return {
type: "object",
properties,
required: fields
.filter(f => f.required)
.map(f => f.name)
}
}
Конфигурация:
const fields = [
{
name: "title",
type: "string",
required: true
},
{
name: "views",
type: "integer",
required: false
}
]
Создание схемы:
const schema = createSchema(fields)
Полученная схема:
{
type: "object",
properties: {
title: {
type: "string"
},
views: {
type: "integer"
}
},
required: ["title"]
}
В сложных системах структура данных бывает древовидной.
Пример конфигурации:
const structure = {
type: "object",
fields: {
name: "string",
profile: {
type: "object",
fields: {
age: "integer"
}
}
}
}
Функция генерации:
function buildSchema(node) {
const schema = {
type: node.type,
properties: {}
}
for (const [key, value] of Object.entries(node.fields)) {
if (typeof value === "string") {
schema.properties[key] = {
type: value
}
} else {
schema.properties[key] = buildSchema(value)
}
}
return schema
}
Использование:
const schema = buildSchema(structure)
Многие UI-конструкторы создают поля формы из конфигурации.
Конфигурация:
const formConfig = [
{
field: "email",
type: "string",
format: "email",
required: true
},
{
field: "password",
type: "string",
minLength: 8,
required: true
}
]
Генерация схемы:
function generateFormSchema(config) {
const properties = {}
const required = []
for (const item of config) {
properties[item.field] = {
type: item.type
}
if (item.format) {
properties[item.field].format = item.format
}
if (item.minLength) {
properties[item.field].minLength = item.minLength
}
if (item.required) {
required.push(item.field)
}
}
return {
type: "object",
properties,
required
}
}
async function createCategorySchema() {
const categories = await db.categories.findMany()
return {
type: "string",
enum: categories.map(c => c.slug)
}
}
Использование:
const schema = await createCategorySchema()
const validate = ajv.compile(schema)
Популярный подход — генерация JSON Schema из TypeScript-интерфейсов.
Инструменты:
Пример интерфейса:
interface User {
id: number
name: string
active: boolean
}
Полученная схема:
{
type: "object",
properties: {
id: {
type: "number"
},
name: {
type: "string"
},
active: {
type: "boolean"
}
},
required: ["id", "name", "active"]
}
Фабрика позволяет стандартизировать создание схем.
function createEntitySchema(entityName, extraProperties = {}) {
return {
type: "object",
properties: {
id: {
type: "integer"
},
createdAt: {
type: "string",
format: "date-time"
},
...extraProperties
},
required: ["id", "createdAt"],
additionalProperties: false
}
}
Использование:
const productSchema = createEntitySchema("product", {
title: {
type: "string"
},
price: {
type: "number"
}
})
Разные версии API могут иметь разные структуры данных.
function createApiSchema(version) {
const schema = {
type: "object",
properties: {
name: {
type: "string"
}
},
required: ["name"]
}
if (version >= 2) {
schema.properties.email = {
type: "string",
format: "email"
}
schema.required.push("email")
}
return schema
}
function createPasswordSchema(strictMode) {
const schema = {
type: "string",
minLength: 6
}
if (strictMode) {
schema.pattern =
"^(?=.*[A-Z])(?=.*[0-9]).+$"
schema.minLength = 12
}
return schema
}
JSON Schema поддерживает:
Пример композиции:
function createUserSchema(options) {
const schemas = []
schemas.push({
type: "object",
properties: {
name: {
type: "string"
}
}
})
if (options.withEmail) {
schemas.push({
type: "object",
properties: {
email: {
type: "string",
format: "email"
}
},
required: ["email"]
})
}
return {
allOf: schemas
}
}
function createArraySchema(itemType) {
return {
type: "array",
items: {
type: itemType
}
}
}
Использование:
const numberArraySchema =
createArraySchema("number")
Иногда схема создаётся автоматически по образцу данных.
function inferSchema(data) {
if (typeof data === "string") {
return { type: "string" }
}
if (typeof data === "number") {
return { type: "number" }
}
if (Array.isArray(data)) {
return {
type: "array",
items: inferSchema(data[0])
}
}
if (typeof data === "object") {
const properties = {}
for (const key in data) {
properties[key] =
inferSchema(data[key])
}
return {
type: "object",
properties
}
}
}
Компиляция схемы в Ajv — дорогостоящая операция.
Неправильно:
app.post("/users", (req, res) => {
const schema = createSchema(req.user.role)
const validate = ajv.compile(schema)
validate(req.body)
})
Каждый запрос создаёт новый валидатор.
const cache = new Map()
function getValidator(role) {
if (!cache.has(role)) {
const schema = createSchema(role)
cache.set(
role,
ajv.compile(schema)
)
}
return cache.get(role)
}
Ajv позволяет регистрировать схемы во время выполнения.
ajv.addSchema({
$id: "user",
type: "object",
properties: {
name: {
type: "string"
}
}
})
Позже:
const validate = ajv.getSchema("user")
function createSchema(name) {
return {
$id: `schema://${name}`,
type: "object"
}
}
function createSchemas(modules) {
const schemas = {}
for (const module of modules) {
schemas[module.name] = {
$id: module.name,
type: "object",
properties: {
config: {
$ref: module.configSchema
}
}
}
}
return schemas
}
function createEventSchema(events) {
return {
oneOf: events.map(event => ({
type: "object",
properties: {
type: {
const: event.type
},
payload: event.payloadSchema
},
required: ["type", "payload"]
}))
}
}
const model = {
table: "users",
columns: [
{
name: "id",
type: "integer",
nullable: false
},
{
name: "email",
type: "string",
nullable: false
}
]
}
Генерация:
function schemaFromModel(model) {
const properties = {}
const required = []
for (const column of model.columns) {
properties[column.name] = {
type: column.type
}
if (!column.nullable) {
required.push(column.name)
}
}
return {
type: "object",
properties,
required
}
}
function schemaBuilder() {
const schema = {
type: "object",
properties: {}
}
return new Proxy(schema, {
get(target, prop) {
if (prop === "string") {
return name => {
target.properties[name] = {
type: "string"
}
return proxy
}
}
return target[prop]
}
})
}
Подобный подход используется в DSL и конструкторах схем.
function tenantSchema(tenant) {
const base = {
type: "object",
properties: {
id: {
type: "integer"
}
}
}
if (tenant.features.crm) {
base.properties.leadSource = {
type: "string"
}
}
if (tenant.features.analytics) {
base.properties.metrics = {
type: "array"
}
}
return base
}
OpenAPI уже содержит описания структур данных.
Пример:
components:
schemas:
User:
type: object
properties:
name:
type: string
Эти схемы могут автоматически использоваться в Ajv.
const schema = {
type: "object",
properties: {}
}
schema.properties.name = {
type: "string"
}
schema.required = ["name"]
Более безопасный стиль:
function extendSchema(schema, extra) {
return {
...schema,
properties: {
...schema.properties,
...extra
}
}
}
function registerMinWordsKeyword(limit) {
ajv.addKeyword({
keyword: `minWords${limit}`,
type: "string",
validate(schema, data) {
return data.split(" ").length >= limit
}
})
}
const ui = {
controls: [
{
name: "age",
component: "number-input"
}
]
}
Преобразование:
function uiToSchema(ui) {
const properties = {}
for (const control of ui.controls) {
if (control.component === "number-input") {
properties[control.name] = {
type: "number"
}
}
}
return {
type: "object",
properties
}
}
Частые ошибки:
Иногда выгоднее заранее подготовить все варианты.
const validators = {
admin: ajv.compile(
createSchema("admin")
),
user: ajv.compile(
createSchema("user")
)
}
class ValidatorRegistry {
constructor(ajv) {
this.ajv = ajv
this.cache = new Map()
}
get(type) {
if (!this.cache.has(type)) {
const schema = buildSchema(type)
this.cache.set(
type,
this.ajv.compile(schema)
)
}
return this.cache.get(type)
}
}
Ajv умеет валидировать сами схемы.
const valid = ajv.validateSchema(schema)
if (!valid) {
console.log(ajv.errors)
}
Это особенно важно при генерации сложных схем из внешних источников.
function buildPluginSchema(plugins) {
const properties = {}
for (const plugin of plugins) {
properties[plugin.name] =
plugin.schema
}
return {
type: "object",
properties
}
}
const schema = object({
name: string().required(),
age: number().min(18)
})
Подобные DSL обычно внутри генерируют JSON Schema для Ajv.
config.on("update", newConfig => {
const schema =
createSchema(newConfig)
validator =
ajv.compile(schema)
})
function featureSchema(flags) {
const properties = {
username: {
type: "string"
}
}
if (flags.betaProfile) {
properties.profileTheme = {
type: "string"
}
}
return {
type: "object",
properties
}
}
При создании схем из внешних данных возможны:
function buildSchema(node, depth = 0) {
if (depth > 10) {
throw new Error("Max depth exceeded")
}
return {
type: "object",
properties: {}
}
}
Часто динамическая генерация выглядит так:
Метаданные
↓
Генератор схем
↓
JSON Schema
↓
Ajv.compile()
↓
Кэш валидаторов
↓
Валидация данных
Хорошая практика:
schemas/
generators/
validators/
cache/
registry/
export function createUserSchema() {}
export function createOrderSchema() {}
export function createProductSchema() {}
export class SchemaCache {}
registry.register("user", schema)
registry.get("user")
Динамические схемы позволяют:
Ajv превращается не просто в валидатор, а в основу системы типизации и контрактов приложения.