WordPress FSE Block Validation Failed: The Hidden Cause of Missing JSON Quotes
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