Composer 2.9.x fails to validate expanded composer.json when project or CI steps add repositories

Migrated issue

Reported by: volman

Related to !451 (merged)

Problem/Motivation

expand_composer_json.php can generate an invalid repositories structure in the expanded composer.json if a project or CI step adds additional repositories (for example via composer config repositories.NAME or composer repo add).

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.

Composer 2.9.x performs stricter schema validation and rejects this structure with errors such as:

repositories[n] : Matched a schema which it should not              
repositories[n] : Failed to match all schemas                      
repositories[n] : Object value found, but a boolean is required    
repositories[n] : Does not have a value in the enumeration [false] 
repositories[n] : Failed to match at least one schema   

Older Composer versions accepted this output more leniently, so the failure appears when upgrading to Composer 2.9.x.

Composer documentation states that the object/map form of repositories is deprecated and that the list/array form is preferred, as repository order is significant (source: https://getcomposer.org/doc/04-schema.md#repositories).

Steps to reproduce

  1. Create a minimal composer.json that represents a custom Drupal module and contains a single additional repository (as commonly added by CI steps):
    {
      "name": "drupal/example_module",
      "type": "drupal-module",
      "repositories": [
        {
          "name": "example",
          "type": "package",
          "package": {
            "name": "example/library",
            "version": "1.0.0",
            "type": "drupal-library",
            "dist": {
              "type": "zip",
              "url": "https://example.com/library.zip"
            }
          }
        }
      ],
      "require": {},
      "require-dev": {}
    }
  2. Use expand_composer_json.php from the GitLab templates:
    curl -LO https://git.drupalcode.org/project/gitlab_templates/-/raw/main/scripts/expand_composer_json.php
    chmod +x expand_composer_json.php
  3. Set the minimal environment variables required by the script:
    export CI_PROJECT_NAME=example_module
    export PROJECT_NAME=example_module
    export DRUPAL_CORE=11.3.2
    export _WEB_ROOT=web
  4. Run the expansion script:
    php expand_composer_json.php --input composer.json
  5. Inspect the resulting repositories section in the expanded composer.json. It will be emitted as a JSON object with mixed keys, similar to:
    "repositories": {
      "0": {
        "name": "example",
        "type": "package",
        "package": { ... }
      },
      "drupal": {
        "type": "composer",
        "url": "https://packages.drupal.org/8"
      }
    }
  6. Validate the expanded composer.json using Composer 2.9.x:
    docker run --rm -v "$PWD":/app -w /app composer:2.9 validate --strict
  7. Composer fails schema validation with errors similar to:
                           
      "./composer.json" does not match the expected JSON schema:            
       - repositories[0] : Matched a schema which it should not             
       - repositories[0] : Failed to match all schemas                      
       - repositories[0] : Object value found, but a boolean is required    
       - repositories[0] : Does not have a value in the enumeration [false] 
       - repositories[0] : Failed to match at least one schema  

Proposed resolution

Normalize the repositories section in expand_composer_json.php so that it always uses the recommended list/array form.

  • Emit default repositories as a list instead of an object.
  • After deep-merging project and default configuration, normalize the result so that:
    • mixed numeric/string keys are converted to a sequential list,
    • map keys are preserved the as name property if not already set,
    • deprecated map-form disabling (e.g. "packagist.org": false) is preserved using its documented list-form equivalent.

This aligns the generated composer.json with Composer’s current recommendations and restores compatibility with Composer 2.9.x while remaining compatible with older Composer versions.

Remaining tasks

  1. Implement
  2. Test

User interface changes

N/A

API changes

N/A

Data model changes

N/A