validate

js/validate.ts

fino:validate - JSON Schema validation with fluent builders.

JSON Schema specification: https://json-schema.org/specification

This module treats JSON Schema as the canonical schema representation. The builder API is only a convenient way to construct JSON-Schema-shaped objects; builders serialize directly with toJSON(), so they can be passed to JSON.stringify() or stored on disk without a conversion step.

Raw JSON Schema objects are accepted anywhere a builder is accepted. This is intentional: applications can load schemas from disk, receive schemas from tools, or build schemas fluently in code while using the same validator pipeline.

The supported JSON Schema subset is intentionally small and runtime-focused: type, const, enum, properties, required, additionalProperties, items, prefixItems, anyOf, string length/pattern/format constraints, numeric minimum/maximum, and array length constraints. Unknown keywords are preserved on schemas for tooling compatibility but ignored by validation. Supported string formats are email, url, and uri.

This is not a full JSON Schema implementation. $ref, $defs, definitions, oneOf, allOf, not, pattern-property/dependency keywords, unevaluated keywords, and unknown string formats are not resolved or enforced in this release. Validate schemas with a full JSON Schema engine first when unsupported keywords should be treated as application errors.

Validators are compiled into closure graphs. The implementation avoids generated source and eval, but still avoids re-walking the schema metadata for every input value.

import { parse, v } from 'fino:validate';

const schema = v.object({
  name: v.string().min(1),
  port: v.integer().min(1).max(65535).default(3000),
});

const config = parse(schema, { name: 'api' });
JSON.stringify(schema); // valid JSON Schema

Types

type JsonSchema = Record<string, unknown>

JSON-Schema-shaped object accepted by validators and builders.

This type is intentionally broad because callers can pass raw JSON Schema objects or builder output. Unsupported JSON Schema keywords are preserved in the object but ignored by this validator.

import type { JsonSchema } from 'fino:validate';

const schema: JsonSchema = { type: 'string', minLength: 1 };

Interfaces

