JSPM

@jamx-framework/validator

1.0.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 9
  • Score
    100M100P100Q74910F
  • License MIT

JAMX Framework — Schema validation with type inference

Package Exports

  • @jamx-framework/validator

Readme

@jamx-framework/validator

Descripción

Sistema de validación de datos con inferencia de tipos para JAMX Framework. Proporciona una API fluida y type-safe para definir schemas de validación (strings, numbers, booleans, arrays, objects, unions, enums, literals) y validar datos en runtime con errores detallados. Los schemas permiten inferir automáticamente los tipos TypeScript, garantizando consistencia entre validación y tipado.

Cómo funciona

El módulo implementa un sistema de schemas composables:

  1. Schema base: Clase abstracta con método parse() que retorna ValidationResult
  2. Schemas específicos: StringSchema, NumberSchema, BooleanSchema, ArraySchema, ObjectSchema, etc.
  3. API fluida: v.string(), v.number(), v.object() para construir schemas
  4. Validación: validate(schema, value) retorna resultado con ok, data o errors
  5. Type inference: Infer<typeof schema> extrae el tipo TypeScript del schema

Componentes principales

API Principal (src/index.ts, src/v.ts)

  • v: Objeto con métodos para crear schemas (v.string(), v.number(), etc.)
  • validate<T>(schema, value): Valida un valor contra un schema
  • parseOrThrow<T>(schema, value): Valida y lanza error si falla

Schemas (src/schemas/)

  • StringSchema: Validación de strings (min, max, pattern, email, url)
  • NumberSchema: Validación de números (min, max, int, positive)
  • BooleanSchema: Validación de booleanos
  • EnumSchema: Validación de valores de un conjunto
  • LiteralSchema: Validación de valores literales
  • ArraySchema: Validación de arrays (items, min, max)
  • ObjectSchema: Validación de objetos con shape
  • UnionSchema: Validación de tipos unión

Tipos (src/types/)

  • Schema<T>: Interface base de un schema
  • ValidationResult<T>: Resultado de validación ({ ok: true, data } o { ok: false, errors })
  • ValidationError: Error individual con path, message, code

Uso básico

Validación simple

import { v, validate } from '@jamx-framework/validator';

// Schema de string
const nameSchema = v.string().min(2).max(50);

// Validar
const result = validate(nameSchema, 'Alice');
if (result.ok) {
  console.log('Nombre válido:', result.data); // 'Alice'
} else {
  console.log('Errores:', result.errors);
}

Validación de objetos

import { v, validate } from '@jamx-framework/validator';

const userSchema = v.object({
  id: v.number().int().positive(),
  name: v.string().min(1).max(100),
  email: v.string().email(),
  age: v.number().min(0).max(150).optional(),
  role: v.enum(['admin', 'user', 'guest']),
});

const data = {
  id: 1,
  name: 'Alice',
  email: 'alice@example.com',
  role: 'user',
};

const result = validate(userSchema, data);
if (result.ok) {
  // result.data está tipado como:
  // { id: number; name: string; email: string; age?: number; role: 'admin' | 'user' | 'guest' }
  console.log('Usuario válido:', result.data);
}

Type inference

import { v } from '@jamx-framework/validator';

const productSchema = v.object({
  id: v.string().uuid(),
  name: v.string(),
  price: v.number().positive(),
  tags: v.array(v.string()),
});

type Product = Infer<typeof productSchema>;
// type Product = {
//   id: string;  // uuid validado como string
//   name: string;
//   price: number;
//   tags: string[];
// }

Validación en API

import { validate, parseOrThrow } from '@jamx-framework/validator';
import { v } from '@jamx-framework/validator';

const createUserSchema = v.object({
  name: v.string().min(2).max(100),
  email: v.string().email(),
  password: v.string().min(8),
});

server.post('/users', async (req, res) => {
  // Opción 1: validate
  const result = validate(createUserSchema, req.body);
  if (!result.ok) {
    res.json({ errors: result.errors }, 400);
    return;
  }

  const user = result.data; // tipado correcto
  await db.users.create(user);
  res.created(user);

  // Opción 2: parseOrThrow (más conciso)
  // const user = parseOrThrow(createUserSchema, req.body);
});

