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:
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: blockName instead of blockType):
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: 400 validation error (block invalid / blockType missing / required heading missing).
-
Actual: 201 Created, block silently dropped:
{ "doc": { "title": "Wholesale", "layout": [], "id": "..." }, "message": "Page successfully created." }
-
Case B — control, valid blockType but missing the required heading: returns 400 as expected, proving validation does run when blockType matches:
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 blockType carrying 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.
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
```
Describe the Bug
When a document with a
blocksfield is created/updated through the REST API (or Local API), any block row whoseblockTypedoes not match a configured block — including rows that omitblockTypeentirely — is silently discarded. The request returns201 Created/200 OKwith 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.
blockNameinstead ofblockType:{ "id": "block-wholesale-hero", "blockName": "hero-alpha", /* ...fields... */ } // WRONG { "id": "block-wholesale-hero", "blockType": "hero-alpha", /* ...fields... */ } // CORRECTPayload 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
blockTypeis valid, the block's field validation runs and is enforced (a missing required field returns400). WhenblockTypeis 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
beforeChangefield hook, a block row is only validated/kept insideif (block) { ... }with noelse, so an unmatched row vanishes silently —packages/payload/src/fields/hooks/beforeChange/promise.ts:The blocks validator compounds this by filtering out rows without a
blockTypebefore doing anything, so a missing discriminator never registers as invalid —packages/payload/src/fields/validations.ts:Suggested fix. When a row cannot be matched to a configured block, push a
ValidationError(return400) 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
pagescollection has alayoutblocks field with one blockherothat has a requiredheadingtext 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):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:{ "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):400(unknown block type). Actual:201 Created,"layout": []— the"Our Catalog"content is silently discarded.A
GET /api/pagesafterwards 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
```