Skip to content

Keep nested objects as objects when JsonManipulator rewrites a node - #13210

Merged
Seldaek merged 4 commits into
composer:mainfrom
b84330808:fix-json-manipulator-empty-objects
Nov 3, 2026
Merged

Seldaek merged 4 commits into
composer:mainfrom
b84330808:fix-json-manipulator-empty-objects

Conversation

@b84330808

Copy link
Copy Markdown
Contributor

When composer config adds or removes a dotted sub-key, JsonManipulator decodes the surrounding node with json_decode($json, true) and formats it back. Any nested {} comes back as [], and an object keyed "0", "1"… comes back as a list.

In config this breaks the schema check. A plain sequence of commands is enough:

composer config allow-plugins.acme/plugin true
composer config --unset allow-plugins.acme/plugin   # leaves "allow-plugins": {}
composer config platform.php 8.2.0
composer config platform.ext-intl 1.0
composer config --unset platform.ext-intl

The last one fails and reverts the file:

Failed to update composer.json with a valid format, reverting to the original content. Please report an issue to us with details (command you run and a copy of your composer.json).
config.allow-plugins : Array value found, but an object or a boolean is required

In extra there's no schema, so it silently rewrites things instead, e.g. "patches": {} becomes "patches": [] after composer config --unset extra.foo.bar.

Now only the levels that get modified are turned into arrays and deeper objects stay stdClass, so format() writes them back as objects. format() also no longer writes an object as a list just because its keys are sequential. Added tests for the add/remove sub-key paths in JsonManipulatorTest.

I used Claude Code to write this change.

Adding or removing a dotted sub-key (e.g. `composer config --unset
platform.php` or `composer config extra.foo.bar baz`) decoded the
surrounding node with json_decode($json, true) and formatted it back, so
every nested `{}` came out as `[]` and objects keyed 0..n came out as
lists. In config this made the schema check fail ("config.allow-plugins :
Array value found, but an object or a boolean is required") and the
command reverted the file.

Only the levels that get modified are decoded to arrays now, deeper
objects stay stdClass, and format() no longer writes an object as a list
because its keys happen to be sequential.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@Seldaek Seldaek added this to the 2.11 milestone Nov 2, 2026
@composer composer deleted a comment from fadaile Nov 2, 2026
…hapes

Converting stdClass to an array one level at a time lost the object-ness of
the edited level, so objects with keys 0..n-1 were still written as lists,
and an emptied list was written as {}. Removing a list item now reindexes it.
isset() accepts a numeric offset into a string, so removing extra.foo.0 from a
string value crashed with "Cannot unset string offsets". A "0" sub-key was
also skipped by the falsy check.
@Seldaek
Seldaek merged commit bec4675 into composer:main Nov 3, 2026
23 checks passed
@Seldaek

Seldaek commented Nov 3, 2026

Copy link
Copy Markdown
Member

Thanks

gemmaro pushed a commit to phpusers-ja/composer that referenced this pull request Nov 9, 2026
…omposer#13210)

* Keep nested objects as objects when JsonManipulator rewrites a node

Adding or removing a dotted sub-key (e.g. `composer config --unset
platform.php` or `composer config extra.foo.bar baz`) decoded the
surrounding node with json_decode($json, true) and formatted it back, so
every nested `{}` came out as `[]` and objects keyed 0..n came out as
lists. In config this made the schema check fail ("config.allow-plugins :
Array value found, but an object or a boolean is required") and the
command reverted the file.

Only the levels that get modified are decoded to arrays now, deeper
objects stay stdClass, and format() no longer writes an object as a list
because its keys happen to be sequential.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Edit decoded objects in place so sub-key edits keep object and list shapes

Converting stdClass to an array one level at a time lost the object-ness of
the edited level, so objects with keys 0..n-1 were still written as lists,
and an emptied list was written as {}. Removing a list item now reindexes it.

* Skip removing a sub-key of a scalar value

isset() accepts a numeric offset into a string, so removing extra.foo.0 from a
string value crashed with "Cannot unset string offsets". A "0" sub-key was
also skipped by the falsy check.

* Add return type to sub-key edit test provider

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Jordi Boggiano <[email protected]>
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.

2 participants