payloadcms / payloadcms/payload
bug: blocks without matching blockType are silently dropped, no error
@r1tsuu is already working on this.
Since Jun 17, 2026.
- Dominant language
- TypeScript
- Stars
- 44.8k
- Forks
- 4.2k
- Avg merge
- 2d 21h
- Merged PRs (30d)
- 53
Description
Describe the Bug
When a document with a blocks field is created/updated through the REST API (or Local API), any block row whose blockType does not match a configured block — including rows that omit blockType entirely — is silently discarded. The request returns 201 Created / 200 OK with no error, and the persisted array simply does not contain the row.
This is dangerous when content is written programmatically (an AI agent or a migration script). A common mistake is sending the discriminator under the wrong key — e.g. blockName instead of blockType:
{ "id": "block-wholesale-hero", "blockName": "hero-alpha", /* ...fields... */ } // WRONG
{ "id": "block-wholesale-hero", "blockType": "hero-alpha", /* ...fields... */ } // CORRECT
Payload accepts the wrong version without complaint, so the caller gets no signal the data was invalid. The result is documents with empty layout content — broken pages — that appear to have saved successfully.
The inconsistency is the core problem: when blockType is valid, the block's field validation runs and is enforced (a missing required field returns 400). When blockType is missing or unknown, all of that validation is skipped and the row is dropped without error. "Block could not be matched to a configured block" is strictly worse than a normal validation failure, yet it is the only case that does not produce an error.
Root cause. In the beforeChange field hook, a block row is only validated/kept inside if (block) { ... } with no else, so an unmatched row vanishes silently — packages/payload/src/fields/hooks/beforeChange/promise.ts:
const blockTypeToMatch = row.blockType || rowSiblingDoc.blockType
const block =
req.payload.blocks[blockTypeToMatch] ??
(field.blockReferences ?? field.blocks).find(
(curBlock) => typeof curBlock !== 'string' && curBlock.slug === blockTypeToMatch,
)
if (block) {
promises.push(traverseFields({ /* ... fields: block.fields ... */ })) // validation only here
}
// no `else` → unmatched row is never validated and is dropped silently
The blocks validator compounds this by filtering out rows without a blockType before doing anything, so a missing discriminator never registers as invalid — packages/payload/src/fields/validations.ts:
const allBlockSlugs = Array.isArray(value)
? value.map((b) => b.blockType).filter((s) => Boolean(s))
: []
Suggested fix. When a row cannot be matched to a configured block, push a ValidationError (return 400) instead of silently skipping it, so programmatic callers get a clear signal.
Link to the code that reproduces this issue
https://github.com/jhb-dev/payload-rest-blocks-missing-blocktype
Reproduction Steps
The pages collection has a layout blocks field with one block hero that has a required heading text field. The collection uses open access so the calls below need no auth.
-
Clone the reproduction repository and run the development server (
pnpm dev). -
Case A — block with no
blockType(the real-world mistake:blockNameinstead ofblockType):curl -i -X POST http://localhost:3000/api/pages \ -H "Content-Type: application/json" \ -d '{"title":"Wholesale","layout":[{"id":"block-wholesale-hero","blockName":"hero-alpha"}]}'-
Expected:
400validation error (block invalid /blockTypemissing / requiredheadingmissing). -
Actual:
201 Created, block silently dropped:{ "doc": { "title": "Wholesale", "layout": [], "id": "..." }, "message": "Page successfully created." }
-
-
Case B — control, valid
blockTypebut missing the requiredheading: returns400as expected, proving validation does run whenblockTypematches:curl -i -X POST http://localhost:3000/api/pages \ -H "Content-Type: application/json" \ -d '{"title":"Retail","layout":[{"id":"block-retail-hero","blockType":"hero"}]}'{ "errors": [ { "name": "ValidationError", "message": "The following field is invalid: Layout > Block 1 (Hero) > Heading", "data": { "errors": [ { "message": "This field is required.", "path": "layout.0.heading" } ] } } ] } -
Case C — unknown
blockTypecarrying real field data (data loss):curl -i -X POST http://localhost:3000/api/pages \ -H "Content-Type: application/json" \ -d '{"title":"Catalog","layout":[{"blockType":"hero-alpha","heading":"Our Catalog"}]}'- Expected:
400(unknown block type). Actual:201 Created,"layout": []— the"Our Catalog"content is silently discarded.
- Expected:
A GET /api/pages afterwards confirms both invalid pages were persisted with empty layouts (layout: []).
Which area(s) are affected?
area: core
Environment Info
```
payload: 3.85.1
next: 16.2.6
@payloadcms/db-mongodb: 3.85.1
@payloadcms/richtext-lexical: 3.85.1
Node: 22.19.0
pnpm: 11.5.1
Platform: darwin arm64
```
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.
Assessment
This issue has not been assessed yet.