payloadcms / payloadcms/payload

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

Open
#16,980 1 comment 0 reactions 1 assignee View on GitHub

@r1tsuu is already working on this.

Since Jun 17, 2026.

area: core created-by: Contributor
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.

  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
```

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.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.