Amazon Vinyl logo Amazon Vinyl v3.2.1

@amazon/vinyl-validation

Website npm size

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