Skip to main content

What is Standard Schema?

Standard Schema v1 is a shared protocol that validation libraries implement so that frameworks can accept any compliant schema without coupling to a specific library. When a framework supports Standard Schema, it reads a ~standard property on your schema object to validate inputs in a uniform way. ts-chas implements this protocol automatically. Every guard chain you build with is.* and every schema you define with defineSchemas exposes the ~standard property — you don’t need to adapt or wrap anything.
The ~standard property is attached automatically to every guard chain produced by is.* and to every schema returned by defineSchemas. You never need to configure it manually.

Integration with tRPC

tRPC accepts any Standard Schema-compliant object as an input validator. You can use a raw is.* guard chain or a defineSchemas schema directly in your procedure definition.
import { initTRPC } from '@trpc/server';
import { is, defineSchemas } from 'ts-chas/guard';

const t = initTRPC.create();

// Option 1: inline guard chain
const userRouter = t.router({
  getUser: t.procedure
    .input(is.object({ id: is.string.uuid('v4') }))
    .query(({ input }) => {
      // input is typed as { id: string }
      return fetchUser(input.id);
    }),
});

// Option 2: named schema from defineSchemas
const schemas = defineSchemas({
  CreateUserInput: {
    name: is.string.min(1),
    email: is.string.email,
    age: is.number.gte(18).error('Must be 18 or older'),
  },
});

const userMutationRouter = t.router({
  createUser: t.procedure
    .input(schemas.CreateUserInput)
    .mutation(({ input }) => {
      // input is typed as { name: string; email: string; age: number }
      return createUser(input);
    }),
});

Integration with react-hook-form

react-hook-form’s useForm accepts a resolver option. Libraries like @hookform/resolvers provide Standard Schema support, meaning you pass your guard or schema directly as the resolver value.
import { useForm } from 'react-hook-form';
import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';
import { is, defineSchemas, type InferSchema } from 'ts-chas/guard';

const schemas = defineSchemas({
  SignUpForm: {
    email: is.string.email.error('Enter a valid email'),
    password: is.string.min(8).error('Password must be at least 8 characters'),
    age: is.number.gte(18).error('You must be 18 or older'),
  },
});

type SignUpForm = InferSchema<typeof schemas.SignUpForm>;

function SignUpPage() {
  const { register, handleSubmit, formState: { errors } } = useForm<SignUpForm>({
    resolver: standardSchemaResolver(schemas.SignUpForm),
  });

  return (
    <form onSubmit={handleSubmit((data) => submitSignUp(data))}>
      <input {...register('email')} />
      {errors.email && <p>{errors.email.message}</p>}

      <input type="password" {...register('password')} />
      {errors.password && <p>{errors.password.message}</p>}

      <button type="submit">Sign up</button>
    </form>
  );
}
The resolver validates the form values against your schema on every submit, and maps GuardErr messages directly to react-hook-form’s errors object.

Integration with Drizzle

Drizzle’s queryClient and insert helpers accept Standard Schema validators for runtime validation of query inputs and results. Pass a guard or schema to Drizzle’s validation slot to enforce your types at the boundary between the database and your application code.
import { is, defineSchemas, type InferSchema } from 'ts-chas/guard';

const schemas = defineSchemas({
  UserRow: {
    id: is.string.uuid('v4'),
    name: is.string.min(1),
    email: is.string.email,
    createdAt: is.date,
  },
});

type UserRow = InferSchema<typeof schemas.UserRow>;

// Pass the schema to Drizzle's validation slot (exact API depends on your Drizzle version)
// Drizzle will call schemas.UserRow['~standard'].validate() on each row
const users = await db.select().from(usersTable).validate(schemas.UserRow);
// users is typed as UserRow[]
Even if a row from the database contains unexpected shapes — for example, after a migration that isn’t yet reflected in your types — the schema catches the mismatch before it silently propagates through your application.

Any Standard Schema-compatible library

Because ts-chas implements the Standard Schema v1 protocol, any library that supports it works with ts-chas guards out of the box. The pattern is always the same: find the library’s validation slot and pass a guard chain or a defineSchemas schema directly.
import { is, defineSchemas } from 'ts-chas/guard';

// A plain guard chain works anywhere a Standard Schema is accepted
const isPositiveNumber = is.number.positive;

// A defineSchemas schema also works
const schemas = defineSchemas({
  Config: {
    timeout: is.number.positive,
    retries: is.number.gte(0).lte(10),
  },
});

// Pass either one to the library's validation slot
someLibrary.setSchema(isPositiveNumber);
someLibrary.setSchema(schemas.Config);
Check the documentation for the library you’re integrating with to find its Standard Schema resolver or validator option.