Skip to content

Overview & Quick Start

Canonical Payload and Zod fields

@nexload-sdk/payload-schema

v2.0.0 Stable Released

Canonical Payload field definitions with reusable Zod schemas.

Payload CMSNode.jsESM

@nexload-sdk/payload-schema eliminates double-declaration of data validation rules in Payload CMS applications. Define intrinsic validation and normalization once, compile it into ordinary Payload collection fields, and expose the identical Zod schemas to client forms, server actions, and API boundaries.


import { defineEntity, field } from "@nexload-sdk/payload-schema";
import { z } from "zod";
// 1. Define intrinsic fields once
export const productEntity = defineEntity({
name: "Product",
fields: {
title: field.text({ required: true, trim: true, minLength: 3 }),
slug: field.slug({ required: true }),
price: field.money({ currency: "USD", required: true }),
stock: field.number({ integer: true, safe: true, defaultValue: 0 }),
},
});
// 2. Export directly to Payload Collection
export const Products = {
slug: "products",
fields: productEntity.payload.all(),
};
// 3. Derive Zod schemas for forms, Next.js server actions, or RPC
export const createProductSchema = productEntity.schema(({ pick }) =>
pick(["title", "slug", "price", "stock"], { optional: ["stock"] })
);
export type CreateProductInput = z.infer<typeof createProductSchema>;

  • Single Source of Truth: Eliminate discrepancies between frontend Zod validation schemas and backend Payload collection rules.
  • 14 Built-In Field Factories: Native factories for text, textarea, slug, number, money, boolean, date, select, relationship, upload, group, array, richText, and native.
  • Automatic Payload Normalization: Injected beforeValidate hooks guarantee that string trimming, case conversions, and slug formats execute even on writes through the Payload Admin UI and Local API.
  • Flexible Schema Derivation: Extract custom DTOs (pick, omit), add custom transformations (.transform()), and perform cross-field validations (.refine()).
  • Zero Framework Leakage: Outputted schemas are ordinary Zod objects and outputted fields are standard Payload field definitions.
  • Compile-Time Immutability: All entity facades and compiled field definitions are frozen to guarantee deterministic runtime behavior.

Terminal window
pnpm add @nexload-sdk/payload-schema payload zod

With alternative package managers:

Terminal window
# npm
npm install @nexload-sdk/payload-schema payload zod
# bun
bun add @nexload-sdk/payload-schema payload zod
Package / RuntimeSupported RangeNotes
Node.js>=20.9.0Server/config safe
Payload CMS>=3.85.0 <4.0.0Collection fields engine
Zod>=4.0.0 <5.0.0Schema derivation engine
Module FormatESM onlyServer and configuration safe

A field definition owns only the rules intrinsic to that field’s value (e.g. “a product title must be 3-120 characters and trimmed”). Application-specific workflow rules (e.g. “is this field optional during draft creation?”) belong in derived schemas, not in the core entity definition.

When you run defineEntity():

  • entity.payload.all() compiles standard Payload fields with canonical normalization appended to beforeValidate.
  • entity.schema() produces pure Zod schemas reflecting the exact same constraints.

Canonical schemas must execute synchronously. Asynchronous refinements are rejected at definition time with ASYNC_CANONICAL_SCHEMA_UNSUPPORTED to ensure compatibility with Payload’s synchronous field validation lifecycle.

The package does not create collections, own access control, replace Payload-generated types, or manage database migrations. Payload remains authoritative for persistence types and database schema.


import { defineEntity, field } from "@nexload-sdk/payload-schema";
export const articleEntity = defineEntity({
name: "Article",
fields: {
title: field.text({ required: true, trim: true, minLength: 5 }),
slug: field.slug({ required: true }),
content: field.textarea({ required: true, maxLength: 10_000 }),
status: field.select({
values: ["draft", "published", "archived"] as const,
defaultValue: "draft",
}),
author: field.relationship({
relationTo: "users",
required: true,
}),
},
});
// Use in Payload Collection
export const ArticlesCollection = {
slug: "articles",
fields: articleEntity.payload.all(),
};
// Derive input validation schema
export const createArticleSchema = articleEntity.schema(({ pick }) =>
pick(["title", "slug", "content", "author"], { strict: true })
);