Schemas complejos

import { v } from '@jamx-framework/validator';

// Union
const statusSchema = v.union(
  v.literal('pending'),
  v.literal('active'),
  v.literal('inactive')
);
// type Status = 'pending' | 'active' | 'inactive'

// Array con validación
const tagsSchema = v.array(v.string().min(1)).max(10);

// Object con propiedades opcionales
const configSchema = v.object({
  debug: v.boolean().default(false),
  port: v.number().int().min(1).max(65535).default(3000),
  host: v.string().optional(),
});

// Nested objects
const addressSchema = v.object({
  street: v.string(),
  city: v.string(),
  zip: v.string().pattern(/^\d{5}$/),
});

const userSchema = v.object({
  name: v.string(),
  address: addressSchema.optional(),
});

Validadores personalizados

import { v } from '@jamx-framework/validator';

// Custom validator para password fuerte
const passwordSchema = v.string()
  .min(8)
  .pattern(/[A-Z]/) // al menos una mayúscula
  .pattern(/[a-z]/) // al menos una minúscula
  .pattern(/\d/)    // al menos un número
  .pattern(/[!@#$%^&*]/) // al menos un especial
  .refine((value) => {
    // validación adicional asíncrona (ej: check contra breached passwords)
    return !isBreachedPassword(value);
  }, 'Password has been breached')
  .refine((value) => {
    // otra validación
    return value !== 'password123';
  }, 'Password too common');

// Custom refine para lógica compleja
const startDateSchema = v.string().datetime();
const endDateSchema = v.string().datetime();

const dateRangeSchema = v.object({
  start: startDateSchema,
  end: endDateSchema,
}).refine(
  (data) => new Date(data.end) > new Date(data.start),
  'end date must be after start date'
);

Default values

import { v } from '@jamx-framework/validator';

const settingsSchema = v.object({
  theme: v.enum(['light', 'dark']).default('light'),
  notifications: v.boolean().default(true),
  language: v.string().default('en'),
});

// Al validar, los campos faltantes se completan con defaults
const result = validate(settingsSchema, {});
// result.data = { theme: 'light', notifications: true, language: 'en' }

Transformaciones

import { v } from '@jamx-framework/validator';

// Transformar string a number
const ageSchema = v.string()
  .pattern(/^\d+$/)
  .transform((value) => parseInt(value, 10));

// Transformar trim
const trimmedString = v.string()
  .transform((value) => value.trim());

// Transformar fecha
const dateSchema = v.string()
  .datetime()
  .transform((value) => new Date(value));

API Reference

v (Factory)

v.string()

v.string(): StringSchema

Crea un schema de string.

v.number()

v.number(): NumberSchema

Crea un schema de number.

v.boolean()

v.boolean(): BooleanSchema

Crea un schema de boolean.

v.enum()

v.enum<T extends string>(values: readonly T[]): EnumSchema<T>

Crea un schema que valida que el valor esté en el conjunto.

v.literal()

v.literal<T extends string | number | boolean>(value: T): LiteralSchema<T>

Crea un schema que valida un valor literal exacto.

v.array()

v.array<T>(itemSchema: Schema<T>): ArraySchema<T>

Crea un schema de array.

v.object()

v.object<T extends ObjectShape>(shape: T): ObjectSchema<T>

Crea un schema de objeto.

v.union()

v.union<T>(...schemas: Schema<T>[]): UnionSchema<T>

Crea un schema que acepta cualquiera de los schemas dados.

StringSchema

Métodos de cadena

.min(length: number): this
.max(length: number): this
.pattern(regex: RegExp): this
.email(): this           // valida formato email
.url(): this             // valida formato URL
.uuid(): this            // valida UUID v4
.nonempty(): this        // no vacío
.optional(): this        // permite undefined

Ejemplo:

v.string().min(3).max(100).email();

NumberSchema

Métodos numéricos

.min(value: number): this
.max(value: number): this
.int(): this             // debe ser entero
.positive(): this        // > 0
.negative(): this        // < 0
.nonnegative(): this     // >= 0
.nonpositive(): this     // <= 0
.optional(): this

Ejemplo:

v.number().int().min(0).max(100);

BooleanSchema

.optional(): this

Ejemplo:

v.boolean();

EnumSchema

// Ya creado con v.enum(values)

Valida que el valor esté en el array de valores.

LiteralSchema

// Ya creado con v.literal(value)

Valida igualdad estricta.

ArraySchema

Métodos

.min(length: number): this
.max(length: number): this
.optional(): this

Ejemplo:

v.array(v.string()).min(1).max(10);

ObjectSchema

Métodos

.refine(
  predicate: (data: T) => boolean,
  message: string
): this

Añade validación personalizada.

.optional() (en campos individuales): Hace una propiedad opcional.

Ejemplo:

v.object({
  name: v.string().min(1),
  age: v.number().optional(),
}).refine(
  (data) => data.age === undefined || data.age >= 18,
  'Must be at least 18 years old'
);

UnionSchema

// Ya creado con v.union(...schemas)

Acepta cualquiera de los schemas.

Schema (interface base)

interface Schema<T> {
  parse(value: unknown): ValidationResult<T>;
  optional(): Schema<T | undefined>;
}

ValidationResult

interface ValidationResult<T> {
  ok: boolean;
  data?: T;        // presente si ok === true
  errors?: ValidationError[]; // presente si ok === false
}

ValidationError

interface ValidationError {
  path: string[];   // ruta al campo, ej: ['address', 'city']
  message: string;  // mensaje de error
  code: string;     // código de error, ej: 'invalid_type', 'too_small'
}

Ejemplos completos

Formulario de login

import { v, validate } from '@jamx-framework/validator';

const loginSchema = v.object({
  email: v.string().email(),
  password: v.string().min(8).max(100),
  remember: v.boolean().default(false),
});

type LoginData = Infer<typeof loginSchema>;

async function handleLogin(req: JamxRequest, res: JamxResponse) {
  const result = validate(loginSchema, req.body);

  if (!result.ok) {
    // errors = [{ path: ['email'], message: 'Invalid email', code: 'invalid_email' }]
    return res.json({ errors: result.errors }, 400);
  }

  const { email, password, remember } = result.data;
  const user = await authenticate(email, password);

  if (!user) {
    return res.json({ error: 'Invalid credentials' }, 401);
  }

  const token = generateToken(user, { remember });
  res.json({ token, user });
}

Validación de query params

import { v, validate } from '@jamx-framework/validator';

const paginationSchema = v.object({
  page: v.number().int().positive().default(1),
  limit: v.number().int().min(1).max(100).default(20),
  sort: v.enum(['asc', 'desc']).default('asc'),
});

server.get('/users', async (req, res) => {
  const result = validate(paginationSchema, req.query);

  if (!result.ok) {
    return res.json({ errors: result.errors }, 400);
  }

  const { page, limit, sort } = result.data;
  const users = await db.users.find({}).paginate({ page, limit, sort });
  res.json(users);
});

Validación anidada

import { v, validate } from '@jamx-framework/validator';

const addressSchema = v.object({
  street: v.string().min(1),
  city: v.string().min(1),
  zip: v.string().pattern(/^\d{5}$/),
  country: v.string().default('US'),
});

const orderSchema = v.object({
  customerId: v.string().uuid(),
  shippingAddress: addressSchema,
  billingAddress: addressSchema.optional(),
  items: v.array(
    v.object({
      productId: v.string().uuid(),
      quantity: v.number().int().positive(),
      price: v.number().positive(),
    })
  ).min(1),
  total: v.number().positive(),
}).refine(
  (data) => {
    const itemsTotal = data.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
    return Math.abs(itemsTotal - data.total) < 0.01; // tolerancia para redondeo
  },
  'Total does not match sum of items'
);

type Order = Infer<typeof orderSchema>;

Validación condicional

import { v } from '@jamx-framework/validator';

const signupSchema = v.object({
  email: v.string().email(),
  password: v.string().min(8),
  confirmPassword: v.string(),
  marketingOptIn: v.boolean().default(false),
}).refine(
  (data) => data.password === data.confirmPassword,
  'Passwords do not match'
).omit(['confirmPassword']); // eliminar campo sensible antes de usar

// O usando union para diferentes tipos de usuario
const userSchema = v.union(
  v.object({
    type: v.literal('admin'),
    adminKey: v.string(),
  }),
  v.object({
    type: v.literal('user'),
    username: v.string(),
  })
);

Transformaciones y defaults

import { v } from '@jamx-framework/validator';

const searchSchema = v.object({
  query: v.string().trim().min(1),
  page: v.string().transform(Number).default(1),
  limit: v.string().transform(Number).min(1).max(100).default(20),
  sortBy: v.string().default('createdAt'),
  filters: v.object({
    status: v.enum(['active', 'inactive']).optional(),
    category: v.string().optional(),
  }).default({}),
});

// Al validar '?query= test &page=2':
// result.data = {
//   query: 'test',
//   page: 2,
//   limit: 20,
//   sortBy: 'createdAt',
//   filters: {}
// }

Validación de arrays complejos

import { v } from '@jamx-framework/validator';

const matrixSchema = v.array(
  v.array(v.number())
).min(1).max(10).refine(
  (matrix) => matrix.every(row => row.length === matrix[0].length),
  'All rows must have same length'
);

// Validar matriz 3x3
const result = validate(matrixSchema, [[1,2,3], [4,5,6], [7,8,9]]);

Schema reutilizable

import { v } from '@jamx-framework/validator';

const emailSchema = v.string().email().trim();
const passwordSchema = v.string().min(8).max(100);

const userRegistrationSchema = v.object({
  email: emailSchema,
  password: passwordSchema,
  confirmPassword: passwordSchema,
}).refine(
  (data) => data.password === data.confirmPassword,
  'Passwords do not match'
).omit(['confirmPassword']);

Flujo interno

Validación de string

class StringSchema extends BaseSchema<string> {
  _parse(value: unknown, path: string[]): ValidationResult<string> {
    // 1. Verificar tipo
    if (typeof value !== "string") {
      return fail([{ path, message: "Expected string", code: "invalid_type" }]);
    }

    // 2. Validar nonempty
    if (this._nonempty && value.length === 0) {
      return fail([{ path, message: "Cannot be empty", code: "too_small" }]);
    }

    // 3. Validar min
    if (this._min !== undefined && value.length < this._min) {
      return fail([{ path, message: `Min ${this._min} chars`, code: "too_small" }]);
    }

    // 4. Validar max
    if (this._max !== undefined && value.length > this._max) {
      return fail([{ path, message: `Max ${this._max} chars`, code: "too_large" }]);
    }

    // 5. Validar pattern
    if (this._pattern && !this._pattern.test(value)) {
      return fail([{ path, message: "Invalid format", code: "invalid_pattern" }]);
    }

    // 6. Validar email
    if (this._email && !this._validateEmail(value)) {
      return fail([{ path, message: "Invalid email", code: "invalid_email" }]);
    }

    // 7. Validar url
    if (this._url && !this._validateUrl(value)) {
      return fail([{ path, message: "Invalid URL", code: "invalid_url" }]);
    }

    // 8. Aplicar transformaciones y retornar éxito
    return ok(value);
  }
}

Validación de objeto

class ObjectSchema<T extends ObjectShape> extends BaseSchema<T> {
  _parse(value: unknown, path: string[]): ValidationResult<T> {
    // 1. Verificar que es objeto
    if (typeof value !== "object" || value === null || Array.isArray(value)) {
      return fail([{ path, message: "Expected object", code: "invalid_type" }]);
    }

    const obj = value as Record<string, unknown>;
    const result: Record<string, unknown> = {};
    const errors: ValidationError[] = [];

    // 2. Validar cada propiedad del shape
    for (const [key, schema] of Object.entries(this._shape)) {
      const fieldPath = [...path, key];
      const fieldResult = schema.parse(obj[key]);

      if (fieldResult.ok) {
        result[key] = fieldResult.data;
      } else {
        errors.push(...fieldResult.errors.map(err => ({
          ...err,
          path: fieldPath,
        })));
      }
    }

    // 3. Aplicar refinements
    for (const refine of this._refinements) {
      if (!refine.predicate(result)) {
        errors.push({
          path,
          message: refine.message,
          code: 'custom',
        });
      }
    }

    // 4. Retornar
    return errors.length > 0
      ? fail(errors)
      : ok(result as T);
  }
}

validate()

export function validate<T>(
  schema: Schema<T>,
  value: unknown,
): ValidationResult<T> {
  return schema.parse(value);
}

parseOrThrow()

export function parseOrThrow<T>(schema: Schema<T>, value: unknown): T {
  const result = schema.parse(value);
  if (!result.ok) {
    const messages = result.errors
      .map(e => `${e.path.join(".") || "root"}: ${e.message}`)
      .join(", ");
    throw new Error(`Validation failed: ${messages}`);
  }
  return result.data;
}

Consideraciones de rendimiento

Compilación vs runtime

  • Los schemas se ejecutan en runtime (no hay compilación)
  • La validación es síncrona y rápida para la mayoría de casos
  • Para validación masiva, considera batch processing

Type inference

  • La inferencia de tipos es en compile-time (TypeScript)
  • No hay overhead en runtime por la inferencia

Caching

  • Los schemas son inmutables una vez creados
  • Puedes reutilizar schemas sin problemas
  • No hay caching interno de resultados

Profiling

// Para medir performance
const start = Date.now();
const result = validate(largeSchema, largeObject);
console.log(`Validation took ${Date.now() - start}ms`);

Testing

Tests unitarios

import { describe, it, expect } from 'vitest';
import { v, validate } from '@jamx-framework/validator';

describe('StringSchema', () => {
  it('should validate min length', () => {
    const schema = v.string().min(3);
    const result = validate(schema, 'ab');
    expect(result.ok).toBe(false);
    expect(result.errors[0].code).toBe('too_small');
  });

  it('should validate email', () => {
    const schema = v.string().email();
    const result = validate(schema, 'invalid-email');
    expect(result.ok).toBe(false);
    expect(result.errors[0].code).toBe('invalid_email');
  });
});

describe('ObjectSchema', () => {
  it('should validate nested objects', () => {
    const schema = v.object({
      user: v.object({
        name: v.string(),
      }),
    });

    const result = validate(schema, { user: { name: 'Alice' } });
    expect(result.ok).toBe(true);
    expect(result.data.user.name).toBe('Alice');
  });
});

Tests con type inference

import { v, Infer } from '@jamx-framework/validator';

const schema = v.object({
  id: v.string().uuid(),
  count: v.number().int().positive(),
});

type Data = Infer<typeof schema>;

// TypeScript sabe que Data es:
// { id: string; count: number }

function process(data: Data) {
  // data.id es string
  // data.count es number
}

Seguridad

Sanitización

El validador NO sanitiza datos, solo valida. Si necesitas sanitización, hazla después:

const schema = v.string().min(1);
const result = validate(schema, req.body.name);
if (result.ok) {
  const sanitized = sanitizeHtml(result.data); // sanitizar manualmente
}

Inyección

La validación previene algunos ataques (ej: tipo de dato incorrecto), pero no sustituye otras medidas de seguridad (prepared statements, escaping, etc.).

DoS

Validar inputs muy grandes puede ser costoso. Considerar límites:

const largeString = v.string().max(10_000); // límite de 10KB

Limitaciones

Sin async validation

Los schemas son síncronos. Para validación asíncrona (ej: check en DB), usa refine con función async pero debes manejar manualmente:

// No soportado nativamente
const schema = v.string().refine(async (value) => {
  const exists = await db.userExists(value);
  return !exists;
});

// Alternativa: validar después
const result = validate(v.string(), email);
if (result.ok && !(await db.userExists(result.data))) {
  // error
}

No soporta todos los tipos

Solo soporta: string, number, boolean, enum, literal, array, object, union. Faltan: Date, RegExp, Function, etc. Usa transformaciones:

const dateSchema = v.string().datetime().transform((v) => new Date(v));

Mensajes de error fijos

Los mensajes de error están hardcodeados (ej: "Expected string"). Para mensajes personalizados, usa refine:

const schema = v.string().refine(
  (v) => v === 'admin' || v === 'user',
  'Must be admin or user'
);

Sin validación de referencias circulares

Los objetos con referencias circulares fallarán. Usa v.any() para campos problemáticos.

Buenas prácticas

1. Definir schemas una vez

// ✅ Bien: schema reutilizable
const userSchema = v.object({ ... });
export { userSchema };

// ❌ No: crear schema en cada validación
function handler(req) {
  const schema = v.object({ ... }); // crea nuevo schema cada vez
  validate(schema, req.body);
}

2. Usar type inference

// ✅ Bien: inferir tipo
const productSchema = v.object({ ... });
type Product = Infer<typeof productSchema>;

// ❌ No: duplicar tipos
interface Product {
  id: string;
  name: string;
}
const productSchema: Schema<Product> = v.object({ ... });

3. Validar early

// ✅ Bien: validar al inicio del handler
server.post('/users', async (req, res) => {
  const result = validate(userSchema, req.body);
  if (!result.ok) {
    return res.json({ errors: result.errors }, 400);
  }
  // ... resto del handler con datos validados
});

4. Usar defaults para campos opcionales

const configSchema = v.object({
  debug: v.boolean().default(false),
  port: v.number().default(3000),
});

5. Refine para lógica compleja

const dateRangeSchema = v.object({
  start: dateSchema,
  end: dateSchema,
}).refine(
  (data) => data.end > data.start,
  'end must be after start'
);

6. Omitir campos sensibles

const passwordSchema = v.object({
  password: v.string().min(8),
  confirmPassword: v.string(),
}).refine(
  (data) => data.password === data.confirmPassword,
  'Passwords do not match'
).omit(['password', 'confirmPassword']); // eliminar antes de usar

Integración con otros paquetes

Con @jamx-framework/server

import { validate } from '@jamx-framework/validator';
import { v } from '@jamx-framework/validator';

const userSchema = v.object({
  name: v.string().min(1),
  email: v.string().email(),
});

server.post('/users', async (req, res) => {
  const result = validate(userSchema, req.body);
  if (!result.ok) {
    return res.json({ errors: result.errors }, 400);
  }

  const user = result.data;
  await db.users.create(user);
  res.created(user);
});

Con @jamx-framework/auth

import { validate } from '@jamx-framework/validator';

const loginSchema = v.object({
  email: v.string().email(),
  password: v.string(),
});

server.post('/auth/login', async (req, res) => {
  const credentials = parseOrThrow(loginSchema, req.body);
  const user = await auth.authenticate(credentials);
  const token = await auth.createToken(user);
  res.json({ token, user });
});

Con @jamx-framework/db

import { validate } from '@jamx-framework/validator';

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

server.post('/users', async (req, res) => {
  const data = parseOrThrow(insertSchema, req.body);
  const user = await db.users.insert(data);
  res.created(user);
});

Con @jamx-framework/config

import { validate } from '@jamx-framework/validator';
import { v } from '@jamx-framework/validator';

const configSchema = v.object({
  server: v.object({
    port: v.number().int().min(1).max(65535).default(3000),
    host: v.string().default('localhost'),
  }),
  database: v.object({
    url: v.string().url(),
  }).optional(),
});

// Validar configuración cargada
const config = await configManager.load('./');
const result = validate(configSchema, config);
if (!result.ok) {
  throw new Error(`Invalid config: ${JSON.stringify(result.errors)}`);
}

Preguntas frecuentes

¿Cómo validar arrays de objetos?

const itemsSchema = v.array(
  v.object({
    id: v.string().uuid(),
    quantity: v.number().int().positive(),
  })
).min(1);

validate(itemsSchema, [{ id: '...', quantity: 2 }]);

¿Cómo hacer un campo opcional?

const schema = v.object({
  requiredField: v.string(),
  optionalField: v.string().optional(), // puede ser undefined
});

¿Cómo omitir campos de la salida?

const schema = v.object({
  password: v.string(),
  confirmPassword: v.string(),
}).omit(['password', 'confirmPassword']);

¿Cómo validar fechas?

const dateSchema = v.string()
  .datetime() // formato ISO
  .transform((v) => new Date(v)); // transformar a Date

¿Cómo combinar múltiples validadores?

const schema = v.string()
  .min(3)
  .max(100)
  .pattern(/^[a-zA-Z]+$/)
  .email();

¿Cómo crear schema de tupla?

// Para tuple exacta [string, number, boolean]
const tupleSchema = v.array(v.union(v.string(), v.number(), v.boolean()))
  .length(3); // no hay .length(), usar refine
// o
const tupleSchema = v.object({
  0: v.string(),
  1: v.number(),
  2: v.boolean(),
});

¿Cómo validar null?

// v.null() no existe, usar union
const nullableString = v.union(v.string(), v.null());
// o
const nullableString = v.string().optional(); // undefined o string

¿Cómo validar cualquier tipo?

const anySchema = v.any(); // no existe, usar v.literal(undefined) o omitir validación
// Para "cualquier cosa", no uses schema o usa v.object({}) sin propiedades

Referencia rápida

Crear schemas

v.string()
v.number()
v.boolean()
v.enum(['a', 'b', 'c'])
v.literal('fixed')
v.array(itemSchema)
v.object({ prop: schema })
v.union(schema1, schema2)

Métodos comunes

// String
.min(n) .max(n) .pattern(regex) .email() .url() .uuid() .nonempty() .optional()

// Number
.min(n) .max(n) .int() .positive() .negative() .nonnegative() .optional()

// Array
.min(n) .max(n) .optional()

// Object
.refine(predicate, message) .optional() (por campo)
.omit(['field1', 'field2']) // eliminar campos

// Todos
.transform(fn) // transformar valor

Validar

validate(schema, value) // retorna ValidationResult
parseOrThrow(schema, value) // retorna valor o lanza

Inferir tipo

type MyType = Infer<typeof mySchema>;

Archivos importantes

  • src/index.ts - Punto de entrada
  • src/v.ts - API fluida
  • src/validate.ts - Funciones de validación
  • src/schemas/ - Implementaciones de schemas
  • src/types/ - Tipos de resultado
  • tests/unit/schemas/ - Tests de schemas
  • tests/unit/validators/validate.test.ts - Tests de validate

Dependencias

  • @types/node - Tipos de Node.js
  • vitest - Testing
  • rimraf - Limpieza

Scripts del paquete

  • pnpm build - Compila TypeScript
  • pnpm dev - Watch mode
  • pnpm test - Tests unitarios
  • pnpm test:watch - Tests en watch
  • pnpm type-check - Verificar tipos
  • pnpm clean - Limpiar build

Comparación con Zod

Característica JAMX Validator Zod
API fluida
Type inference
Transformations
Default values
Refine/custom
Async validation No
Branded types No
Lazy schemas No
Tamaño Pequeño Medio

Roadmap futuro

  • Validación asíncrona (refine async)
  • Branded types para tipos nominales
  • Lazy schemas para recursión
  • Más métodos: .length(), .includes(), .startsWith(), .endsWith()
  • Schema para Date, RegExp, Function
  • Validación de discriminated unions mejorada
  • Error messages templates
  • Internationalización de errores
  • Plugin system para custom validators

Conclusión

@jamx-framework/validator es una biblioteca ligera y type-safe para validación de datos en JAMX. Su API fluida y composable permite definir schemas complejos con inferencia de tipos, ideal para validar requests de API, configuraciones, y cualquier dato en runtime.