valrs

Standard Schema

How valrs implements the Standard Schema specification

Standard Schema Compliance

valrs implements the Standard Schema specification v1, enabling seamless interoperability with any tool or library that supports the standard.

What is Standard Schema?

Standard Schema is an open specification that defines a common interface for schema validation libraries. It solves a fundamental problem in the JavaScript ecosystem: validation libraries have historically been incompatible with each other, forcing framework authors to write adapters for each library they want to support.

With Standard Schema, any compliant library can work with any compliant tool. This means:

  • Form libraries can validate using valrs, Zod, Valibot, or any other compliant library
  • API frameworks can accept schemas from any compliant source
  • Documentation tools can generate specs from any compliant schema
  • You can switch validation libraries without rewriting your integration code

How valrs Implements Standard Schema

Every valrs schema exposes the Standard Schema interface through the ~standard property. This is an internal implementation detail that enables interoperability while keeping the public API clean.

The ~standard Interface

The ~standard property is a namespaced object (using the ~ prefix to avoid conflicts) that contains everything a Standard Schema consumer needs:

interface StandardSchemaV1<Input = unknown, Output = Input> {
  readonly '~standard': {
    readonly version: 1;
    readonly vendor: string;
    readonly validate: (
      value: unknown
    ) => ValidationResult<Output> | Promise<ValidationResult<Output>>;
  };
}
PropertyTypeDescription
version1The Standard Schema specification version
vendorstringThe library identifier ('valrs')
validatefunctionValidates unknown input, returning sync or async result

Validation Results

The validate function returns a discriminated union that cleanly separates success from failure:

type ValidationResult<T> =
  | { readonly value: T; readonly issues?: undefined }      // Success
  | { readonly issues: ReadonlyArray<ValidationIssue>; readonly value?: undefined }; // Failure

When validation succeeds, the result contains the typed value. When it fails, the result contains an array of issues describing what went wrong:

interface ValidationIssue {
  readonly message: string;
  readonly path?: ReadonlyArray<PathSegment>;
}
 
type PathSegment = string | number | { readonly key: string | number };

Type Guards

valrs exports type guards for working with validation results:

import { isValidationSuccess, isValidationFailure } from 'valrs';
 
const result = schema['~standard'].validate(data);
 
if (isValidationSuccess(result)) {
  // result.value is typed correctly
  console.log(result.value);
}
 
if (isValidationFailure(result)) {
  // result.issues is available
  console.error(result.issues);
}

Interoperability with Other Libraries

The power of Standard Schema is that valrs works seamlessly alongside other compliant libraries like Zod and Valibot.

Writing Library-Agnostic Code

Any function that accepts StandardSchemaV1 will work with valrs:

import type { StandardSchemaV1, InferOutput } from 'valrs';
 
async function validate<T extends StandardSchemaV1>(
  schema: T,
  value: unknown
): Promise<InferOutput<T> | null> {
  const result = await schema['~standard'].validate(value);
  return result.issues === undefined ? result.value : null;
}
 
// Works with any compliant schema
await validate(StringSchema, 'hello');     // valrs
await validate(z.string(), 'hello');       // zod
await validate(v.string(), 'hello');       // valibot

Mixed Schema Collections

You can mix schemas from different libraries in the same application:

import { StringSchema, Int32Schema } from 'valrs';
import { z } from 'zod';
import * as v from 'valibot';
 
// All implement StandardSchemaV1
const schemas: StandardSchemaV1[] = [
  StringSchema,              // valrs
  z.number().positive(),     // zod
  v.email(),                 // valibot
];
 
// Process uniformly
for (const schema of schemas) {
  const result = schema['~standard'].validate(input);
  // Handle result...
}

JSON Schema Extension

valrs also implements StandardJSONSchemaV1, which extends the base interface with JSON Schema generation:

interface StandardJSONSchemaV1<Input = unknown, Output = Input>
  extends StandardSchemaV1<Input, Output> {
  readonly '~standard': StandardSchemaV1<Input, Output>['~standard'] & {
    readonly jsonSchema: {
      readonly input: (options: JsonSchemaOptions) => Record<string, unknown>;
      readonly output: (options: JsonSchemaOptions) => Record<string, unknown>;
    };
  };
}

This enables tools to generate JSON Schema from your valrs schemas:

import { StringSchema } from 'valrs';
 
const jsonSchema = StringSchema['~standard'].jsonSchema.input({
  target: 'draft-2020-12'
});
console.log(jsonSchema); // { type: 'string' }

Type Inference

valrs exports utility types for extracting input and output types from any Standard Schema:

import type { InferInput, InferOutput, StandardSchemaV1 } from 'valrs';
 
// Infer types from valrs schemas
type StringInput = InferInput<typeof StringSchema>;   // string
type StringOutput = InferOutput<typeof StringSchema>; // string
 
// Works with transforming schemas
const TransformSchema = createSchema<string, number>(/* ... */);
type TransformInput = InferInput<typeof TransformSchema>;   // string
type TransformOutput = InferOutput<typeof TransformSchema>; // number
 
// Works in generic contexts
function getDefault<T extends StandardSchemaV1>(
  schema: T
): InferOutput<T> | undefined {
  // ...
}

Checking Schema Compliance

You can check if an unknown object implements Standard Schema:

function isStandardSchema(obj: unknown): obj is StandardSchemaV1 {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    '~standard' in obj &&
    typeof (obj as Record<string, unknown>)['~standard'] === 'object' &&
    (obj as { '~standard': { version: unknown } })['~standard'].version === 1 &&
    typeof (obj as { '~standard': { validate: unknown } })['~standard'].validate === 'function'
  );
}
 
// Usage
if (isStandardSchema(unknownSchema)) {
  const result = await unknownSchema['~standard'].validate(data);
}

Why the ~standard Property?

The ~ prefix is intentional. It:

  1. Avoids conflicts with user-defined properties
  2. Signals internal use - consumers should use the public API when available
  3. Enables interoperability without polluting the schema object's namespace

You typically do not need to access ~standard directly when using valrs. The public API (parse, safeParse, etc.) is more ergonomic. The ~standard interface exists for framework and library authors who need to work with schemas generically.

Next Steps