interface ValidationIssue {

One validation failure at a concrete input path.

Issues are collected during traversal and attached to ValidationError. safeParse() also exposes the array directly for callers that do not want to inspect the error object.

import type { ValidationIssue } from 'fino:validate';

const issue: ValidationIssue = { path: 'name', keyword: 'minLength', message: 'too short' };

Properties

path: string

Dotted path to the invalid value; empty string means the root value.

Array indexes use bracket notation, such as items[0].

import type { ValidationIssue } from 'fino:validate';

const issue: ValidationIssue = { path: 'items[0]', keyword: 'type', message: 'expected string' };
message: string

Human-readable diagnostic.

Messages are intended for developer diagnostics and simple application errors; localize or rewrite them before exposing them in user-facing UI.

import type { ValidationIssue } from 'fino:validate';

const issue: ValidationIssue = { path: 'age', keyword: 'minimum', message: 'must be >= 18' };
keyword: string

JSON Schema keyword or fino refinement that failed.

Examples include type, required, minimum, anyOf, and refine.

import type { ValidationIssue } from 'fino:validate';

const issue: ValidationIssue = { path: '', keyword: 'required', message: 'is required' };
value?: unknown

The received value, when useful for diagnostics.

Some issues omit this field, such as missing required properties where the value is undefined.

import type { ValidationIssue } from 'fino:validate';

const issue: ValidationIssue = { path: 'enabled', keyword: 'type', message: 'expected boolean', value: 'yes' };

interface SafeParseSuccess<T = unknown> {

Successful safeParse() result.

The discriminant is success: true; value contains the parsed value with defaults applied.

import type { SafeParseSuccess } from 'fino:validate';

const result: SafeParseSuccess<string> = { success: true, value: 'ok' };

Properties

success: true

Success discriminant.

Use it to narrow the union returned by safeParse().

import { safeParse, v } from 'fino:validate';

const result = safeParse(v.string(), 'ok');
if (result.success) result.value.toUpperCase();
value: T

Parsed value, including applied defaults.

The value may be cloned for defaulted objects and arrays so callers can mutate it without changing the schema default.

import type { SafeParseSuccess } from 'fino:validate';

const result: SafeParseSuccess<number> = { success: true, value: 42 };

interface SafeParseFailure {

Failed safeParse() result.

The discriminant is success: false; error and issues describe all validation failures found during traversal.

import { safeParse, v } from 'fino:validate';

const result = safeParse(v.integer(), 'nope');
if (!result.success) result.issues;

Properties

success: false

Failure discriminant.

Use it to narrow the union returned by safeParse().

import { safeParse, v } from 'fino:validate';

const result = safeParse(v.boolean(), 'yes');
if (!result.success) result.error.message;
error: ValidationError

Error object containing the same issues.

This is the same error shape thrown by parse().

import { safeParse, v } from 'fino:validate';

const result = safeParse(v.number(), 'x');
if (!result.success) result.error.issues;
issues: ValidationIssue[]

Individual validation issues.

The array is exposed directly so callers can inspect failures without touching the error object.

import { safeParse, v } from 'fino:validate';

const result = safeParse(v.string().min(2), 'a');
if (!result.success) result.issues[0]?.keyword;

Classes

class ValidationError extends Error {

Error thrown by parse() when validation fails.

The message summarizes all issues as path: message pairs. Use the issues property for structured handling. safeParse() returns this error instead of throwing it.

import { ValidationError, parse, v } from 'fino:validate';

try {
  parse(v.string(), 123);
} catch (error) {
  if (error instanceof ValidationError) error.issues;
}

Properties

issues: ValidationIssue[]

All validation issues found during traversal.

The array is stored as provided to the constructor and is not deep-cloned.

import { ValidationError } from 'fino:validate';

const error = new ValidationError([{ path: '', keyword: 'type', message: 'expected string' }]);
error.issues;

Constructors

constructor(issues: ValidationIssue[])

Create a validation error from collected issues.

The error name is set to ValidationError, and the message is built from the issue paths and messages. An empty issue array creates a valid error object but is not produced by the parser.

import { ValidationError } from 'fino:validate';

const error = new ValidationError([{ path: 'name', keyword: 'required', message: 'is required' }]);

class CompiledValidator<T = unknown> {

Reusable compiled validator for a builder or raw JSON Schema object.

The constructor compiles the schema once into closure-based validators. Reuse instances for repeated parsing of the same schema to avoid recompilation.

import { CompiledValidator, v } from 'fino:validate';

const validator = new CompiledValidator<string>(v.string().min(1));
const value = validator.parse('ok');

Constructors

constructor(schema: unknown)

Compile schema immediately.

Accepts a SchemaBuilder or raw JSON Schema object. Non-object schemas throw Error. The compiled validator keeps a reference to the schema object, so avoid mutating builders after compiling them.

import { CompiledValidator, v } from 'fino:validate';

const validator = new CompiledValidator<number>(v.integer().min(1));

Getters

get schema(): JsonSchema

Original canonical JSON Schema object used by this validator.

The returned object is the same object captured during construction, not a clone. Mutating it after compilation does not rebuild the closure graph.

import { compile, v } from 'fino:validate';

const validator = compile(v.string());
const schema = validator.schema;

Methods

parse(value: unknown): T

Parse value and return the validated value.

Defaults are applied to missing values. Throws ValidationError when any issue is found.

import { compile, v } from 'fino:validate';

const validator = compile<string>(v.string().min(1));
const value = validator.parse('name');
safeParse(value: unknown): SafeParseSuccess<T> | SafeParseFailure

Parse value without throwing.

The success branch contains the parsed value. The failure branch contains a ValidationError plus the issue array for direct inspection.

import { compile, v } from 'fino:validate';

const validator = compile<number>(v.number());
const result = validator.safeParse('nope');

class SchemaBuilder<T = unknown> {

Base fluent builder.

The schema property is the actual JSON Schema object. Builder methods mutate and return the same builder so users can fluently compose constraints while still preserving direct JSON serialization.

const documentedClass = 'SchemaBuilder';
console.log(documentedClass);

Properties

schema: JsonSchema

Mutable JSON Schema object represented by this builder.

Builder methods update this object and return the same builder. It can be passed anywhere a raw JSON Schema object is accepted.

import { v } from 'fino:validate';

const schema = v.string().schema;

Constructors

constructor(schema: JsonSchema, optional = false)

Create a builder around an existing JSON Schema object.

optional marks the schema as optional when it is used in v.object(). The marker is stored as a non-enumerable symbol and is not emitted by JSON.stringify().

import { SchemaBuilder } from 'fino:validate';

const builder = new SchemaBuilder<string>({ type: 'string' });

Methods

toJSON(): JsonSchema

Return the canonical JSON Schema object for JSON.stringify().

Custom refinements and optional markers are non-enumerable symbol metadata, so they are intentionally omitted from serialized JSON Schema.

import { v } from 'fino:validate';

const json = JSON.stringify(v.string().min(1).toJSON());
parse(value: unknown): T

Validate value with this schema and throw on failure.

This compiles the builder for the call, applies defaults, and returns the parsed value. Validation failures throw ValidationError.

import { v } from 'fino:validate';

const value = v.integer().min(1).parse(3);
safeParse(value: unknown): SafeParseSuccess<T> | SafeParseFailure

Validate value with this schema and return a tagged result.

This compiles the builder for the call. Success returns { success: true, value }; failure returns { success: false, error, issues }.

import { v } from 'fino:validate';

const result = v.string().safeParse(123);
optional(): this

Mark this schema as optional when used as an object property.

Optional properties are omitted from the generated required array in v.object(). The method mutates and returns the same builder.

import { v } from 'fino:validate';

const schema = v.object({ nickname: v.string().optional() });
nullable(): SchemaBuilder<T | null>

Accept this schema or null.

Returns a new builder using anyOf with the current schema and { type: 'null' }. The original builder is not marked optional.

import { v } from 'fino:validate';

const schema = v.string().nullable();
default(value: unknown): this

Apply this default when the input value is missing.

Defaults are used when the input is undefined; object and array defaults are recursively cloned before returning parsed output.

import { v } from 'fino:validate';

const schema = v.integer().default(3000);
describe(text: string): this

Attach a human-readable description to the schema.

The description surfaces in generated JSON Schema, OpenAPI 3.1 output, and LLM tool parameter specs, so it doubles as documentation for both machines and humans.

import { v } from 'fino:validate';

const schema = v.string().describe('The user's display name');
refine(fn: (value: T) => boolean, message = 'failed custom validation'): this

Attach a custom in-process refinement.

Refinements cannot be represented in JSON Schema. They are preserved on the builder object for runtime validation, but they are intentionally omitted when serialized with toJSON().

import { v } from 'fino:validate';

const schema = v.string().refine((value) => value.startsWith('x-'), 'must start with x-');

Constants

const v

Fluent builder namespace.

Every builder method returns a schema builder whose toJSON() result is a JSON-Schema-shaped object. Raw JSON Schema objects can be mixed with builders anywhere a child schema is accepted.

const documentedMember = 'v';
console.log(documentedMember);

Methods

any(): SchemaBuilder<unknown>

Create a schema that accepts any value.

The generated JSON Schema is {}. Use this for intentionally unvalidated extension points or values that are validated elsewhere.

import { v } from 'fino:validate';

const schema = v.any();
string(): StringBuilder

Create a string schema builder.

Chain min(), max(), length(), pattern(), or format() to add string constraints.

import { v } from 'fino:validate';

const schema = v.string().min(1).max(64);
number(): NumberBuilder

Create a number schema builder.

The schema accepts finite JavaScript numbers. Chain min() and max() to add numeric bounds.

import { v } from 'fino:validate';

const schema = v.number().min(0);
integer(): NumberBuilder

Create an integer schema builder.

The schema accepts JavaScript numbers that satisfy Number.isInteger(). Chain min() and max() to add integer bounds.

import { v } from 'fino:validate';

const schema = v.integer().min(1).max(65535);
boolean(): SchemaBuilder<boolean>

Create a boolean schema builder.

import { v } from 'fino:validate';

const schema = v.boolean().default(false);
null(): SchemaBuilder<null>

Create a schema that accepts only null.

Use someSchema.nullable() when a non-null schema should also accept null.

import { v } from 'fino:validate';

const schema = v.null();
literal(value: unknown): SchemaBuilder<unknown>

Create a constant-value schema.

The value is stored as JSON Schema const and must compare equal during validation.

import { v } from 'fino:validate';

const schema = v.literal('production');
enum(values: unknown[]): SchemaBuilder<unknown>

Create an enum schema from allowed values.

The values array is copied into JSON Schema enum, so later mutations to the caller's array do not change the schema.

import { v } from 'fino:validate';

const schema = v.enum(['dev', 'prod']);
array(item: unknown): ArrayBuilder

Create an array schema with one item schema.

item may be another builder or a raw JSON Schema object. Chain min() and max() to constrain item count.

import { v } from 'fino:validate';

const schema = v.array(v.string()).min(1);
tuple(items: unknown[]): ArrayBuilder

Create a fixed-length tuple schema.

items may contain builders or raw JSON Schema objects. The generated schema uses prefixItems and sets minItems and maxItems to the tuple length.

import { v } from 'fino:validate';

const schema = v.tuple([v.string(), v.integer()]);
union(items: unknown[]): SchemaBuilder<unknown>

Create a union schema.

items may contain builders or raw JSON Schema objects. The generated schema uses JSON Schema anyOf; validation succeeds when any branch accepts the value.

import { v } from 'fino:validate';

const schema = v.union([v.string(), v.integer()]);

Properties

object

Create an object schema from a property shape.

Shape values may be builders or raw JSON Schema objects. Properties are required by default; call .optional() on a builder to omit that property from the generated required list.

import { v } from 'fino:validate';

const schema = v.object({
  name: v.string(),
  nickname: v.string().optional(),
});

Functions

function compile<T = unknown>(schema: unknown): CompiledValidator<T>

Compile a builder or raw JSON Schema object into a reusable validator.

Use this when validating many values with the same schema. Non-object schemas throw Error; validation failures occur later when calling parse() or safeParse() on the returned validator.

import { compile, v } from 'fino:validate';

const validator = compile<{ name: string }>(v.object({ name: v.string() }));

function parse<T = unknown>(schema: unknown, value: unknown): T

Parse a value once with a builder or raw JSON Schema object.

This compiles the schema for the call, applies defaults, and returns the parsed value. Validation failures throw ValidationError.

import { parse, v } from 'fino:validate';

const value = parse<string>(v.string().min(1), 'ok');

function safeParse<T = unknown>( schema: unknown, value: unknown ): SafeParseSuccess<T> | SafeParseFailure

Parse a value once and return a tagged result instead of throwing.

This compiles the schema for the call. Success returns { success: true, value }; failure returns { success: false, error, issues }.

import { safeParse, v } from 'fino:validate';

const result = safeParse<number>(v.integer(), 'not an integer');