…ites
`refs/field-flexible_content.schema.json` required `layouts` to be a JSON
array. That matches ACF Pro 6.8.6's own published field schema
(`schemas/fields/v1/flexible_content.json`), so it looked unambiguously
right — but ACF does not hold to it.
ACF's field-group admin renders each layout's settings inputs under
`{$field['prefix']}[layouts][{$layout['key']}]`
(`pro/fields/class-acf-field-flexible-content.php:348`). A save in wp-admin
therefore POSTs an associative array keyed by layout key, and the local-JSON
writer emits it as a JSON **object**. The package was rejecting a file ACF
had just written.
ACF normalises neither shape. Round-tripping the same field group through
`acf_prepare_field_group_for_import()` → `acf_prepare_field_group_for_export()`
on ACF Pro 6.8.6 returns the array form as an array and the keyed form as an
object, both with all layouts intact — so neither shape can be called "the"
export format, and both are consumed correctly at runtime.
`layouts` is now a `oneOf` over the two containers. The layout constraints
moved into `$defs/layout` and are applied by both branches, so nothing is
relaxed: key still `^layout_`, name still snake_case, label still required,
sub_fields still recurse through `field-item.schema.json`.
Three fixtures, all previously absent — the type had no coverage at all:
- `valid/starter_theme/component-probe-flexible_content-list` (array form)
- `valid/starter_theme/component-probe-flexible_content-keyed` (keyed form) —
fails on the pre-change schema with exactly the downstream error,
`/fields/0/layouts: The data (object) must match the type: array`
- `invalid/flexible_content-bad-layout-key` — a map keyed by the layout *name*
instead of its key, guarding against the fix degrading into a free-for-all
Not done: no attempt to normalise or prefer one shape. Emitting a preference
would put this package ahead of ACF on a question ACF has not settled, and
would churn every downstream file on the next admin save.
Found downstream on `sloneek`: `composer lint:acf-json` failed on a
hand-authored component modelled on ACF-5-era exports.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WFWbuftrwMwpRqzG91HyNU (tools@portadesign.cz)
From-Project
sloneek(redesign branch).composer lint:acf-jsonfailed onstatic/templates/component/team-gallery/acf.jsonwith:Why
The ref requires
layoutsto be a JSON array. That matches ACF Pro 6.8.6's ownpublished field schema (
schemas/fields/v1/flexible_content.json), so the rulelooked unambiguously correct. ACF does not hold to it.
ACF's field-group admin renders each layout's settings inputs under
{$field['prefix']}[layouts][{$layout['key']}](
pro/fields/class-acf-field-flexible-content.php:348),so a save in wp-admin POSTs an associative array keyed by layout key and the
local-JSON writer emits a JSON object. This package was rejecting a file ACF
had just written — a recurring false failure, not a one-time authoring mistake.
ACF normalises neither shape. Round-tripping the same group through
acf_prepare_field_group_for_import()→acf_prepare_field_group_for_export()on 6.8.6:
OBJECTARRAYSo neither shape is "the" export format, and both are consumed correctly.
What
layoutsbecomes aoneOfover the two containers. Layout constraints movedinto
$defs/layoutand are applied by both branches — nothing is relaxed:keystill^layout_,namestill snake_case,labelstill required,sub_fieldsstill recurse throughfield-item.schema.json. The keyed branchadditionally constrains
propertyNamesto^layout_.Mirrored to
src/templates/refs/andschemas/refs/(byte-identical, perAGENTS.md).
Limit of the keyed branch, stated up front
It constrains
propertyNamesto^layout_and validates each value as a layout.It does not assert that a map key equals its layout's own
key— JSON Schema2020-12 cannot compare a property name against a nested value, so
{"layout_a": {"key": "layout_b"}}validates. ACF readskeyfrom the layoutbody and never from the map key, so the equality carries no runtime meaning. Raised
by a review pass that read the original wording as a guarantee; documented rather
than papered over.
Coverage
flexible_contenthad no fixtures at all before this. Three added:valid/starter_theme/component-probe-flexible_content-listvalid/starter_theme/component-probe-flexible_content-keyedinvalid/flexible_content-bad-layout-keyVerified before/after by checking out the baseline ref and re-running: the keyed
fixture fails with
/fields/0/layouts: The data (object) must match the type: array, the list fixture passes. With the change, 197 tests / 716 assertionspass, 1 skipped (
SnapshotTest, noACF_SCHEMA_TEST_WP_ROOT— expected).composer checkgreen (phpunit + PHPStan level 8 + ADR index).Deliberately not done
package ahead of ACF on a question ACF has not settled, and would churn every
downstream file on the next admin save.
scope here.
Downstream note
sloneekfixed its file independently (converted to the array form and droppedtwo dead
"uploader": ""keys — that key is not a field property in ACF 6, theuploader mode is a global setting read via
acf_get_setting('uploader')). ThisPR is about the next time someone edits a flexible-content group in wp-admin.
🤖 Generated with Claude Code
https://claude.ai/code/session_01WFWbuftrwMwpRqzG91HyNU