Skip to content

Payload validation ​


Size (checker): 0.8 KB

check validates objects and arrays against a validation schema. Fields absent from the schema are deleted in place by mutation, with optional logging. Mismatches are returned as a list of errors.

The compact library and validation schemas add little to the final bundle size. Native TS types also avoid slowing down tsc and the IDE, unlike libraries that require manually defined validation schemas and rely on complex type inference.

ts
import { check } from '@arfa-tools/generator/api-contract-check/checker';

const schema = { request: { id: 'string' }, response: 'any' } as const;
const value = { id: 'user-1', debug: true };

const errors = check({
  schema,
  value,
  type: 'request',
  getExtraneous: (paths) => console.log(paths), // ['request.debug']
});

console.log(errors); // null
console.log(value); // { id: 'user-1' }

Example errors:

ts
[
  'request.id is not a string',
  'response.items[].name is missing',
  'response.role is none of "manager", "admin"',
];

Arguments ​

ts
schema: TypeRootSchema

The complete { request, response } contract validation schema returned by createValidators

ts
type: 'request' | 'response'

The part of the validation schema used to check the value

ts
value: unknown

The data to validate. Unknown object fields are deleted in place (by mutation), even when validation errors occur

ts
getExtraneous?: (
  paths: Array<string>
) => void

Receives unique paths of deleted fields. Called once, only if fields were deleted, including when validation errors occur. Array indices in paths are replaced with []

Using with fetch ​

The wrapper for JSON POST requests uses a generated validation schema: it checks the request before sending it and the response before returning it. This ensures that the TS request and response types match the actual data exactly.

ts
import { check, type TypeRootSchema } from '@arfa-tools/generator/api-contract-check/checker';

async function request<TRequest, TResponse>({ url, validator, request }: {
  url: string;
  request: TRequest;
  validator: TypeRootSchema;
}): Promise<TResponse> {
  const requestErrors = check({ 
    schema: validator, 
    type: 'request', 
    value: request,
    getExtraneous: console.warn 
  });
  
  if (requestErrors) {
    throw new Error(`Request validation failed:\n${requestErrors.join('\n')}`);
  }

  const response = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(request),
  });
  
  if (!response.ok) throw new Error(`HTTP ${response.status}: ${url}`);

  const value: unknown = await response.json();
  
  const responseErrors = check({ 
    schema: validator, 
    type: 'response', 
    value, 
    getExtraneous: console.warn 
  });
  
  if (responseErrors) {
    throw new Error(`Response validation failed:\n${responseErrors.join('\n')}`);
  }

  return value as TResponse;
}

import type { TypeRequest, TypeResponse } from './api/getUser';
import { getUser } from './validators/api/getUser';

const user = await request<TypeRequest, TypeResponse>({
  url: '/api/getUser',
  request: { id: 'user-1' },
  validator: getUser,
});

Using with WebSocket ​

The wrapper for WebSocket requests uses a generated validation schema: it checks the request before sending it and the response before returning it. This ensures that the TS request and response types match the actual data exactly.

In this example, the server accepts JSON messages in the { id, method, payload } format and responds with { id, payload }.

ts
import { check, type TypeRootSchema } from '@arfa-tools/generator/api-contract-check/checker';

const socket = new WebSocket('wss://example.com/api');
const pending = new Map<string, (value: unknown) => void>();

socket.addEventListener('message', (event: MessageEvent<string>) => {
  const { id, payload } = JSON.parse(event.data);
  
  pending.get(id)?.(payload);
  
  pending.delete(id);
});

async function request<TRequest, TResponse>({ method, validator, payload }: {
  method: string;
  payload: TRequest;
  validator: TypeRootSchema;
}): Promise<TResponse> {
  const requestErrors = check({
    schema: validator,
    type: 'request',
    value: payload,
    getExtraneous: console.warn,
  });

  if (requestErrors) {
    throw new Error(`Request validation failed:\n${requestErrors.join('\n')}`);
  }

  const response = await new Promise<unknown>((resolve) => {
    const id = crypto.randomUUID();
    
    pending.set(id, resolve);
    
    socket.send(JSON.stringify({ id, method, payload }));
  });

  const responseErrors = check({
    schema: validator,
    type: 'response',
    value: response,
    getExtraneous: console.warn,
  });

  if (responseErrors) {
    throw new Error(`Response validation failed:\n${responseErrors.join('\n')}`);
  }

  return response as TResponse;
}

import type { TypeRequest, TypeResponse } from './api/getUser';
import { getUser } from './validators/api/getUser';

const user = await request<TypeRequest, TypeResponse>({
  method: 'getUser',
  payload: { id: 'user-1' },
  validator: getUser,
});

MIT License