payloadcms / payloadcms/payload

payload.create() requires fields that have both required: true and defaultValue in the data parameter

Open
#17,203 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

area: core invalid-reproduction 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

When a collection field is configured with both required: true and a defaultValue, Payload correctly fills in the default at runtime if the field is omitted from payload.create({ data: ... }). However, TypeScript still marks that field as required in the data type, causing a compile-time error even though the operation succeeds at runtime.

This is a known issue, as the comments in the code explain, but I can't find a public issue about this, so creating a new one to make it searchable by other users who suffer with this issue.

Link to the code that reproduces this issue

none

Reproduction Steps
Steps to reproduce
// collection config
{
  name: 'status',
  type: 'select',
  options: ['draft', 'published'],
  required: true,
  defaultValue: 'draft',
}
await payload.create({
  collection: 'posts',
  data: {
    title: 'Hello',
    // `status` omitted — Payload fills in 'draft' at runtime
  },
})
// TS Error: Property 'status' is missing in type '{ title: string; }'
// but required in type 'RequiredDataFromCollection<Post>'
Root cause

payload.create() types its data parameter as RequiredDataFromCollectionSlug<TSlug>, which is based on the output/read generated type. In that type, required: true fields are correctly non-optional (a saved document always has them). The utility type only makes the five system-managed fields optional: 1

It has no mechanism to also make defaultValue fields optional, because the generated output interface carries no defaultValue metadata — it only reflects the read shape.

The isOptionalOnInput logic that does account for defaultValue only activates for the 'input' schema variant used by generateInputTypes. Even with generateInputTypes: true enabled, payload.create() still uses RequiredDataFromCollectionSlug, not the generated PostInput type: 2

This is a known gap, documented as a @todo:

Workaround

Since PostInput (generated by generateInputTypes: true) correctly marks defaultValue fields as optional, type your write helper or seed script against it instead of relying on payload.create()'s built-in data type. A value typed as PostInput is always assignable to payload.create()'s data without any cast, because the input type is a valid subset of the output type:

import type { Config } from './payload-types'

type PostInput = Config['collectionsInput']['posts']
// `status` is `'draft' | 'published' | undefined` here — correctly optional

async function seedPost(data: PostInput) {
  // PostInput is assignable to payload.create()'s data — no cast needed
  return payload.create({
    collection: 'posts',
    data,
  })
}

// No TS error — status is optional in PostInput
await seedPost({ title: 'Hello' })

This is confirmed by the docs:

And verified by the type test suite:

Suggested fix

Have payload.create() and payload.update() use the input type for their data parameter when generateInputTypes: true is set. The @todo in the source already identifies this as the desired direction. The blocker is the read-modify-write pattern: a document fetched at depth > 0 has populated relationship objects, not IDs, and the strict ID-only input type would reject that. The fix requires the input type to accept either an ID or a full document for relationship fields before it can be used on the main write path.

Citations

File: packages/payload/src/collections/config/types.ts (L123-126)

export type RequiredDataFromCollection<TData extends JsonObject> = MarkOptional<
  TData,
  'collection' | 'createdAt' | 'deletedAt' | 'id' | 'updatedAt'
>

File: packages/payload/src/collections/operations/local/create.ts (L114-168)

export type Options<
  TSlug extends CollectionSlug,
  TSelect extends SelectType,
> = GeneratedTypes extends { strictDraftTypes: true }
  ? CollectionsWithoutDrafts extends TSlug
    ? {
        /**
         * The data for the document to create.
         */
        data: DataFromCollectionSlug<TSlug>
        /**
         * Create a **draft** document. [More](https://payloadcms.com/docs/versions/drafts#draft-api)
         */
        draft?: boolean
      } & BaseOptions<TSlug, TSelect>
    : TSlug extends CollectionsWithoutDrafts
      ? {
          data: RequiredDataFromCollectionSlug<TSlug>
          /**
           * The `draft` property is not allowed because this collection does not have `versions.drafts` enabled.
           */
          draft?: never
        } & BaseOptions<TSlug, TSelect>
      : (
          | {
              /**
               * The data for the document to create.
               */
              data: RequiredDataFromCollectionSlug<TSlug>
              /**
               * Create a **draft** document. [More](https://payloadcms.com/docs/versions/drafts#draft-api)
               * Omit this property or set to `false` to create a published document.
               */
              draft?: false
            }
          | {
              /**
               * The data for the document to create.
               * When creating a draft, required fields are optional as validation is skipped by default.
               */
              data: DraftDataFromCollectionSlug<TSlug>
              /**
               * Create a **draft** document. [More](https://payloadcms.com/docs/versions/drafts#draft-api)
               */
              draft: true
            }
        ) &
          BaseOptions<TSlug, TSelect>
  :
      | ({
          /**
           * The data for the document to create.
           */
          data: RequiredDataFromCollectionSlug<TSlug>
          /**

File: packages/payload/src/config/types.ts (L1598-1606)

     * @todo We'd like to turn this on by default (or have the Local API use the input type
     * directly), but there's a catch. When you read a document with `depth > 0`, its relationships
     * come back as full documents rather than IDs. A strict ID-only input type would reject that and
     * break the common "read a doc, change a field, save it back" pattern. To enable it by default,
     * the input type would first need to accept a relationship as either an ID or the full document
     * (which is what Payload already does at runtime), while keeping `id`, `defaultValue`, and
     * auto-managed fields optional. Until then it stays opt-in, so we don't put a type that's
     * stricter than the runtime on the main write path.
     */

File: docs/typescript/generating-types.mdx (L242-242)

The input types remain a valid **subset** of what those operations accept, so a value typed as `PostInput` is always assignable to `create` / `update` `data`. Reach for `PostInput` (or `Config['collectionsInput'][...]`) when you want to strictly type a write helper, a form payload, or a seed script.

File: test/types/types.spec.ts (L1401-1404)

    test('fields with a defaultValue are optional in write data', () => {
      expect<InputType['status']>().type.toBe<'draft' | 'published'>()
      expect<InputTypeInput['status']>().type.toBe<'draft' | 'published' | undefined>()
    })
Which area(s) are affected?

area: core

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.7
  react-dom: 19.2.7
Operating System:
  Platform: linux
  Arch: x64
  Version: #27-Ubuntu SMP PREEMPT_DYNAMIC Thu Jun 18 19:13:49 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

Read the @todo in packages/payload/src/config/types.ts and the create options in packages/payload/src/collections/operations/local/create.ts. Compare RequiredDataFromCollectionSlug in packages/payload/src/collections/config/types.ts with the input-type behavior covered by test/types/types.spec.ts. Done means create and update accept defaultValue fields as optional when input types are enabled without rejecting populated relationships.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
api, backend-api-design
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.