Standard Schema Validation
Standard Schemaย is a convention supported by modern validation libraries that implement a common interface for defining and validating data structures. It is supported by libraries such as Zod 4ย , ArkTypeย , Valibotย , and more. See the full list of implementations in the repositoryย .
Vovk.ts provides a simple createStandardValidation function to build a custom validation library using Standard Schema. Its key option is toJSONSchema, which defines how to convert models to JSON Schema.
ArkType Example
Create withArk somewhere in your codebase:
import { createStandardValidation } from 'vovk';
import { type } from 'arktype';
export const withArk = createStandardValidation({
toJSONSchema: (model: type) =>
model.toJsonSchema({
fallback: {
proto: (ctx) =>
ctx.proto === File
? {
type: 'string',
format: 'binary',
}
: ctx.base,
default: (ctx) => ctx.base,
},
}),
});Then use it in your controller:
import { prefix, post, operation } from 'vovk';
import { type } from 'arktype';
import { withArk } from '@/lib/withArk';
@prefix('users')
export default class UserController {
@operation({
summary: 'Update user (Arktype)',
description: 'Update user by ID with Arktype validation',
})
@post('{id}')
static updateUser = withArk({
body: type({
name: type('string').describe('User full name'),
age: type('0 < number < 120').describe('User age'),
email: type('string.email').describe('User email'),
}),
params: type({
id: type('string.uuid').describe('User ID'),
}),
query: type({
notify: type('"email" | "push" | "none"').describe('Notification type'),
}),
output: type({
success: type('boolean').describe('Success status'),
}),
async handle(req, { id }) {
const { name, age } = await req.json();
const notify = req.nextUrl.searchParams.get('notify');
// do something with the data
console.log(`Updating user ${id}:`, { name, age, notify });
return {
success: true,
};
},
});
}View live example on examples.vovk.dev ยปย
Valibot Example
Create withValibot somewhere in your codebase:
import { createStandardValidation } from 'vovk';
import { toJsonSchema } from '@valibot/to-json-schema';
export const withValibot = createStandardValidation({
toJSONSchema: (model) =>
toJsonSchema(model, {
overrideSchema(context) {
if (context.valibotSchema.type === 'file') {
return { type: 'string', format: 'binary' };
}
},
}),
});Then use it in your controller:
import { prefix, post, operation } from 'vovk';
import * as v from 'valibot';
import { withValibot } from '@/lib/withValibot';
@prefix('users')
export default class UserController {
@operation({
summary: 'Update user (Valibot)',
description: 'Update user by ID with Valibot validation',
})
@post('{id}')
static updateUser = withValibot({
body: v.pipe(
v.object({
name: v.pipe(v.string(), v.description('User full name')),
age: v.pipe(v.number(), v.minValue(0), v.maxValue(120), v.description('User age')),
email: v.pipe(v.string(), v.email(), v.description('User email')),
}),
v.description('User object')
),
params: v.object({
id: v.pipe(v.string(), v.uuid(), v.description('User ID')),
}),
query: v.object({
notify: v.pipe(v.picklist(['email', 'push', 'none']), v.description('Notification type')),
}),
output: v.pipe(
v.object({
success: v.pipe(v.boolean(), v.description('Success status')),
}),
v.description('Response object')
),
async handle(req, { id }) {
const { name, age } = await req.json();
const notify = req.nextUrl.searchParams.get('notify');
// do something with the data
console.log(`Updating user ${id}:`, { name, age, notify });
return {
success: true,
};
},
});
}View live example on examples.vovk.dev ยปย
Other libraries follow the same pattern; only the toJSONSchema implementation differs. Once standard-schema/issues/21ย is resolved, using createStandardValidation will likely be unnecessary in most cases.