Skip to content

JSLEEKR/valx

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

valx

TypeScript Node.js License Tests Version

High-performance data validation library for TypeScript with schema inference, composable validators, and AI output validation.

Installation | Quick Start | API Reference | AI Output Validation | JSON Schema


Why This Exists

Every modern TypeScript application needs runtime data validation. API responses, form inputs, configuration files, and now AI/LLM outputs all require structured validation with clear error reporting. Existing solutions either sacrifice performance for features, or features for simplicity.

valx provides a TypeScript-first schema definition API with:

  • Full type inference from schemas (define once, get types for free)
  • Composable, chainable validators with immutable schema instances
  • Detailed error reporting with dot-notation paths
  • First-class AI output validation (extract JSON from markdown, auto-fix common LLM issues)
  • JSON Schema conversion for interoperability
  • Zero external dependencies

Built in the age of AI agents, where tools like opencli (8k+ stars, newcomer_score: 9.7) and Trellis (4k+ stars) need reliable structured data validation for tool calls, function parameters, and agent responses.


Installation

npm install valx

Quick Start

import { v } from 'valx';

// Define a schema
const userSchema = v.object({
  name: v.string().min(1).max(100),
  email: v.string().email(),
  age: v.number().int().min(0).max(150),
  role: v.enum(['admin', 'user', 'guest'] as const),
  preferences: v.object({
    theme: v.enum(['light', 'dark'] as const).default('light'),
    notifications: v.boolean().default(true),
  }).optional(),
});

// Type is automatically inferred
type User = ReturnType<typeof userSchema.parse>;

// Parse with error throwing
const user = userSchema.parse(data);

// Parse without throwing
const result = userSchema.safeParse(data);
if (result.success) {
  console.log(result.data); // typed as User
} else {
  console.log(result.issues); // structured error details
}

// Type guard
if (userSchema.is(unknownData)) {
  // unknownData is now typed as User
}

API Reference

Primitive Schemas

v.string()

v.string()                       // any string
v.string().min(1)                // at least 1 character
v.string().max(100)              // at most 100 characters
v.string().length(10)            // exactly 10 characters
v.string().nonempty()            // shorthand for .min(1)
v.string().email()               // email format
v.string().url()                 // URL format
v.string().uuid()                // UUID format
v.string().isoDate()             // YYYY-MM-DD
v.string().isoDateTime()         // ISO 8601 datetime
v.string().json()                // valid JSON string
v.string().regex(/^[a-z]+$/)     // regex pattern
v.string().startsWith('http')    // prefix check
v.string().endsWith('.json')     // suffix check
v.string().includes('@')         // contains substring
v.string().trim()                // trim whitespace
v.string().toLowerCase()         // transform to lowercase
v.string().toUpperCase()         // transform to uppercase
v.string().coerce()              // coerce numbers/booleans/dates to string

v.number()

v.number()                       // any finite number
v.number().int()                 // integer only
v.number().min(0)                // minimum value
v.number().max(100)              // maximum value
v.number().positive()            // > 0
v.number().negative()            // < 0
v.number().nonnegative()         // >= 0
v.number().multipleOf(5)         // divisible by 5
v.number().allowInfinite()       // allow Infinity/-Infinity
v.number().clamp(0, 100)         // clamp to range (transform)
v.number().round()               // round to integer (transform)
v.number().floor()               // floor (transform)
v.number().ceil()                // ceil (transform)
v.number().coerce()              // coerce strings/booleans to number

v.boolean()

v.boolean()                      // true or false
v.boolean().true()               // must be true
v.boolean().false()              // must be false
v.boolean().coerce()             // coerce "true"/"false"/0/1

v.literal(value)

v.literal('active')              // exact string match
v.literal(42)                    // exact number match
v.literal(true)                  // exact boolean match

v.date()

v.date()                         // Date instance
v.date().min(new Date('2024-01-01'))  // after date
v.date().max(new Date('2024-12-31'))  // before date
v.date().coerce()                // coerce strings/timestamps

Composite Schemas

v.object(shape)

const schema = v.object({
  name: v.string(),
  age: v.number(),
});

schema.strict()                  // reject unknown keys
schema.strip()                   // remove unknown keys
schema.passthrough()             // keep unknown keys (default)
schema.catchall(v.string())      // validate unknown keys

// Schema manipulation
schema.pick('name')              // pick specific keys
schema.omit('age')               // omit specific keys
schema.extend({ role: v.string() })  // add new keys
schema.partial()                 // make all keys optional
schema.merge(otherSchema)        // merge two schemas

v.array(element)

