Skip to main content

Overview

data-schema is a lightweight, sync-first validation library that implements the Standard Schema v1 specification. It provides a minimal API for runtime type validation with full TypeScript support. Key Features:
  • Standard Schema v1 compatible
  • Sync-first validation (no async overhead)
  • Minimal API surface
  • Runs anywhere JavaScript runs (browser, Node.js, Bun, Deno, Workers)
  • Composable with other Standard Schema libraries (Zod, Valibot, ArkType)
  • Built-in error message customization

Installation

npm i remix

Core Types

Schema

A sync, Standard Schema v1-compatible schema with a chainable API.
type Schema<input, output = input> = {
  '~standard': {
    version: 1
    vendor: string
    validate: (value: unknown, options?: ValidationOptions) => ValidationResult<output>
  }
  
  pipe(...checks: Check<output>[]): Schema<input, output>
  refine(predicate: (value: output) => boolean, message?: string): Schema<input, output>
}

ValidationResult

The result of schema validation.
type ValidationResult<output> =
  | { value: output }              // Success
  | { issues: Issue[] }            // Failure

Issue

A validation issue with a message and optional path.
type Issue = {
  message: string
  path?: Array<string | number>
}

API Reference

Parsing Functions

parse

Parses a value with a schema, throwing on validation failure.
schema
Schema<input, output>
required
Schema to validate against.
value
unknown
required
Value to parse.
options
ParseOptions
Parse options including errorMap and abortEarly.
result
output
Parsed and validated value.
import { object, string, number, parse } from 'remix/data-schema'

let User = object({ name: string(), age: number() })

try {
  let user = parse(User, { name: 'Ada', age: 37 })
  // user: { name: string; age: number }
} catch (error) {
  // ValidationError with issues array
}

parseSafe

Parses a value with a schema, returning a result object.
schema
Schema<input, output>
required
Schema to validate against.
value
unknown
required
Value to parse.
options
ParseOptions
Parse options including errorMap and abortEarly.
result
{ success: true; value: output } | { success: false; issues: Issue[] }
Result object with success flag.
import { object, string, number, parseSafe } from 'remix/data-schema'

let User = object({ name: string(), age: number() })
let result = parseSafe(User, input)

if (result.success) {
  let user = result.value
} else {
  console.error(result.issues)
}

Primitive Schemas

string

Validates that a value is a string.
schema
Schema<unknown, string>
String schema.
import { string } from 'remix/data-schema'

let schema = string()
parse(schema, 'hello') // 'hello'
parse(schema, 42)      // throws

number

Validates that a value is a finite number (rejects NaN and Infinity).
schema
Schema<unknown, number>
Number schema.
import { number } from 'remix/data-schema'

let schema = number()
parse(schema, 42)   // 42
parse(schema, NaN)  // throws

boolean

Validates that a value is a boolean.
schema
Schema<unknown, boolean>
Boolean schema.
import { boolean } from 'remix/data-schema'

let schema = boolean()
parse(schema, true)    // true
parse(schema, 'true')  // throws

bigint

Validates that a value is a bigint.
schema
Schema<unknown, bigint>
Bigint schema.
import { bigint } from 'remix/data-schema'

let schema = bigint()
parse(schema, 123n)  // 123n
parse(schema, 123)   // throws

symbol

Validates that a value is a symbol.
schema
Schema<unknown, symbol>
Symbol schema.
import { symbol } from 'remix/data-schema'

let schema = symbol()
let sym = Symbol('test')
parse(schema, sym)  // sym

null_

Validates that a value is null.
schema
Schema<unknown, null>
Null schema.
import { null_ } from 'remix/data-schema'

let schema = null_()
parse(schema, null)       // null
parse(schema, undefined)  // throws

undefined_

Validates that a value is undefined.
schema
Schema<unknown, undefined>
Undefined schema.
import { undefined_ } from 'remix/data-schema'

let schema = undefined_()
parse(schema, undefined)  // undefined
parse(schema, null)       // throws

any

Accepts any value without validation.
schema
Schema<any, unknown>
Any schema.
import { any } from 'remix/data-schema'

let schema = any()
parse(schema, 'anything')  // 'anything'
parse(schema, { any: 'value' })  // { any: 'value' }

Literal and Enum Schemas

literal

Validates that a value exactly matches a literal value.
value
Literal
required
Expected literal value.
schema
Schema<unknown, Literal>
Literal schema.
import { literal } from 'remix/data-schema'

let schema = literal('yes')
parse(schema, 'yes')  // 'yes'
parse(schema, 'no')   // throws

enum_

Validates that a value is one of the allowed enum values.
values
readonly string[]
required
Array of allowed values.
schema
Schema<unknown, values[number]>
Enum schema.
import { enum_ } from 'remix/data-schema'

let Status = enum_(['active', 'inactive', 'pending'] as const)
parse(Status, 'active')   // 'active'
parse(Status, 'deleted')  // throws

Collection Schemas

array

Validates an array by validating each element.
elementSchema
Schema<input, output>
required
Schema for array elements.
schema
Schema<unknown, output[]>
Array schema.
import { array, number } from 'remix/data-schema'

let schema = array(number())
parse(schema, [1, 2, 3])     // [1, 2, 3]
parse(schema, [1, 'two', 3]) // throws

tuple

Validates a fixed-length tuple with specific types for each position.
schemas
readonly Schema[]
required
Array of schemas for each tuple position.
schema
Schema<unknown, [...]>
Tuple schema.
import { tuple, string, number, boolean } from 'remix/data-schema'

let schema = tuple([string(), number(), boolean()])
parse(schema, ['hello', 42, true])  // ['hello', 42, true]
parse(schema, ['hello', 42])        // throws (wrong length)

object

Validates an object with a specific shape.
shape
Record<string, Schema>
required
Object shape definition.
options.unknownKeys
'strip' | 'passthrough' | 'error'
How to handle unknown keys. Defaults to ‘strip’.
schema
Schema<unknown, { [K in keyof shape]: InferOutput<shape[K]> }>
Object schema.
import { object, string, number } from 'remix/data-schema'

let User = object({
  name: string(),
  age: number(),
})

parse(User, { name: 'Ada', age: 37 })
// { name: 'Ada', age: 37 }

parse(User, { name: 'Ada', age: 37, extra: 'removed' })
// { name: 'Ada', age: 37 } (extra key stripped by default)

let StrictUser = object({ name: string() }, { unknownKeys: 'error' })
parse(StrictUser, { name: 'Ada', extra: 'causes error' })
// throws

record

Validates a record (dictionary) with typed keys and values.
keySchema
Schema<unknown, string>
required
Schema for keys.
valueSchema
Schema<input, output>
required
Schema for values.
schema
Schema<unknown, Record<string, output>>
Record schema.
import { record, string, number } from 'remix/data-schema'

let schema = record(string(), number())
parse(schema, { a: 1, b: 2 })  // { a: 1, b: 2 }
parse(schema, { a: 1, b: 'x' }) // throws

map

Validates a Map with typed keys and values.
keySchema
Schema<unknown, K>
required
Schema for keys.
valueSchema
Schema<unknown, V>
required
Schema for values.
schema
Schema<unknown, Map<K, V>>
Map schema.
import { map, string, number } from 'remix/data-schema'

let schema = map(string(), number())
let value = new Map([['a', 1], ['b', 2]])
parse(schema, value)  // Map(2) { 'a' => 1, 'b' => 2 }

set

Validates a Set with typed elements.
elementSchema
Schema<unknown, T>
required
Schema for set elements.
schema
Schema<unknown, Set<T>>
Set schema.
import { set, number } from 'remix/data-schema'

let schema = set(number())
let value = new Set([1, 2, 3])
parse(schema, value)  // Set(3) { 1, 2, 3 }

Modifier Schemas

nullable

