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
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.
Parse options including errorMap and abortEarly.
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.
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.
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).
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.
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.
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.
import { symbol } from 'remix/data-schema'
let schema = symbol()
let sym = Symbol('test')
parse(schema, sym) // sym
null_
Validates that a value is null.
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.
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.
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.
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.
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.
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.
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.
Field name used for discrimination.
variants
Record<string, Schema>
required
Map of discriminator values to schemas.
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 function to check against.
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.
One or more checks to apply.
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.
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
Validates exact string length.
Validates minimum string length.
Validates maximum string length.
Validates against regex pattern.
Validates string starts with prefix.
Validates string ends with suffix.
Number Checks
Validates positive number (> 0).
Validates negative number (< 0).
Validates non-negative number (>= 0).
Validates non-positive number (<= 0).
Array Checks
Validates minimum array length.
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
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
}
},
})