v.array(v.string())              // array of strings
v.array(v.number()).min(1)       // at least 1 element
v.array(v.number()).max(10)      // at most 10 elements
v.array(v.number()).length(5)    // exactly 5 elements
v.array(v.number()).nonempty()   // shorthand for .min(1)
v.array(v.number()).unique()     // no duplicate values
v.array(v.object({ id: v.number() })).unique('id')  // unique by key
v.array(v.number()).sort((a, b) => a - b)  // sort (transform)

v.tuple(...items)

v.tuple(v.string(), v.number())  // [string, number]
v.tuple(v.string()).rest(v.number())  // [string, ...number[]]

v.record(valueSchema)

v.record(v.number())             // Record<string, number>
v.record(v.number()).min(1)      // at least 1 entry
v.record(v.number()).max(10)     // at most 10 entries
v.recordWithKeys(
  v.string().regex(/^[a-z]+$/),  // validate keys too
  v.number()
)

v.enum(values)

v.enum(['a', 'b', 'c'] as const)      // string enum
v.enum([1, 2, 3] as const)            // numeric enum
v.nativeEnum(TypeScriptEnum)           // TypeScript enum
schema.exclude('a')                    // remove a value
schema.extract('a', 'b')              // keep specific values

Union & Intersection

// Union (OR)
v.union(v.string(), v.number())

// Discriminated union (fast matching)
v.discriminatedUnion('type', [
  v.object({ type: v.literal('dog'), breed: v.string() }),
  v.object({ type: v.literal('cat'), indoor: v.boolean() }),
])

// Intersection (AND)
v.intersection(
  v.object({ name: v.string() }),
  v.object({ age: v.number() })
)

Special Schemas

v.any()                          // accepts anything
v.unknown()                      // accepts anything (prefer over any)
v.never()                        // rejects everything
v.void()                         // accepts undefined
v.null()                         // accepts null
v.undefined()                    // accepts undefined
v.instanceof(Date)               // instanceof check
v.lazy(() => schema)             // recursive schemas

Schema Modifiers

// Available on all schemas:
schema.optional()                // T | undefined
schema.nullable()                // T | null
schema.default(value)            // provide default for undefined
schema.describe('description')   // add metadata
schema.refine(fn, message)       // custom validation
schema.transform(fn)             // transform output

Custom Refinements

const password = v.string()
  .min(8)
  .refine(s => /[A-Z]/.test(s), 'Must contain uppercase')
  .refine(s => /[0-9]/.test(s), 'Must contain a digit');

// Refinements can return string for dynamic messages
const age = v.number().refine(n => {
  if (n < 0) return 'Cannot be negative';
  if (n > 150) return 'Unrealistic age';
  return true;
});

Transforms

// Transform output type
const schema = v.string()
  .transform(s => parseInt(s, 10))
  .transform(n => n * 2);
// Input: string, Output: number

// Combine with defaults
const greeting = v.string()
  .default('world')
  .transform(s => `Hello, ${s}!`);
// greeting.parse(undefined) => "Hello, world!"

AI Output Validation

valx includes first-class support for validating AI/LLM outputs, handling common issues like markdown code blocks, trailing commas, and comments.

import { v, validateAiOutput } from 'valx';

const responseSchema = v.object({
  thought: v.string(),
  action: v.object({
    tool: v.string(),
    input: v.record(v.unknown()),
  }),
  confidence: v.number().min(0).max(1),
});

// AI often wraps JSON in markdown code blocks
const aiOutput = `I'll use the search tool.
\`\`\`json
{
  "thought": "Need to search for information",
  "action": {
    "tool": "search",
    "input": {"query": "TypeScript validation"}
  },
  "confidence": 0.95
}
\`\`\``;

const result = validateAiOutput(aiOutput, responseSchema);

if (result.success) {
  console.log(result.data);         // typed and validated
  console.log(result.wasExtracted); // true (extracted from markdown)
  console.log(result.wasFixed);     // false (no fixes needed)
} else {
  console.log(result.retryPrompt);  // suggested prompt for retry
}

AI Validation Features

  • JSON Extraction: Automatically extracts JSON from markdown code blocks
  • Auto-fix: Handles trailing commas, single-line comments, undefined/NaN/Infinity values
  • Retry Prompts: Generates structured retry prompts when validation fails
  • Configurable: Toggle extraction, auto-fix, and suggestion limits
validateAiOutput(output, schema, {
  extractJson: true,    // extract from markdown (default: true)
  autoFix: true,        // fix common issues (default: true)
  maxSuggestions: 3,    // max error details in retry prompt (default: 3)
});

JSON Schema Conversion

Convert valx schemas to JSON Schema for use with OpenAPI, JSON Schema validators, or AI function calling definitions.

import { v, toJsonSchema } from 'valx';

const schema = v.object({
  id: v.number().int().describe('User ID'),
  name: v.string().min(1).max(255).describe('User name'),
  status: v.enum(['active', 'inactive'] as const),
  tags: v.array(v.string()),
  metadata: v.record(v.unknown()).optional(),
});

