npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pothos/plugin-validation

v4.3.4

Published

A Pothos plugin for adding schema based validation

Readme

Validation plugin

Validate arguments and input objects before a field resolver runs. Attach a schema from any library that implements Standard Schema, including Zod, Valibot, and ArkType. Validation can be asynchronous, and chained validators can transform the values passed to your resolver. If validation fails, the resolver does not run.

Usage

Install

To use the validation plugin, you'll need to install the validation plugin and a compatible validation library:

npm install --save @pothos/plugin-validation zod
# OR
npm install --save @pothos/plugin-validation valibot
# OR
npm install --save @pothos/plugin-validation arktype

Setup

import SchemaBuilder from '@pothos/core';
import ValidationPlugin from '@pothos/plugin-validation';
import { z } from 'zod'; // or your preferred validation library

const builder = new SchemaBuilder({
  plugins: [ValidationPlugin],
});

builder.queryType({
  fields: (t) => ({
    simple: t.boolean({
      args: {
        // Validate individual arguments
        email: t.arg.string({
          required: true,
          validate: z.string().email(),
        }),
      },
      resolve: () => true,
    }),
  }),
});

Validation API Overview

The validation plugin supports validating inputs and arguments in several different ways:

  • Argument validation: t.arg.string({ validate: schema }) or t.arg.string().validate(schema) - Validate individual arguments
  • Validate all field args: t.field({ args, validate: schema, ... }) or t.field({ args: t.validate(args, schema), ... }) - Validate all arguments together
  • Input type validation: builder.inputType('Input', { validate: schema, ... }) or builder.inputType('Input', { ... }).validate(schema) - Validate entire input objects
  • Input field validation: t.string({ validate: schema }) or t.string().validate(schema) - Validate individual input type fields

Each example below is independent. Reuse the setup imports and builder configuration, then use the example's query and input definitions in place of any earlier ones.

Validation Patterns

Argument Validation

Validate each field argument independently using either the object syntax or chaining API:

builder.queryType({
  fields: (t) => ({
    user: t.string({
      args: {
        email: t.arg.string({
          required: true,
          validate: z.string().email(),
        }),
        name: t.arg.string({ required: true })
          .validate(z.string().min(2).max(50)),
      },
      resolve: (_, args) => `User: ${args.name}`,
    }),
  }),
});

Data Transformation with Argument Validation

When using the chaining API, you can transform data as part of the validation process:

builder.queryType({
  fields: (t) => ({
    processData: t.string({
      args: {
        // Convert comma-separated string to array
        tags: t.arg.string({ required: true })
          .validate(z.string().transform(str => str.split(',').map(s => s.trim()))),
      },
      resolve: (_, args) => {
        return `Processed ${args.tags.length} tags`;
      },
    }),
  }),
});

Validating all Field Arguments Together

Pass validate on the output field to check its arguments together. Optional GraphQL arguments can be omitted or explicitly null; use a schema that accepts both when those are valid inputs.

builder.queryType({
  fields: (t) => ({
    contact: t.boolean({
      args: {
        email: t.arg.string(),
        phone: t.arg.string(),
      },
      // Ensure at least one contact method is provided
      validate: z
        .object({
          email: z.string().nullish(),
          phone: z.string().nullish(),
        })
        .refine(
          (args) => !!args.phone || !!args.email,
          { message: 'Must provide either phone or email' }
        ),
      resolve: () => true,
    }),
  }),
});

With transforms

Use t.validate(args, schema) to transform all arguments and infer the transformed resolver arguments:

builder.queryType({
  fields: (t) => ({
    user: t.string({
      args: t.validate({
        email: t.arg.string(),
        phone: t.arg.string(),
      },
        z.object({
          email: z.string().nullish(),
          phone: z.string().nullish(),
        })
        .refine(
          (args) => !!args.phone || !!args.email,
          { message: 'Must provide either phone or email' }
        )
        .transform((args) => ({
          filter: {
            email: args.email ? args.email.toLowerCase() : undefined,
            phone: args.phone ? args.phone.replace(/\D/g, '') : undefined,
          },
        }))
      ),
      resolve: (_, args) => {
        // args has transformed shape:
        // { filter: { email?: string, phone?: string } }
        return `User filter: ${JSON.stringify(args.filter)}`;
      },
    }),
  }),
});

Input Type Validation

Validate entire input objects with complex validation logic using either object syntax or chaining:

// Object syntax
const UserInput = builder.inputType('UserInput', {
  fields: (t) => ({
    name: t.string({ required: true }),
    age: t.int({ required: true }),
  }),
  validate: z
    .object({
      name: z.string(),
      age: z.number(),
    })
    .refine((user) => user.name !== 'admin', {
      message: 'Username "admin" is not allowed',
    })
});

Input Type Transformation

Transform entire input types:

