npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

Install

npm install @spinajs/validation

The 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 / $ref of 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.

  • data is null / undefined → tryValidate returns an error with keyword invalid_argument; validate throws InvalidArgument('data is null or undefined').
  • The schema cannot be resolved (unknown id, or a plain object with no @Schema decorator) → tryValidate returns an error with keyword empty_schema; validate throws InvalidArgument('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 decorator

Reference 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 .json file is skipped with an error log,
  • a schema missing $id or 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.