Makes a schema accept null values.
schema
Schema<input, output>
required
Inner schema.
schema
Schema<input | null, output | null>
Nullable schema.
import { nullable, string } from 'remix/data-schema'

let schema = nullable(string())
parse(schema, 'hello')  // 'hello'
parse(schema, null)     // null

optional

Makes a schema accept undefined values.
schema
Schema<input, output>
required
Inner schema.
schema
Schema<input | undefined, output | undefined>
Optional schema.
import { optional, string } from 'remix/data-schema'

let schema = optional(string())
parse(schema, 'hello')     // 'hello'
parse(schema, undefined)   // undefined

defaulted

Provides a default value when the input is undefined.
schema
Schema<input, output>
required
Inner schema.
defaultValue
output
required
Default value to use.
schema
Schema<input | undefined, output>
Defaulted schema.
import { defaulted, string } from 'remix/data-schema'

let schema = defaulted(string(), 'guest')
parse(schema, 'Ada')       // 'Ada'
parse(schema, undefined)   // 'guest'

Advanced Schemas

union

Validates against multiple schemas, accepting the first match.
schemas
readonly Schema[]
required
Array of schemas to try.
schema
Schema<unknown, output>
Union schema.
import { union, string, number } from 'remix/data-schema'

let schema = union([string(), number()])
parse(schema, 'hello')  // 'hello'
parse(schema, 42)       // 42
parse(schema, true)     // throws

variant

Validates a discriminated union based on a discriminator field.
discriminator
string
required
Field name used for discrimination.
variants
Record<string, Schema>
required
Map of discriminator values to schemas.
schema
Schema<unknown, output>
Variant schema.
import { variant, object, literal, string, number } from 'remix/data-schema'

let Event = variant('type', {
  created: object({ type: literal('created'), id: string() }),
  updated: object({ type: literal('updated'), id: string(), version: number() }),
  deleted: object({ type: literal('deleted'), id: string() }),
})

parse(Event, { type: 'created', id: 'evt_1' })
// { type: 'created', id: 'evt_1' }

instanceof_

Validates that a value is an instance of a specific constructor.
constructor
Constructor<T>
required
Constructor function to check against.
schema
Schema<unknown, T>
Instance schema.
import { instanceof_, object } from 'remix/data-schema'

let schema = object({
  created: instanceof_(Date),
  pattern: instanceof_(RegExp),
})

parse(schema, {
  created: new Date(),
  pattern: /test/,
})

Schema Methods

pipe

Composes one or more checks onto a schema.
...checks
Check<output>[]
required
One or more checks to apply.
schema
Schema<input, output>
New schema with checks applied.
import { string } from 'remix/data-schema'
import { email, minLength } from 'remix/data-schema/checks'

let schema = string().pipe(minLength(3), email())
parse(schema, '[email protected]')  // '[email protected]'
parse(schema, 'ab')                // throws (too short)
parse(schema, 'not-an-email')     // throws (not email)

refine

Adds an inline predicate check.
predicate
(value: output) => boolean
required
Validation predicate.
message
string
Optional error message.
schema
Schema<input, output>
New schema with refinement.
import { number } from 'remix/data-schema'

let EvenNumber = number().refine(
  (n) => n % 2 === 0,
  'Expected an even number'
)

parse(EvenNumber, 4)  // 4
parse(EvenNumber, 3)  // throws

Built-in Checks

Available from remix/data-schema/checks.

String Checks

email()
Check<string>
Validates email format.
url()
Check<string>
Validates URL format.
uuid()
Check<string>
Validates UUID format.
length(length)
Check<string>
Validates exact string length.
minLength(min)
Check<string>
Validates minimum string length.
maxLength(max)
Check<string>
Validates maximum string length.
regex(pattern)
Check<string>
Validates against regex pattern.
startsWith(prefix)
Check<string>
Validates string starts with prefix.
endsWith(suffix)
Check<string>
Validates string ends with suffix.

Number Checks