const jsonSchema = toJsonSchema(schema);
// {
//   type: "object",
//   properties: {
//     id: { type: "integer", description: "User ID" },
//     name: { type: "string", minLength: 1, maxLength: 255, description: "User name" },
//     status: { enum: ["active", "inactive"] },
//     tags: { type: "array", items: { type: "string" } },
//     metadata: {}
//   },
//   required: ["id", "name", "status", "tags"]
// }

Error Handling

valx provides structured, path-aware error reporting.

const schema = v.object({
  users: v.array(
    v.object({
      name: v.string(),
      age: v.number().int().min(0),
    })
  ),
});

const result = schema.safeParse({
  users: [
    { name: 'Alice', age: 30 },
    { name: 'Bob', age: -5 },
  ],
});

if (!result.success) {
  for (const issue of result.issues) {
    console.log(issue.path);     // ['users', '1', 'age']
    console.log(issue.message);  // 'Number must be >= 0'
    console.log(issue.code);     // 'too_small'
    console.log(issue.expected); // '>= 0'
    console.log(issue.received); // -5
    console.log(issue.severity); // 'error'
  }
}

ValidationError

When using .parse(), a ValidationError is thrown with structured issues:

import { ValidationError } from 'valx';

try {
  schema.parse(invalidData);
} catch (e) {
  if (e instanceof ValidationError) {
    console.log(e.issues); // ValidationIssue[]
    console.log(e.message); // formatted error string
  }
}

Real-World Examples

API Response Validation

const apiResponse = v.object({
  status: v.enum(['success', 'error'] as const),
  data: v.object({
    users: v.array(v.object({
      id: v.number().int().positive(),
      name: v.string().min(1),
      email: v.string().email(),
      role: v.enum(['admin', 'user'] as const),
    })),
    pagination: v.object({
      total: v.number().int().nonnegative(),
      page: v.number().int().positive(),
      perPage: v.number().int().positive(),
    }),
  }),
  meta: v.object({
    requestId: v.string().uuid(),
    timestamp: v.string().isoDateTime(),
  }),
});

Configuration File Validation

const configSchema = v.object({
  server: v.object({
    host: v.string().default('localhost'),
    port: v.number().int().min(1).max(65535).default(3000),
  }),
  database: v.object({
    url: v.string().nonempty(),
    pool: v.object({
      min: v.number().int().nonnegative().default(2),
      max: v.number().int().positive().default(10),
    }).optional(),
  }),
  logging: v.object({
    level: v.enum(['debug', 'info', 'warn', 'error'] as const).default('info'),
  }).optional(),
});

AI Function Calling

const functionCall = v.discriminatedUnion('type', [
  v.object({
    type: v.literal('function_call'),
    name: v.string().nonempty(),
    arguments: v.string().json(),
  }),
  v.object({
    type: v.literal('text'),
    content: v.string(),
  }),
  v.object({
    type: v.literal('tool_use'),
    tool: v.string(),
    input: v.record(v.unknown()),
  }),
]);

Recursive Schemas

interface TreeNode {
  value: string;
  children?: TreeNode[];
}

const treeSchema: ReturnType<typeof v.object> = v.object({
  value: v.string(),
  children: v.array(v.lazy(() => treeSchema)).optional(),
});

Performance

valx is designed for performance. Benchmarks on 10,000 objects with 3 fields each complete in under 100ms. Deeply nested schemas (10 levels) validate in under 10ms.


Architecture

valx/
  src/
    core/types.ts         # Base Schema class, ValidationError, core types
    schemas/
      string.ts           # StringSchema with format validators
      number.ts           # NumberSchema with numeric constraints
      boolean.ts          # BooleanSchema with coercion
      literal.ts          # LiteralSchema for exact values
      array.ts            # ArraySchema with element validation
      object.ts           # ObjectSchema with pick/omit/extend/partial
      tuple.ts            # TupleSchema with rest elements
      record.ts           # RecordSchema for key-value maps
      enum.ts             # EnumSchema and NativeEnumSchema
      date.ts             # DateSchema with range validation
      union.ts            # UnionSchema, DiscriminatedUnionSchema, IntersectionSchema
      special.ts          # AnySchema, NeverSchema, LazySchema, InstanceOfSchema
    utils/
      ai-output.ts        # AI/LLM output validation with auto-fix
      json-schema.ts      # JSON Schema conversion
    index.ts              # Main entry point with `v` builder
  tests/                  # 242 tests across 17 test files

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Run tests: npm test
  4. Run type check: npm run lint
  5. Submit a pull request

License

MIT

About

High-performance data validation library for TypeScript with schema inference, composable validators, and AI output validation

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors