@amazon/vinyl-validation
A TypeScript-first validation library for runtime type checking and schema validation. Composable validators for primitives, objects, arrays, sets, records, tuples, functions, and unions, with strong inference of the result type.
Install
npm install @amazon/vinyl-validation
Basic Usage
import { string, number } from '@amazon/vinyl-validation'
const nameValidator = string().notEmpty()
const ageValidator = number().gte(0)
nameValidator.isValid('John') // true
ageValidator.isValid(-5) // false
// Get validation errors
const errors = nameValidator.validate('')
// [{ message: 'Expected: not empty, but was: "". At: ', path: [] }]
Core Validators
Primitive Types
import { string, number, boolean, symbol, any } from '@amazon/vinyl-validation'
const stringValidator = string()
const numberValidator = number()
const booleanValidator = boolean()
const symbolValidator = symbol()
const anyValidator = any() // accepts any value
Null and Undefined
import {
exactlyNull,
exactlyUndefined,
nullish,
} from '@amazon/vinyl-validation'
const nullValidator = exactlyNull() // only null
const undefinedValidator = exactlyUndefined() // only undefined
const nullishValidator = nullish() // null or undefined
Value Matching
import { isOneOf, instanceOf } from '@amazon/vinyl-validation'
const statusValidator = isOneOf('active', 'inactive', 'pending')
const dateValidator = instanceOf(Date)
const errorValidator = instanceOf(Error)
String Validation
import { string } from '@amazon/vinyl-validation'
const validator = string()
.notEmpty()
.minLength(3)
.maxLength(50)
.noWhitespace()
.matches(/^[a-z]+$/)
validator.isValid('hello') // true
validator.isValid('') // false (empty)
validator.isValid('ab') // false (too short)
Number Validation
import { number } from '@amazon/vinyl-validation'
const validator = number().gte(0).lte(100).within(1, 99).safeInteger().finite()
validator.isValid(50) // true
validator.isValid(-1) // false (less than 0)
validator.isValid(Infinity) // false (not finite)
Object Validation
import {
object,
string,
number,
isOneOf,
array,
} from '@amazon/vinyl-validation'
const userValidator = object({
name: string().notEmpty(),
age: number().gte(0),
email: string().matches(/^[^\s@]+@[^\s@]+\.[^\s@]+$/),
})
const user = { name: 'John', age: 30, email: 'john@example.com' }
userValidator.isValid(user) // true
// Extend existing schemas
const adminValidator = userValidator.extend({
role: isOneOf('admin', 'superadmin'),
permissions: array(string()),
})
Array Validation
import { array, string, number, tuple, boolean } from '@amazon/vinyl-validation'
const stringArrayValidator = array(string())
const numberArrayValidator = array(number().gte(0))
.minLength(1)
.maxLength(10)
.notEmpty()
// Tuples (fixed-length, positional)
const coordinateValidator = tuple(number(), number()) // [x, y]
const personValidator = tuple(string(), number(), boolean()) // [name, age, active]
stringArrayValidator.isValid(['a', 'b', 'c']) // true
coordinateValidator.isValid([10, 20]) // true
coordinateValidator.isValid([10, 20, 30]) // false (wrong length)
Record Validation
import { record, recordValues, string, number } from '@amazon/vinyl-validation'
const scoresValidator = recordValues(number().gte(0))
const configValidator = record(
string().matches(/^[A-Z_]+$/),
string().notEmpty()
)
scoresValidator.isValid({ alice: 95, bob: 87 }) // true
configValidator.isValid({ API_KEY: 'secret', DB_URL: 'localhost' }) // true
Set Validation
import { set, string } from '@amazon/vinyl-validation'
const tagValidator = set(string().notEmpty())
tagValidator.isValid(new Set(['tag1', 'tag2'])) // true
tagValidator.isValid(new Set(['tag1', ''])) // false (empty string)
Function Validation
import { func } from '@amazon/vinyl-validation'
const functionValidator = func().withArity(2).withMinArity(1).withMaxArity(3)
functionValidator.isValid((a, b) => a + b) // true
functionValidator.isValid(() => 'hello') // false (wrong arity)
Optional and Nullable Values
import { string } from '@amazon/vinyl-validation'
const optionalName = string().optional() // allows undefined
const nullableName = string().nullable() // allows null
const maybeName = string().maybe() // allows null or undefined
optionalName.isValid(undefined) // true
nullableName.isValid(null) // true
maybeName.isValid(null) // true
maybeName.isValid(undefined) // true
Combining Validators
OR Logic
import { orValidators, string, number } from '@amazon/vinyl-validation'
const stringOrNumber = orValidators(string(), number())
stringOrNumber.isValid('hello') // true
stringOrNumber.isValid(42) // true
stringOrNumber.isValid(true) // false
AND Logic
import { andValidators, string } from '@amazon/vinyl-validation'
const shortUppercaseString = andValidators(
string().maxLength(10),
string().matches(/^[A-Z]+$/)
)
shortUppercaseString.isValid('HELLO') // true
shortUppercaseString.isValid('hello') // false (not uppercase)
shortUppercaseString.isValid('VERYLONGSTRING') // false (too long)
Custom Validators
import { custom } from '@amazon/vinyl-validation'
const evenNumberValidator = custom<number>(
'even number',
(input): input is number => typeof input === 'number' && input % 2 === 0
)
evenNumberValidator.isValid(4) // true
evenNumberValidator.isValid(3) // false
Validation Options
import { string, object } from '@amazon/vinyl-validation'
const validator = object({
name: string().notEmpty(),
email: string().matches(/^[^\s@]+@[^\s@]+\.[^\s@]+$/),
})
// Collect all errors (default: stop at first error)
const errors = validator.validate({ name: '', email: 'invalid' }, { all: true })
Error Handling
import { ValidationError } from '@amazon/vinyl-util'
import { string } from '@amazon/vinyl-validation'
const validator = string().notEmpty()
try {
validator.assert('') // throws ValidationError if invalid
} catch (error) {
if (error instanceof ValidationError) {
console.log(error.message)
}
}
TypeScript Integration
import { object, string, number, array } from '@amazon/vinyl-validation'
interface User {
name: string
age?: number
tags: string[]
}
const userValidator = object<User>({
name: string(),
age: number().optional(),
tags: array(string()),
})
function processUser(data: unknown) {
if (userValidator.isValid(data)) {
console.log(data.name) // typed as string
}
}
Descriptions and JSON Schema Export
Any schema node can carry a human-readable description, and any schema can be
exported as a JSON Schema definition. This is useful
for describing a shape to an LLM or another consumer, and works similarly to
zod's describe and zod-to-json-schema.
import {
object,
string,
number,
isOneOf,
toJsonSchema,
} from '@amazon/vinyl-validation'
const userSchema = object({
name: string().notEmpty().describe('The user full name'),
age: number().gte(0).lte(120),
role: isOneOf('admin', 'user').describe('Access level'),
nickname: string().optional(),
}).describe('A user record')
toJsonSchema(userSchema)
// {
// type: 'object',
// properties: {
// name: { type: 'string', minLength: 1, description: 'The user full name' },
// age: { type: 'number', minimum: 0, maximum: 120 },
// role: { enum: ['admin', 'user'], description: 'Access level' },
// nickname: { type: 'string' },
// },
// required: ['name', 'age', 'role'],
// description: 'A user record',
// }
describe is metadata only; it does not affect validation. Optional properties
are omitted from the required list. Unions (or, maybe) export as anyOf,
arrays as items, tuples as positional items, sets as unique-item arrays, and
records as objects with typed additionalProperties.
License
Apache-2.0