Composer 2.9.x fails to validate expanded composer.json when project or CI steps add repositories
>>> [!note] Migrated issue
<!-- Drupal.org comment -->
<!-- Migrated from issue #3570436. -->
Reported by: [volman](https://www.drupal.org/user/3658178)
Related to !451
>>>
<h3 id="summary-problem-motivation">Problem/Motivation</h3>
<p>
<code>expand_composer_json.php</code> can generate an invalid <code>repositories</code> structure in the expanded <code>composer.json</code> if a project or CI step adds additional repositories (for example via <code>composer config repositories.NAME</code> or <code>composer repo add</code>).
</p>
<p>
The hardcoded default template treats repositories as an object, while CI-added repositories are effectively a list. When both are merged, this produces a mixed structure that ends up serialized as a JSON object with numeric keys.
</p>
<p>
Composer 2.9.x performs stricter schema validation and rejects this structure with errors such as:
</p>
<pre>repositories[n] : Matched a schema which it should not <br>repositories[n] : Failed to match all schemas <br>repositories[n] : Object value found, but a boolean is required <br>repositories[n] : Does not have a value in the enumeration [false] <br>repositories[n] : Failed to match at least one schema </pre><p>
Older Composer versions accepted this output more leniently, so the failure appears when upgrading to Composer 2.9.x.
</p>
<p>
Composer documentation states that the object/map form of <code>repositories</code> is deprecated and that the list/array form is preferred, as repository order is significant (source: <a href="https://getcomposer.org/doc/04-schema.md#repositories">https://getcomposer.org/doc/04-schema.md#repositories</a>).
</p>
<h4 id="summary-steps-reproduce">Steps to reproduce</h4>
<ol>
<li>
Create a minimal <code>composer.json</code> that represents a custom Drupal module and contains a single additional repository (as commonly added by CI steps):
<pre>{<br> "name": "drupal/example_module",<br> "type": "drupal-module",<br> "repositories": [<br> {<br> "name": "example",<br> "type": "package",<br> "package": {<br> "name": "example/library",<br> "version": "1.0.0",<br> "type": "drupal-library",<br> "dist": {<br> "type": "zip",<br> "url": "https://example.com/library.zip"<br> }<br> }<br> }<br> ],<br> "require": {},<br> "require-dev": {}<br>}</pre></li>
<li>
Use <code>expand_composer_json.php</code> from the GitLab templates:
<pre>curl -LO https://git.drupalcode.org/project/gitlab_templates/-/raw/main/scripts/expand_composer_json.php<br>chmod +x expand_composer_json.php</pre></li>
<li>
Set the minimal environment variables required by the script:
<pre>export CI_PROJECT_NAME=example_module<br>export PROJECT_NAME=example_module<br>export DRUPAL_CORE=11.3.2<br>export _WEB_ROOT=web</pre></li>
<li>
Run the expansion script:
<pre>php expand_composer_json.php --input composer.json</pre></li>
<li>
Inspect the resulting <code>repositories</code> section in the expanded <code>composer.json</code>. It will be emitted as a JSON object with mixed keys, similar to:
<pre>"repositories": {<br> "0": {<br> "name": "example",<br> "type": "package",<br> "package": { ... }<br> },<br> "drupal": {<br> "type": "composer",<br> "url": "https://packages.drupal.org/8"<br> }<br>}</pre></li>
<li>
Validate the expanded <code>composer.json</code> using Composer 2.9.x:
<pre>docker run --rm -v "$PWD":/app -w /app composer:2.9 validate --strict</pre></li>
<li>
Composer fails schema validation with errors similar to:
<pre> <br> "./composer.json" does not match the expected JSON schema: <br> - repositories[0] : Matched a schema which it should not <br> - repositories[0] : Failed to match all schemas <br> - repositories[0] : Object value found, but a boolean is required <br> - repositories[0] : Does not have a value in the enumeration [false] <br> - repositories[0] : Failed to match at least one schema </pre></li>
</ol>
<h3 id="summary-proposed-resolution">Proposed resolution</h3>
<p>
Normalize the <code>repositories</code> section in <code>expand_composer_json.php</code> so that it always uses the recommended list/array form.
</p>
<ul>
<li>Emit default repositories as a list instead of an object.</li>
<li>After deep-merging project and default configuration, normalize the result so that:
<ul>
<li>mixed numeric/string keys are converted to a sequential list,</li>
<li>map keys are preserved the as <code>name</code> property if not already set,</li>
<li>deprecated map-form disabling (e.g. <code>"packagist.org": false</code>) is preserved using its documented list-form equivalent.</li>
</ul>
</li>
</ul>
<p>
This aligns the generated <code>composer.json</code> with Composer’s current recommendations and restores compatibility with Composer 2.9.x while remaining compatible with older Composer versions.
</p>
<h3 id="summary-remaining-tasks">Remaining tasks</h3>
<ol>
<li>Implement</li>
<li>Test</li>
</ol>
<h3 id="summary-ui-changes">User interface changes</h3>
<p>N/A</p>
<h3 id="summary-api-changes">API changes</h3>
<p>N/A</p>
<h3 id="summary-data-model-changes">Data model changes</h3>
<p>N/A</p>
issue