payloadcms / payloadcms/payload

`typescriptSchema` cannot promote a field into the `required` array; `admin.condition` workaround for GraphQL nullability silently breaks TypeScript type generation

Open
#16,797 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

area: graphql stale status: needs-triage v3
Dominant language
TypeScript
Stars
44.8k
Forks
4.2k
Avg merge
2d 21h
Merged PRs (30d)
53

Description

Describe the Bug

Two interacting bugs prevent typescriptSchema from making a field required in the generated TypeScript interface:

  1. typescriptSchema is an opt-out-only mechanism — the required property returned by the callback is never used to promote a field into the requiredFieldNames set. The condition that adds a field name to requiredFieldNames is gated on the pre-computed isRequired boolean (computed before typescriptSchema runs), so returning { required: true } from the callback has no effect if the field was not already required according to Payload's own logic.

  2. admin.condition unconditionally suppresses isRequiredfieldIsRequired() short-circuits to false for any field with admin.condition defined, even condition: () => true. This is the workaround commonly recommended for GraphQL non-nullability (see #15811), which means the two documented workarounds actively cancel each other out.


Relevant source (packages/payload/src/utilities/configToJSONSchema.ts)
// 1. isRequired is computed BEFORE typescriptSchema runs
const fieldIsRequired = (field) => {
  const isConditional = Boolean(field?.admin?.condition) // ← kills required for any conditional field
  if (isConditional) return false
  // …
}

// 2. typescriptSchema runs and CAN replace the whole fieldSchema object
if ('typescriptSchema' in field && field?.typescriptSchema?.length) {
  for (const schema of field.typescriptSchema) {
    fieldSchema = schema({ jsonSchema: fieldSchema })
  }
}

// 3. But the required promotion gate is still the pre-computed isRequired
if (isRequired && fieldSchema.required !== false) { // ← isRequired is false → short-circuits
  requiredFieldNames.add(field.name)
}

The fieldSchema.required !== false sub-condition is only an opt-out escape hatch (it lets a required field suppress itself from the array). It cannot promote an optional field to required because the outer isRequired && short-circuits first.


Expected behavior

A field configured with typescriptSchema: [() => ({ type: 'number' })] combined with required: true on the field config should be generated as myNumber: number (non-optional).

Alternatively (the current documented workaround pattern), returning a schema with a truthy required property from typescriptSchema should add the field to the required array.


Actual behavior

The field is generated as myNumber?: number (optional) regardless of both required: true on the field and what typescriptSchema returns — as long as admin.condition is present.

Link to the code that reproduces this issue

https://github.com/Murz-forks/payload-issues/tree/issue-graphql-required

Reproduction Steps

Add the following fields to test/_community/collections/Posts/index.ts:

import type { CollectionConfig } from 'payload'
import { lexicalEditor } from '@payloadcms/richtext-lexical'

export const postsSlug = 'posts'

export const PostsCollection: CollectionConfig = {
  slug: postsSlug,
  admin: {
    useAsTitle: 'title',
  },
  fields: [
    {
      name: 'title',
      type: 'text',
    },
    {
      name: 'content',
      type: 'richText',
      editor: lexicalEditor({
        features: ({ defaultFeatures }) => [...defaultFeatures],
      }),
    },

    // ── Reproduction case 1 ──────────────────────────────────────────────────
    // A required number field with admin.condition (GraphQL nullability
    // workaround) and a typescriptSchema override.
    // Expected generated type: `myNumberWithCondition: number`   (non-optional)
    // Actual generated type:   `myNumberWithCondition?: number`  (optional)
    {
      name: 'myNumberWithCondition',
      type: 'number',
      required: true,
      defaultValue: 10,
      typescriptSchema: [() => ({ type: 'number' })],
      admin: {
        // Workaround from https://github.com/payloadcms/payload/discussions/15811
        // to make the field non-null in GraphQL — but it silently breaks
        // TypeScript type generation because fieldIsRequired() returns false
        // for any field with admin.condition defined.
        condition: () => true,
      },
    },

    // ── Reproduction case 2 ──────────────────────────────────────────────────
    // Same field WITHOUT admin.condition. field.required: true is respected
    // and the field IS required in the generated types — showing that
    // typescriptSchema cannot independently opt a field in; only field.required
    // can. This case is correct and included for comparison only.
    {
      name: 'myNumber',
      type: 'number',
      required: true,
      defaultValue: 10,
      typescriptSchema: [() => ({ type: 'number' })],
      // No admin.condition → myNumber: number  ✓
    },
  ],
}

Then run:

pnpm payload generate:types

Inspect test/_community/payload-types.ts — the Post interface will contain:

myNumberWithCondition?: number; // ← BUG: should be `myNumberWithCondition: number`
myNumber: number;               // ← correct (no admin.condition)

Root cause

fieldIsRequired() (line ~8 of configToJSONSchema.ts) unconditionally returns false for any field where admin.condition is defined. The comment suggests this is intentional — conditional fields may not appear in the document. However condition: () => true is a workaround to trigger GraphQL non-nullability inference; the field is always present. More critically, there is no mechanism for typescriptSchema to override the requiredFieldNames set regardless, because the check is:

if (isRequired && fieldSchema.required !== false)
//  ^^^^^^^^^^
//  set before typescriptSchema runs; the typescriptSchema output is never
//  consulted for the required promotion decision

Proposed fix

Change the condition in fieldsToJSONSchema from:

if (isRequired && fieldSchema.required !== false) {
  requiredFieldNames.add(field.name)
}

to:

if ((isRequired || fieldSchema.required === true) && fieldSchema.required !== false) {
  requiredFieldNames.add(field.name)
}

This allows typescriptSchema to opt a field in to required by returning { required: true, … }, while preserving the existing opt-out escape hatch (required: false still suppresses).


Workaround (until fixed)

Remove admin.condition and accept that the GraphQL type will be nullable. Without admin.condition, fieldIsRequired() returns true and required: true on the field is respected in the generated types:

{
  name: 'myNumber',
  type: 'number',
  required: true,
  defaultValue: 10,
  // No admin.condition — generated as `myNumber: number` ✓
  // GraphQL type will be nullable, but TypeScript will be correct.
}
Which area(s) are affected?

area: graphql

Environment Info
Binaries:
  Node: 24.15.0
  npm: 11.12.1
  Yarn: N/A
  pnpm: 11.1.3
Relevant Packages:
  payload: 3.83.0
  next: 16.2.6
  @payloadcms/db-mongodb: 3.83.0
  @payloadcms/graphql: 3.83.0
  @payloadcms/live-preview: 3.83.0
  @payloadcms/live-preview-react: 3.83.0
  @payloadcms/next/utilities: 3.83.0
  @payloadcms/plugin-import-export: 3.83.0
  @payloadcms/plugin-nested-docs: 3.83.0
  @payloadcms/plugin-redirects: 3.83.0
  @payloadcms/plugin-search: 3.83.0
  @payloadcms/plugin-seo: 3.83.0
  @payloadcms/richtext-lexical: 3.83.0
  @payloadcms/sdk: 3.83.0
  @payloadcms/translations: 3.83.0
  @payloadcms/ui/shared: 3.83.0
  react: 19.2.1
  react-dom: 19.2.1
Operating System:
  Platform: linux
  Arch: x64
  Version: #15-Ubuntu SMP PREEMPT_DYNAMIC Wed Apr 22 16:06:43 UTC 2026
  Available memory (MB): 31515
  Available CPU cores: 16

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in packages/payload/src/utilities/configToJSONSchema.ts, focusing on fieldIsRequired, the typescriptSchema callback, and requiredFieldNames. Reproduce the issue by updating test/_community/collections/Posts/index.ts and running pnpm payload generate:types, then inspect test/_community/payload-types.ts. Done means the conditional required field is non-optional while the existing required:false opt-out remains intact.

Written by the indexing model from the issue text.

Assessment

Tech stack
graphql, typescript
Domain
api, backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.