Hasta hace poco, el ecosistema de validación en TypeScript sufría una profunda fragmentación arquitectónica. Cada biblioteca popular imponía su propia API cerrada:
.safeParse(data) y propiedades internas _input / _output.v.safeParse(schema, data) acompañada de utilidades TypeScript como InferInput / InferOutput.type(data) comprobados mediante instanceof type.errors.Ajv o TypeCompiler.Para los creadores de herramientas y bibliotecas de formularios (React Hook Form, TanStack Form, tRPC, Hono, Astro, Next.js Server Actions), esto generaba un problema exponencial de mantenimiento de adaptadores ($M \times N$ combinaciones). Los equipos debían publicar y mantener docenas de paquetes independientes: @hookform/resolvers/zod, @hookform/resolvers/valibot, @hookform/resolvers/yup, @trpc/server/adapters/arktype. Cualquier cambio menor en las firmas de errores de un proveedor rompía la cadena de integraciones aguas abajo.
En 2024, los creadores de los motores de validación más influyentes — Colin McDonnell (Zod), Fabian Cook (Valibot) y David Blass (ArkType), con el respaldo activo de los equipos centrales de TanStack y tRPC — unieron fuerzas para definir una especificación abierta y neutral: Standard Schema (@standard-schema/spec).
Standard Schema es una interfaz de contrato de validación ligera y agnóstica respecto al proveedor, adoptada de forma masiva en el ecosistema 2025–2026. La especificación define:
~standard.~standard.validate(value).Gracias a Standard Schema, cualquier biblioteca o framework puede aceptar un esquema de cualquier proveedor sin instalar resolvers intermedios ni código puente personalizado (Zero-Adapter Architecture).
En lugar de construir adaptadores independientes para cada biblioteca, el framework se conecta directamente al bus unificado de la especificación ~standard:
+-------------------------------------------------------------------------+
| FRAGMENTACIÓN DEL ECOSISTEMA (Pasado: M x N adaptadores)|
+-------------------------------------------------------------------------+
| React Hook Form ===> [@hookform/resolvers/zod] ===> Zod |
| TanStack Form ===> [@tanstack/valibot-adapter] ===> Valibot |
| tRPC Server ===> [@trpc/arktype-resolver] ===> ArkType |
| * Docenas de adaptadores frágiles, desalineación de versiones y firmas!|
+-------------------------------------------------------------------------+
| |
| ARQUITECTURA STANDARD SCHEMA (Presente: 1 contrato) |
+-------------------------------------------------------------------------+
| React Hook Form ---\ |
| TanStack Form -----> [ StandardSchemaV1: ~standard.validate() ] |
| tRPC / Hono / Actions -/ | |
| +---> Zod v4 (~standard) |
| +---> Valibot (~standard) |
| +---> ArkType (~standard) |
| * ¡Interoperabilidad total entre validadores directamente de fábrica! |
+-------------------------------------------------------------------------+Cada esquema compatible incorpora la propiedad de metadatos ~standard. El método ~standard.validate() recibe datos sin procesar y devuelve un resultado discriminado determinista: { value: Output } en caso de éxito, o { issues: [...] } si se detectan errores de validación.
<FormInput name="email" schema={EmailSchema} />) con validación autónoma sin acoplamiento a una biblioteca npm específica.El consumidor comprueba que el objeto entrante contenga la propiedad '~standard' y coincida con la versión soportada ('version: 1').
Invoca la función de validación unificada del proveedor, discerniendo entre ejecución síncrona y resolución de Promesas.
En caso de fallo, el array issues expone descriptores homogéneos: mensajes legibles y segmentos deterministas de ruta de clave.
En caso de éxito, devuelve la propiedad value, garantizada estrictamente bajo la firma de tipo de salida del esquema.
@standard-schema/spec#A continuación se muestra la tipificación estricta de StandardSchemaV1 sin recurrir a tipos any:
// Segmento de ruta del error: clave de propiedad de objeto o índice de array
export type StandardPathSegment = string | number | symbol;
export interface StandardPath {
readonly key: StandardPathSegment;
}
// Descriptor estandarizado de una incidencia de validación
export interface StandardIssue {
readonly message: string;
readonly path?: ReadonlyArray<StandardPathSegment | StandardPath>;
}
// Resultado de validación satisfactoria
export interface StandardSuccessResult<TOutput> {
readonly value: TOutput;
readonly issues?: undefined;
}
// Resultado de validación fallida
export interface StandardFailureResult {
readonly issues: ReadonlyArray<StandardIssue>;
readonly value?: undefined;
}
export type StandardResult<TOutput> =
| StandardSuccessResult<TOutput>
| StandardFailureResult;
// Interfaz central de la especificación universal
export interface StandardSchemaV1<TInput = unknown, TOutput = TInput> {
readonly '~standard': {
readonly version: 1;
readonly vendor: string;
readonly validate: (
value: unknown
) => StandardResult<TOutput> | Promise<StandardResult<TOutput>>;
readonly types?: {
readonly input: TInput;
readonly output: TOutput;
};
};
}
// Utilidades para la inferencia estática de tipos
export type InferStandardInput<TSchema extends StandardSchemaV1> =
NonNullable<TSchema['~standard']['types']>['input'];
export type InferStandardOutput<TSchema extends StandardSchemaV1> =
NonNullable<TSchema['~standard']['types']>['output'];Desarrollamos una función capaz de validar entradas arbitrarias contra cualquier esquema que cumpla con Standard Schema:
import {
StandardSchemaV1,
StandardResult,
InferStandardOutput,
} from './standard-schema-spec';
// Guardia de tipo para asegurar compatibilidad con la especificación
export function isStandardSchema(value: unknown): value is StandardSchemaV1 {
return (
typeof value === 'object' &&
value !== null &&
'~standard' in value &&
typeof (value as Record<string, unknown>)['~standard'] === 'object' &&
(value as Record<string, unknown>)['~standard'] !== null &&
(value as { '~standard': { version: unknown } })['~standard'].version === 1
);
}
// Validador universal con soporte para esquemas sincrónicos y asincrónicos
export async function validateUniversal<TSchema extends StandardSchemaV1>(
schema: TSchema,
input: unknown
): Promise<InferStandardOutput<TSchema>> {
if (!isStandardSchema(schema)) {
throw new Error('El objeto de esquema proporcionado no cumple con la especificación Standard Schema v1');
}
// Invocación del método unificado del proveedor
let result = schema['~standard'].validate(input);
// Esperar resolución si la validación es asincrónica
if (result instanceof Promise) {
result = await result;
}
if (result.issues && result.issues.length > 0) {
const formattedErrors = result.issues
.map((issue) => {
const path = issue.path
?.map((p) => (typeof p === 'object' ? String(p.key) : String(p)))
.join('.');
return path ? `${path}: ${issue.message}` : issue.message;
})
.join('; ');
throw new Error(`Error de validación de datos: ${formattedErrors}`);
}
return result.value as InferStandardOutput<TSchema>;
}Al compartir el contrato Standard Schema, las tres bibliotecas operan a través del mismo flujo de validación:
import { z } from 'zod';
import * as v from 'valibot';
import { type } from 'arktype';
import { validateUniversal } from './universal-validator';
// 1. Esquema con Zod v4 (soporte nativo de ~standard)
const ZodPayment = z.object({
amountCents: z.number().positive(),
currency: z.enum(['USD', 'EUR', 'GBP']),
});
// 2. Esquema con Valibot (soporte nativo de ~standard)
const ValibotPayment = v.object({
amountCents: v.pipe(v.number(), v.minValue(1)),
currency: v.picklist(['USD', 'EUR', 'GBP']),
});
// 3. Esquema con ArkType 2.0 (soporte nativo de ~standard)
const ArkTypePayment = type({
amountCents: 'number > 0',
currency: '"USD" | "EUR" | "GBP"',
});
// Lógica de negocio universal para procesamiento de pagos
async function processCheckout() {
const rawPayload = { amountCents: 5000, currency: 'EUR' };
// ¡Las tres invocaciones consumen la misma función validateUniversal!
const res1 = await validateUniversal(ZodPayment, rawPayload);
const res2 = await validateUniversal(ValibotPayment, rawPayload);
const res3 = await validateUniversal(ArkTypePayment, rawPayload);
console.log('Validación exitosa por todos los proveedores:', res1.currency, res2.amountCents, res3.amountCents);
}A continuación se muestra una fábrica con seguridad de tipos para generar Server Actions en React 19 / Next.js a partir de esquemas de cualquier proveedor:
import {
StandardSchemaV1,
InferStandardOutput,
InferStandardInput,
} from './standard-schema-spec';
export interface ActionResponse<TData> {
isSuccess: boolean;
data?: TData;
fieldErrors?: Record<string, string[]>;
serverError?: string;
}
export function createUniversalAction<
TSchema extends StandardSchemaV1,
TResult
>(
schema: TSchema,
actionHandler: (data: InferStandardOutput<TSchema>) => Promise<TResult>
) {
return async (
rawInput: InferStandardInput<TSchema>
): Promise<ActionResponse<TResult>> => {
// 1. Verificación del contrato de la especificación
const standard = schema['~standard'];
if (!standard || standard.version !== 1) {
return {
isSuccess: false,
serverError: 'Versión incompatible de la especificación de validación',
};
}
// 2. Ejecutar validación de entrada
let validation = standard.validate(rawInput);
if (validation instanceof Promise) {
validation = await validation;
}
// 3. Procesamiento normalizado de incidencias
if (validation.issues && validation.issues.length > 0) {
const fieldErrors: Record<string, string[]> = {};
for (const issue of validation.issues) {
const pathKey = issue.path
?.map((segment) => (typeof segment === 'object' ? String(segment.key) : String(segment)))
.join('.') || '_root';
if (!fieldErrors[pathKey]) {
fieldErrors[pathKey] = [];
}
fieldErrors[pathKey].push(issue.message);
}
return {
isSuccess: false,
fieldErrors,
};
}
// 4. Ejecución de la lógica de servidor
try {
const result = await actionHandler(validation.value as InferStandardOutput<TSchema>);
return {
isSuccess: true,
data: result,
};
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : 'Fallo interno del servidor';
return {
isSuccess: false,
serverError: msg,
};
}
};
}Es habitual construir una utilidad supuestamente "universal" pero llamar por dentro a schema.parse(data) o v.safeParse(schema, data). Esto acopla de inmediato el código a una biblioteca concreta y provoca un error en tiempo de ejecución (TypeError: schema.parse is not a function) si el usuario pasa un esquema de Valibot o ArkType. En código universal únicamente debe invocarse schema['~standard'].validate().
La firma del método validate() permite retornar un objeto StandardResult sincrónico o una Promise<StandardResult>. Si se omite la comprobación con instanceof Promise o la palabra clave await, la validación de esquemas asíncronos fallará al intentar acceder a la propiedad issues directamente sobre una Promesa aún no resuelta.
// ❌ INCORRECTO: Asume ejecución exclusivamente sincrónica
const result = schema['~standard'].validate(input);
if (result.issues) { /* ¡result.issues será undefined si result es una Promise! */ }
// ✅ CORRECTO: Manejo uniforme de flujos síncronos y asíncronos
let result = schema['~standard'].validate(input);
if (result instanceof Promise) {
result = await result;
}Cuando un esquema incluye transformaciones o coerciones de tipos (por ejemplo, z.coerce.number() o v.trim()), el tipo de entrada sin procesar input difiere del tipo saneado final output. Si se extrae InferStandardInput en lugar de InferStandardOutput para el controlador del servidor, se pasarán datos no saneados ni transformados a la capa de persistencia.
La especificación permite que los segmentos de ruta path sean valores primitivos (string | number) o bien objetos estructurados del tipo { key: string | number }. Intentar una unión directa como issue.path.join('.') generará cadenas erróneas como [object Object].[object Object]. Es indispensable normalizar los segmentos previamente.
Se crea el esquema en Zod, Valibot o ArkType. El motor inyecta automáticamente el contrato de metadatos ~standard.
El esquema se entrega a un gestor de formularios, ruta tRPC o constructor de Server Actions.
El consumidor ejecuta el método unificado sin necesidad de conocer qué validador opera por debajo.
El consumidor resuelve la Promise si es necesario e inspecciona el resultado discriminado issues.
Si no existen incidencias, los datos tipados y limpios (value) se entregan a la lógica de negocio final.