Skip to content

bug: blocks without matching blockType are silently dropped, no error #16980

@jhb-dev

Description

@jhb-dev

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.

  1. Clone the reproduction repository and run the development server (pnpm dev).

  2. 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." }
  3. 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" } ] } } ] }
  4. 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
```

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions