Skip to content

fix(flexible_content): accept the keyed layouts form ACF's admin writes - #35

Merged
parisek merged 2 commits into
mainfrom
fix/flexible-content-layouts-keyed-form
Aug 9, 2026
Merged

fix(flexible_content): accept the keyed layouts form ACF's admin writes#35
parisek merged 2 commits into
mainfrom
fix/flexible-content-layouts-keyed-form

Conversation

@parisek

@parisek parisek commented Aug 9, 2026

Copy link
Copy Markdown
Owner

From-Project

sloneek (redesign branch). composer lint:acf-json failed on
static/templates/component/team-gallery/acf.json with:

/fields/2/layouts — The data (object) must match the type: array

Why

The ref requires 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 the rule
looked 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:

input output layouts
keyed object OBJECT 4, names intact
array ARRAY 4, names intact

So neither shape is "the" export format, and both are consumed correctly.

What

layouts becomes a oneOf over the two containers. Layout constraints moved
into $defs/layout and are applied by both branches — nothing is relaxed:
key still ^layout_, name still snake_case, label still required,
sub_fields still recurse through field-item.schema.json. The keyed branch
additionally constrains propertyNames to ^layout_.

Mirrored to src/templates/refs/ and schemas/refs/ (byte-identical, per
AGENTS.md).

Limit of the keyed branch, stated up front

It constrains propertyNames to ^layout_ and validates each value as a layout.
It does not assert that a map key equals its layout's own key — JSON Schema
2020-12 cannot compare a property name against a nested value, so
{"layout_a": {"key": "layout_b"}} validates. ACF reads key from the layout
body 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_content had no fixtures at all before this. Three added:

Fixture Asserts
valid/starter_theme/component-probe-flexible_content-list array form validates
valid/starter_theme/component-probe-flexible_content-keyed keyed form validates — fails on the pre-change schema with exactly the downstream error
invalid/flexible_content-bad-layout-key a map keyed by the layout name still fails, so the fix does not degrade into a free-for-all

Verified 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 assertions
pass, 1 skipped (SnapshotTest, no ACF_SCHEMA_TEST_WP_ROOT — expected).
composer check green (phpunit + PHPStan level 8 + ADR index).

Deliberately not done

  • No normalisation, no preferred 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.
  • No change to ACF's own inconsistency. Worth an upstream ACF report; out of
    scope here.

Downstream note

sloneek fixed its file independently (converted to the array form and dropped
two dead "uploader": "" keys — that key is not a field property in ACF 6, the
uploader mode is a global setting read via acf_get_setting('uploader')). This
PR is about the next time someone edits a flexible-content group in wp-admin.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WFWbuftrwMwpRqzG91HyNU

…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)
@parisek parisek self-assigned this Aug 9, 2026
…nforcement

A review pass read "keyed by layout key" as a guarantee the schema does not
make. The keyed branch constrains `propertyNames` to `^layout_` and validates
each value as a layout, but it cannot assert that a map key equals its layout's
own `key` — JSON Schema 2020-12 has no way to compare a property name against a
nested value, so `{"layout_a": {"key": "layout_b"}}` validates.

Documented as a deliberate limit rather than left to be discovered: ACF reads
`key` from the layout body and never from the map key, so the equality carries
no runtime meaning. Nothing about the schema changed — only the claim made
about it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WFWbuftrwMwpRqzG91HyNU (tools@portadesign.cz)
@parisek

parisek commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

Agent review — Codex (gpt-5.3-codex), 2026-08-09

Three passes over this branch and its downstream companion. Recorded as a comment, not a formal review — an agent should not spend merge-gating weight.

Changed the PR (1 finding, accepted):

  • The keyed branch does not enforce that a map key equals its layout's own key. Correct: JSON Schema 2020-12 cannot compare a property name against a nested value, so {"layout_a": {"key": "layout_b"}} validates. The original CHANGELOG wording ("keyed by layout key") read as a guarantee the schema does not make. Fixed in 3f146fc by stating the limit explicitly rather than attempting enforcement — ACF reads key from the layout body and never from the map key, so the equality carries no runtime meaning. PR description updated to match.

Rejected (0).

Confirmed clean: no oneOf ambiguity (array and object are disjoint JSON types); the layout constraints apply identically through $defs/layout on both branches; the only flexible-content field in the downstream theme remains valid.

Reviewer limitation worth noting: the sandbox could not reach Docker, so every claim about live ACF behaviour in this PR was verified on our side, not Codex's — the import→export round-trip table in the description comes from a real ACF Pro 6.8.6 install, and Codex confirmed only the source-level reasoning.

CI green on 3f146fc: test (8.3), test (8.4), composer hygiene, ADR index.

@parisek
parisek marked this pull request as ready for review August 9, 2026 10:02
@parisek
parisek merged commit 80d848d into main Aug 9, 2026
4 checks passed
@parisek
parisek deleted the fix/flexible-content-layouts-keyed-form branch August 9, 2026 10:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant