composer/composer · error · InvalidArgumentException

The json file must be an object

Error message

The json file must be an object ({})

What it means

JsonManipulator's constructor throws an InvalidArgumentException unless the (trimmed) contents match '^\{.*\}$' (a single JSON object). JsonManipulator performs regex-based surgical edits on composer.json and assumes an object root; arrays, scalars, or garbage would break every edit method, so it refuses upfront. Empty input is normalized to '{}' first and does not throw.

Solutions

  1. Ensure the input is a valid JSON object — wrap arrays/scalars in an object, or use JsonFile instead for non-object documents.
  2. Repair corrupted composer.json: restore from git (`git checkout -- composer.json`) or re-init (`composer init`).
  3. If you are processing a non-object JSON file, parse/modify/encode with json_decode+json_encode rather than JsonManipulator.

Example fix

// before
$manipulator = new JsonManipulator('["a","b"]'); // array root
// The json file must be an object ({})

// after
$manipulator = new JsonManipulator('{"items":["a","b"]}'); // object root
Defensive patterns

Strategy: validation

Validate before calling

$trimmed = trim($contents);
if ($trimmed === '') { $trimmed = '{}'; }
if (!preg_match('#^\{.*\}$#s', $trimmed)) {
    throw new \RuntimeException('JsonManipulator requires a JSON object root; got: ' . substr($trimmed, 0, 60));
}
return new \Composer\Json\JsonManipulator($trimmed);

Type guard

function isJsonObjectRoot(string $contents): bool {
    $c = trim($contents);
    if ($c === '') return true; // normalized to '{}'
    return (bool) preg_match('#^\{.*\}$#s', $c);
}

Try / catch

try {
    $m = new JsonManipulator($contents);
} catch (\InvalidArgumentException $e) {
    if ($e->getMessage() === 'The json file must be an object ({})') {
        // fall back to decode/modify/encode for non-object JSON documents
        $data = json_decode($contents, true);
        // ...mutate $data...
        $out = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    } else {
        throw $e;
    }
}

Prevention

When it happens

Trigger: Constructing `new JsonManipulator($contents)` where $contents is a JSON array, a bare scalar, or malformed text rather than a top-level JSON object. Reached by `composer config`, plugin installers, and tooling that manipulate composer.json textually.

Common situations: Passing the contents of a file that is actually a JSON array (e.g. an installed.json fragment, a packages.json) into JsonManipulator; feeding it decoded-then-stringified non-object data; corrupt or truncated composer.json.

Related errors


AI-assisted analysis of composer/composer@c435d285c9 (2026-08-07). Data as JSON: /api/errors/5fe2125d23f00922. Report an issue: GitHub.

Appendix: source

Thrown at src/Composer/Json/JsonManipulator.php:48

       (?<object>    \{  (?:  (?&pair)  (?: , (?&pair)  )*+  )?+  \s*+ \} )
       (?<json>      \s*+ (?: (?&number) | (?&boolean) | (?&string) | (?&array) | (?&object) ) )
    )';

    /** @var string */
    private $contents;
    /** @var string */
    private $newline;
    /** @var string */
    private $indent;

    public function __construct(string $contents)
    {
        $contents = trim($contents);
        if ($contents === '') {
            $contents = '{}';
        }
        if (!Preg::isMatch('#^\{(.*)\}$#s', $contents)) {
            throw new \InvalidArgumentException('The json file must be an object ({})');
        }
        $this->newline = false !== strpos($contents, "\r\n") ? "\r\n" : "\n";
        $this->contents = $contents === '{}' ? '{' . $this->newline . '}' : $contents;
        $this->detectIndenting();
    }

    public function getContents(): string
    {
        return $this->contents . $this->newline;
    }

    public function addLink(string $type, string $package, string $constraint, bool $sortPackages = false): bool
    {
        $decoded = JsonFile::parseJson($this->contents);

        // no link of that type yet
        if (!isset($decoded[$type])) {
            return $this->addMainKey($type, [$package => $constraint]);

View on GitHub (pinned to c435d285c9)