@spinajs/validation
v3.0.1
Published
validation library for spinajs framework
Readme
@spinajs/validation
JSON-schema validation for the SpinaJS framework, built on AJV with
ajv-formats, ajv-keywords and ajv-merge-patch pre-registered. Schemas can be attached to
classes with the @Schema decorator or loaded automatically from the filesystem.
Full documentation: docs/validation.
- Validating data
- Validator options
- The
@Schemadecorator - Runtime schemas
- Schema sources
- Schema providers
- Validation errors
- Customizing the validator
Install
npm install @spinajs/validationThe package is registered in the SpinaJS DI container. Resolve the validator wherever you need it:
import { DI } from '@spinajs/di';
import { DataValidator } from '@spinajs/validation';
const validator = await DI.resolve(DataValidator);Configuration
Options under the validation configuration key are passed to the AJV constructor, except two that are
always forced on: $data: true and verbose: true. Verbose errors let the validator spot $data rules;
every error it returns or throws is sanitised first (see ValidationFailed error details),
so the verbose data, schema and parentSchema never reach a caller. Defaults:
const config = {
validation: {
// report every error, not only the first one that occurred
allErrors: true,
// remove properties that are not defined in schema
// (only strips when the schema declares `additionalProperties: false`)
removeAdditional: false,
// fill in `default` values from the schema
useDefaults: true,
// coerce data to the types declared in the schema (e.g. "1" -> 1)
coerceTypes: true,
// disable AJV message generation (leave error messages to tools such as ajv-i18n)
messages: false,
},
};Schema directories are read from system.dirs.schemas (see filesystem schema sources).
DataValidator API
| method | description |
| --- | --- |
| validate(data) / validate(schema, data) | Validates data. Throws on failure. |
| tryValidate(data) / tryValidate(schema, data) | Validates data. Returns [isValid, errors] and never throws. |
| addSchema(schemaObject, id) | Registers a schema. Warns (and skips) if id is already registered. |
| removeSchema(id) | Removes a registered schema. No-op if it is not registered. |
| hasSchema(id) | Returns true if a schema with id is registered. |
| getSchema(id) | Returns the registered schema object (unwrapped) or undefined. |
schema may be:
- a schema object (used inline),
- a string id /
$refof a previously registered schema, - a class instance or constructor decorated with
@Schema(the schema is read from decorator metadata).
// throw-on-failure
try {
validator.validate('http://myapp/product.schema.json', { productId: 1 });
} catch (err) {
// err is a ValidationFailed exception
}
// non-throwing
const [isValid, errors] = validator.tryValidate('http://myapp/product.schema.json', { productId: 1 });
if (!isValid) {
console.log(errors);
}Strict behavior
The validator is intentionally strict: anything it cannot verify is treated as a failure rather than a silent pass.
dataisnull/undefined→tryValidatereturns an error with keywordinvalid_argument;validatethrowsInvalidArgument('data is null or undefined').- The schema cannot be resolved (unknown id, or a plain object with no
@Schemadecorator) →tryValidatereturns an error with keywordempty_schema;validatethrowsInvalidArgument('objects schema is not set').
const [ok, errors] = validator.tryValidate('unknown-schema-id', data);
// ok === false, errors[0].keyword === 'empty_schema'The @Schema decorator
Attach a schema to a class so its instances can be validated without naming a schema explicitly. Two forms are supported.
Inline schema object:
import { Schema } from '@spinajs/validation';
@Schema({
type: 'object',
properties: {
firstName: { type: 'string' },
age: { type: 'integer', minimum: 0 },
},
required: ['firstName'],
})
class Person {
firstName: string;
age: number;
}
const p = new Person();
p.firstName = 'John';
validator.validate(p); // schema is read from the decoratorReference to a registered schema by id ($ref form):
@Schema('http://myapp/product.schema.json')
class ProductDto {
productId: number;
}Both forms also register the class under the '__schemas__' DI map keyed by class name, so the schema can be
resolved by name (used by SchemaProvider, e.g. for OpenAPI generation).
Filesystem schema sources
On resolve, DataValidator loads every schema found in the directories listed in system.dirs.schemas.
Supported file types:
.json— parsed as JSON..js,.cjs,.mjs— imported dynamically; the schema must be the module's default export.
Each schema must declare a $id; it is registered under that id. Files are handled defensively:
- a JS/ESM module with no default export is skipped with a warning,
- a malformed
.jsonfile is skipped with an error log, - a schema missing
$idor failing AJV's schema validation is skipped with an error log.
A bad file never aborts loading of the other schemas.
// schemas/product.schema.mjs
export default {
$id: 'http://myapp/product.schema.json',
type: 'object',
properties: { productId: { type: 'integer' } },
required: ['productId'],
};Extending
Custom schema sources (SchemaSource)
Register additional sources (database, remote, etc.) by implementing SchemaSource. Every registered
source is loaded when the validator resolves.
import { Injectable } from '@spinajs/di';
import { SchemaSource, ISchema } from '@spinajs/validation';
@Injectable(SchemaSource)
export class MySchemaSource extends SchemaSource {
public async Load(): Promise<ISchema[]> {
return [{ schema: { $id: 'http://myapp/remote.schema.json', type: 'object' }, file: 'remote' }];
}
}Custom schema providers (SchemaProvider)
SchemaProvider maps a type name to its JSON schema (used e.g. by @spinajs/http-swagger for OpenAPI). The
built-in DtoSchemaProvider resolves @Schema-decorated DTO classes. Add your own:
import { Injectable } from '@spinajs/di';
import { SchemaProvider } from '@spinajs/validation';
@Injectable(SchemaProvider)
export class MySchemaProvider extends SchemaProvider {
public getSchema(typeName: string): Record<string, unknown> | undefined {
// return a schema, or undefined if this provider doesn't know the type
return undefined;
}
}getSchema is the write contract: what a client may SEND. A type whose stored shape differs from the
shape it is validated against on input can describe the way OUT separately by overriding the optional
getResponseSchema:
@Injectable(SchemaProvider)
export class MySchemaProvider extends SchemaProvider {
public getSchema(typeName: string): Record<string, unknown> | undefined {
// what a client may send — required fields, input-only constraints
return undefined;
}
public getResponseSchema(typeName: string): Record<string, unknown> | undefined {
// what the API hands back — generated columns, nullables, nothing `required`
return undefined;
}
}getResponseSchema is optional and returns undefined by default. A provider that makes no such
distinction — a plain @Schema DTO is the same object both ways — simply does not override it, and callers
fall back to getSchema. @spinajs/orm's ModelSchemaProvider does override it: a model's @Schema
describes what may be written, while its columns describe what a SELECT returns.
Consumers must treat it as optional (provider.getResponseSchema?.(name)): a provider compiled against an
older @spinajs/validation has no such method at all.
ValidationFailed error details
validate throws ValidationFailed on failure. Beyond the AJV errors in parameter (reduced to path, keyword,
params and message) it exposes helpers for inspecting what went wrong:
try {
validator.validate(schema, data);
} catch (err) {
if (err instanceof ValidationFailed) {
err.details; // [{ field, rule, message, params? }, ...]
err.schema; // the schema used for validation (not enumerable: never serialised)
err.getSummary(); // "field: message; field: message"
err.getFieldErrors('/user'); // errors for a field (exact + nested prefix match)
err.getFieldRequirements('user/email'); // schema node for a field, or null if unknown
err.toString(); // full multi-line report incl. schema $id
}
}details[].field is the failing JSON pointer (/user/email, or (root) for the top level), rule is the
AJV keyword (required, type, format, ...), and message falls back to a readable string when AJV
message generation is disabled (messages: false).
The exception never holds a value of the validated data: no details[].value, no data, and parameter
keeps only instancePath, schemaPath, keyword, params, message (and propertyName) of each AJV
error, also with AJV's verbose option. A 400 built from it cannot echo a password or a token back.
A rule written with $data (confirm: { const: { $data: '1/password' } }) reports params: { $data: '1/password' }
and a message naming only the keyword and the pointer, since AJV would build both from the referenced field;
the discriminator tagValue is dropped. tryValidate and validateSchema return errors reduced the same way
(the exported sanitizeError does it).
A ValidationFailed built by hand takes (message, errors, schema?). To redact $data rules it needs AJV
errors from a verbose run (they carry parentSchema) or the schema argument. With neither, the params and
message of a $data rule stay as AJV built them, request value included.