const UserInput = builder.inputType('RawUserInput', {
  fields: (t) => ({
    fullName: t.string({ required: true }),
    email: t.string({ required: true }),
  }),
}).validate(
  z.object({
    fullName: z.string(),
    email: z.string().email(),
  }).transform(data => ({
    firstName: data.fullName.split(' ')[0],
    lastName: data.fullName.split(' ').slice(1).join(' '),
    email: data.email.toLowerCase(),
  }))
);

builder.queryType({
  fields: (t) => ({
    previewUser: t.string({
      args: {
        userData: t.arg({ type: UserInput, required: true }),
      },
      resolve: (_, args) => {
        // args.userData has transformed shape:
        // { firstName: string, lastName: string, email: string }
        return `User preview: ${args.userData.firstName} ${args.userData.lastName} <${args.userData.email}>`;
      },
    }),
  }),
});

Input Field Validation

Validate individual fields within input types:

const UserInput = builder.inputType('UserInput', {
  fields: (t) => ({
    name: t.string({
      required: true,
      validate: z.string().min(2).refine(
        (name) => name[0].toUpperCase() === name[0],
        { message: 'Name must be capitalized' }
      ),
    })
  }),
});

Input Field Transformation

Transform field values during validation:

const UserInput = builder.inputType('UserInput', {
  fields: (t) => ({
    birthDate: t.string({ required: true })
      .validate(z.iso.date())
      .validate(z.string().transform(str => new Date(str))),
  }),
});

Validation before mutation side effects

Validation finishes before a mutation resolver runs. A rejected input therefore cannot trigger writes placed inside that resolver. The following example trims accepted names and stores them in an in-memory array; invalid names or email addresses produce typed validation issues.

This separate builder uses the errors plugin integration. Install @pothos/plugin-errors alongside the validation plugin and Zod. Setting unsafelyHandleInputErrors: true allows validation details to be returned before field authorization hooks run, so only enable it when those details may be public.

import SchemaBuilder from '@pothos/core';
import ErrorsPlugin from '@pothos/plugin-errors';
import ValidationPlugin, {
  InputValidationError,
  type StandardSchemaV1,
} from '@pothos/plugin-validation';
import { z } from 'zod';
// This public example deliberately exposes validation details before authorization.
const builder = new SchemaBuilder({
  plugins: [ErrorsPlugin, ValidationPlugin],
  errors: { unsafelyHandleInputErrors: true },
});
const names: string[] = [];

const Issue = builder.objectRef<StandardSchemaV1.Issue>('ValidationIssue').implement({
  fields: (t) => ({
    message: t.exposeString('message'),
    path: t.stringList({
      resolve: (issue) =>
        issue.path?.map((part) => String(typeof part === 'object' ? part.key : part)) ?? [],
    }),
  }),
});
builder.objectType(InputValidationError, {
  name: 'InputValidationError',
  fields: (t) => ({ issues: t.field({ type: [Issue], resolve: (error) => error.issues }) }),
});

const Registration = builder.inputType('Registration', {
  fields: (t) => ({
    name: t.string({ required: true }).validate(z.string().trim().min(3, 'Name is too short')),
    email: t.string({ required: true, validate: z.email('Enter a valid email') }),
  }),
});
builder.mutationType({
  fields: (t) => ({
    register: t.string({
      args: { input: t.arg({ type: Registration, required: true }) },
      errors: { types: [InputValidationError] },
      resolve: (_, { input }) => {
        names.push(input.name);
        return input.name;
      },
    }),
  }),
});

Plugin Options

'validationError'

By default, failed validation throws InputValidationError, which contains Standard Schema issues with their messages and paths. Set validationError to return your own error or message.

const builder = new SchemaBuilder({
  plugins: [ValidationPlugin],
  validation: {
    validationError: (validationResult, args, context) => {
      // validationResult contains the standard-schema validation result
      return new Error(`Validation failed: ${validationResult.issues.map(i => i.message).join(', ')}`);
    },
  },
});

Return Values

Your error handler can return:

  • Error object: Return a custom Error instance
  • String: Return a string message (will be wrapped in a PothosValidationError)
  • Throw: Throw an error directly

To return validation failures as typed GraphQL results, see the errors plugin integration. This requires unsafelyHandleInputErrors, because input validation runs before field authorization hooks.

Validation Execution Order

Understanding when and how validations are executed:

  1. Input Field Validation: Individual input fields are validated first
  2. Input Type Validation: Whole input object validation runs after field validation passes
  3. Argument Validation: Individual field arguments are validated
  4. Field-Level Validation: The output field's validate option and t.validate() run last

When there are multiple validations for the same field or type, they are executed in order, so that any transforms are applied before passing to the next schema. Validations for separate fields or arguments are executed in parallel, and their results are merged into a single set of issues.