Skip to main content

WordPress FSE Block Validation Failed: The Hidden Cause of Missing JSON Quotes

Β· 4 min read

TL;DR​

In WordPress FSE themes, if a JSON attribute in a Pattern/Template HTML comment has a missing closing quote ", the brace count remains balanced, but parse_blocks() silently sets the block's attrs to null. Gutenberg's save function then produces no inline styles, triggering Block validation failed. Validate JSON with json.loads() to catch this.

The Problem​

Opening the wishlist template in WordPress Site Editor shows a console error:

Block validation: Block validation failed for `core/group`

Content generated by `save` function:
<div class="wp-block-group has-border-color has-neutral-200-border-color"></div>

Content retrieved from post body:
<div class="wp-block-group has-border-color has-neutral-200-border-color"
style="border-style:solid;border-width:1px;border-radius:var(--wp--custom--border--radius--lg);
padding-top:var(--wp--preset--spacing--40);...">

The save function outputs correct CSS classes but completely loses inline styles, and the content is empty β€” even though the file clearly contains style attributes and child blocks.

Root Cause​

The issue is in the JSON attributes of a core/group block HTML comment:

<!-- Broken -->
<!-- wp:group {"style":{"spacing":{"padding":{"top":"var(--wp--preset--spacing--40)",
"right":"var(--wp--preset--spacing--40)",
"bottom":"var(--wp--preset--spacing--40)",
"left":"var(--wp--preset--spacing--40)"}}, <!-- missing closing quote -->
"border":{"radius":"var(--wp--custom--border--radius--lg)","width":"1px","style":"solid"}},
"borderColor":"neutral-200","layout":{"type":"constrained"}} -->

The "left" value "var(--wp--preset--spacing--40)" is missing its closing quote ". It's written as "var(--wp--preset--spacing--40).

Why brace counting misses this:

Correct: { "left": "value" }  β†’ quotes paired, braces balanced
Broken: { "left": "value } β†’ quotes unpaired, but braces still balanced

When the closing quote is missing, the } characters are treated as string content by the JSON parser (the quote never closed), so the brace count stays balanced.

parse_blocks() doesn't throw an error β€” it silently sets attrs to null:

// What parse_blocks returns
[
'blockName' => 'core/group',
'attrs' => null, // entire attribute object discarded
'innerHTML' => '<div ...>', // raw HTML still present
]

Gutenberg calls save() with null attrs, produces no inline styles, and the mismatch triggers Block validation failed.

Why this is hard to spot:

  • No white screen β€” the page still renders (falls back to innerHTML)
  • Braces are balanced, so visual inspection easily misses it
  • Site Editor shows a subtle "block needs recovery" notice
  • Audit scripts typically check brace balance and attribute correspondence, not JSON validity

Solution​

1. Locate the Problem​

Validate JSON with Python:

python3 -c "
import json
with open('templates/wishlist.html') as f:
content = f.read()
marker = 'wp:group {'
start = content.index(marker) + len(marker) - 1
end = content.index(' -->', start)
json_str = content[start:end]
try:
json.loads(json_str)
print('JSON OK')
except json.JSONDecodeError as e:
print(f'Error at position {e.pos}: {e.msg}')
print(f'Context: ...{json_str[max(0,e.pos-20):e.pos+20]}...')
"

Output pinpoints the exact error:

Error at position 196: Expecting ',' delimiter
Context: ...g--40)}},"border":{"...

2. Fix the JSON​

Add the missing closing quote after the "left" value:

<!-- Fixed -->
<!-- wp:group {"style":{"spacing":{"padding":{"top":"var(--wp--preset--spacing--40)",
"right":"var(--wp--preset--spacing--40)",
"bottom":"var(--wp--preset--spacing--40)",
"left":"var(--wp--preset--spacing--40)"}}, <!-- closing quote added -->
"border":{"radius":"var(--wp--custom--border--radius--lg)","width":"1px","style":"solid"}},
"borderColor":"neutral-200","layout":{"type":"constrained"}} -->

3. Verify the Fix​

# Verify parse_blocks correctly parses the block
docker exec wp_cli wp eval '
$blocks = parse_blocks(file_get_contents(get_stylesheet_directory() . "/templates/wishlist.html"));
echo $blocks[...]["attrs"]["style"]["border"]["radius"];
' --allow-root

4. Prevention​

Add a JSON comment validity check to CI:

import json, re, sys

def check_block_json(filepath):
with open(filepath) as f:
content = f.read()
for m in re.finditer(r'<!-- wp:\w+ (\{.*?\}) -->', content):
try:
json.loads(m.group(1))
except json.JSONDecodeError as e:
print(f"{filepath}: JSON error at comment position {m.start()}: {e}")
sys.exit(1)

check_block_json(sys.argv[1])

Interested in similar solutions? Get in touch