min(min)
Check<number>
Validates minimum value.
max(max)
Check<number>
Validates maximum value.
integer()
Check<number>
Validates integer value.
positive()
Check<number>
Validates positive number (> 0).
negative()
Check<number>
Validates negative number (< 0).
nonNegative()
Check<number>
Validates non-negative number (>= 0).
nonPositive()
Check<number>
Validates non-positive number (<= 0).

Array Checks

minItems(min)
Check<unknown[]>
Validates minimum array length.
maxItems(max)
Check<unknown[]>
Validates maximum array length.
import { string, number, array } from 'remix/data-schema'
import { email, minLength, min, minItems } from 'remix/data-schema/checks'

let User = object({
  email: string().pipe(email()),
  username: string().pipe(minLength(3)),
  age: number().pipe(min(13)),
  tags: array(string()).pipe(minItems(1)),
})

Coercion

Available from remix/data-schema/coerce.

coerce.number

Coerces string inputs to numbers.
import * as coerce from 'remix/data-schema/coerce'
import { parse } from 'remix/data-schema'
import { min } from 'remix/data-schema/checks'

let schema = coerce.number().pipe(min(0))
parse(schema, '42')   // 42
parse(schema, 42)     // 42
parse(schema, 'abc')  // throws

coerce.boolean

Coerces string inputs to booleans.
import * as coerce from 'remix/data-schema/coerce'

let schema = coerce.boolean()
parse(schema, 'true')   // true
parse(schema, 'false')  // false
parse(schema, true)     // true
parse(schema, 'yes')    // throws

coerce.bigint

Coerces string and number inputs to bigints.
import * as coerce from 'remix/data-schema/coerce'

let schema = coerce.bigint()
parse(schema, '123')  // 123n
parse(schema, 123)    // 123n
parse(schema, 123n)   // 123n

coerce.date

Coerces string and number inputs to Dates.
import * as coerce from 'remix/data-schema/coerce'

let schema = coerce.date()
parse(schema, '2024-01-01')       // Date
parse(schema, 1704067200000)      // Date
parse(schema, new Date())         // Date

Error Handling

ValidationError

Error thrown by parse() when validation fails.
import { ValidationError, parse, string } from 'remix/data-schema'

try {
  parse(string(), 42)
} catch (error) {
  if (error instanceof ValidationError) {
    console.log(error.issues)
    // [{ message: 'Expected string', path: undefined }]
  }
}

Custom Error Messages

Use errorMap to customize validation messages.
import { object, string, parseSafe } from 'remix/data-schema'
import { minLength } from 'remix/data-schema/checks'

let schema = object({
  username: string().pipe(minLength(3)),
})

let result = parseSafe(schema, input, {
  errorMap(context) {
    if (context.code === 'string.min_length') {
      let min = (context.values as { min: number }).min
      return `Must be at least ${min} characters`
    }
  },
})

Type Inference

InferInput

Infers the input type of a schema.
import type { InferInput } from 'remix/data-schema'
import { object, string, number } from 'remix/data-schema'

let User = object({ name: string(), age: number() })
type UserInput = InferInput<typeof User>
// { name: unknown, age: unknown }

InferOutput

Infers the output type of a schema.
import type { InferOutput } from 'remix/data-schema'
import { object, string, number } from 'remix/data-schema'

let User = object({ name: string(), age: number() })
type UserOutput = InferOutput<typeof User>
// { name: string, age: number }

Integration

With data-table

Use data-schema for table-level validation.
import { column as c, table } from 'remix/data-table'
import { object, parse } from 'remix/data-schema'
import { email } from 'remix/data-schema/checks'

let UserSchema = object({
  email: string().pipe(email()),
  age: number().pipe(min(13)),
})

let users = table({
  name: 'users',
  columns: {
    id: c.uuid(),
    email: c.varchar(255),
    age: c.integer(),
  },
  validate({ value }) {
    try {
      let validated = parse(UserSchema, value)
      return { value: validated }
    } catch (error) {
      if (error instanceof ValidationError) {
        return { issues: error.issues }
      }
      throw error
    }
  },
})

Build docs developers (and LLMs) love