payloadcms / payloadcms/payload
payload.create() requires fields that have both required: true and defaultValue in the data parameter
Nobody has claimed this yet.
- 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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