This project provides a composer
package with generic and vendor-specific normalizers for normalizing JSON documents.
Run
composer require ergebnis/json-normalizer
This project comes with
This project comes with the following generic normalizers:
Ergebnis\Json\Normalizer\CallableNormalizer
Ergebnis\Json\Normalizer\ChainNormalizer
Ergebnis\Json\Normalizer\FormatNormalizer
Ergebnis\Json\Normalizer\IndentNormalizer
Ergebnis\Json\Normalizer\JsonEncodeNormalizer
Ergebnis\Json\Normalizer\SchemaNormalizer
Ergebnis\Json\Normalizer\WithFinalNewLineNormalizer
Ergebnis\Json\Normalizer\WithoutFinalNewLineNormalizer
💡 All of these normalizers implement the Ergebnis\Json\Normalizer\Normalizer
.
When you want to normalize a JSON file with a callable
, you can use the CallableNormalizer
.
<?php
declare(strict_types=1);
use Ergebnis\Json\Json;
use Ergebnis\Json\Normalizer;
$encoded = <<<'JSON'
{
"name": "Andreas Möller",
"url": "https://localheinz.com"
}
JSON;
$json = Json::fromString($encoded);
$callable = function (Json $json): Json {
$decoded = $json->decoded();
foreach (get_object_vars($decoded) as $name => $value) {
if ('https://localheinz.com' !== $value) {
continue;
}
$decoded->{$name} .= '/open-source/';
}
return Json::fromString(json_encode($decoded));
};
$normalizer = new Normalizer\CallableNormalizer($callable);
$normalized = $normalizer->normalize($json);
The normalized version will now have the callable applied to it.
When you want to apply multiple normalizers in a chain, you can use the ChainNormalizer
.
<?php
declare(strict_types=1);
use Ergebnis\Json\Json;
use Ergebnis\Json\Normalizer;
use Ergebnis\Json\Printer;
$encoded = <<<'JSON'
{
"name": "Andreas Möller",
"url": "https://localheinz.com"
}
JSON;
$json = Json::fromString($encoded);
$indent = Normalizer\Format\Indent::fromString(' ');
$jsonEncodeOptions = Normalizer\Format\JsonEncodeOptions::fromInt(JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$normalizer = new Normalizer\ChainNormalizer(
new Normalizer\JsonEncodeNormalizer($jsonEncodeOptions),
new Normalizer\IndentNormalizer(
$indent,
new Printer\Printer()
),
new Normalizer\WithFinalNewLineNormalizer()
);
$normalized = $normalizer->normalize($json);
The normalized version will now contain the result of applying all normalizers in a chain, one after another.
💡 Be careful with the order of the normalizers, as one normalizer might override changes a previous normalizer applied.
When you want to normalize a JSON file with a formatting, you can use the FormatNormalizer
.
<?php
declare(strict_types=1);
use Ergebnis\Json\Json;
use Ergebnis\Json\Normalizer;
use Ergebnis\Json\Printer;
$encoded = <<<'JSON'
{
"name": "Andreas Möller",
"emoji": "🤓",
"url": "https://localheinz.com"
}
JSON;
$json = Json::fromString($encoded);
$format = Normalizer\Format\Format::create(
Normalizer\Format\Indent::fromString(' '),
Normalizer\Format\JsonEncodeOptions::fromInt(JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
Normalizer\Format\NewLine::fromString("\r\n")
true
);
$normalizer = new Normalizer\FormatNormalizer(
new Printer\Printer(),
$format,
);
$normalized = $normalizer->normalize($json);
The normalized version will now have formatting applied according to $format
.
When you need to adjust the indentation of a JSON file, you can use the IndentNormalizer
.
<?php
declare(strict_types=1);
use Ergebnis\Json\Json;
use Ergebnis\Json\Normalizer;
use Ergebnis\Json\Printer;
$encoded = <<<'JSON'
{
"name": "Andreas Möller",
"url": "https://localheinz.com"
}
JSON;
$json = Json::fromString($encoded);
$indent = Normalizer\Format\Indent::fromString(' ');
$normalizer = new Normalizer\IndentNormalizer(
$indent,
new Printer\Printer()
);
$normalized = $normalizer->normalize($json);
The normalized version will now be indented with 2 spaces.
When you need to adjust the encoding of a JSON file, you can use the JsonEncodeNormalizer
.
<?php
declare(strict_types=1);
use Ergebnis\Json\Json;
use Ergebnis\Json\Normalizer;
$encoded = <<<'JSON'
{
"name": "Andreas M\u00f6ller",
"url": "https:\/\/localheinz.com"
}
JSON;
$json = Json::fromString($encoded);
$jsonEncodeOptions = Normalizer\Format\JsonEncodeOptions::fromInt(JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$normalizer = new Normalizer\JsonEncodeNormalizer($jsonEncodeOptions);
$normalized = $normalizer->normalize($json);
The normalized version will now be encoded with $jsonEncodeOptions
.
💡 For reference, see json_encode()
and the corresponding JSON constants.
When you want to rebuild a JSON file according to a JSON schema, you can use the SchemaNormalizer
.
Let's assume the following schema
{
"type": "object",
"additionalProperties": true,
"properties": {
"name" : {
"type" : "string"
},
"role" : {
"type" : "string"
}
}
}
exists at /schema/example.json
.
<?php
declare(strict_types=1);
use Ergebnis\Json\Normalizer;
use Ergebnis\Json\Pointer;
use Ergebnis\Json\SchemaValidator;
use JsonSchema\SchemaStorage;
$encoded = <<<'JSON'
{
"url": "https://localheinz.com",
"name": "Andreas Möller",
"open-source-projects": {
"ergebnis/data-provider": {
"downloads": {
"total": 2,
"monthly": 1
}
},
"ergebnis/composer-normalize": {
"downloads": {
"total": 5,
"monthly": 2
}
}
}
}
JSON;
$json = Json::fromString($encoded);
$normalizer = new Normalizer\SchemaNormalizer(
'file:///schema/example.json',
new SchemaStorage(),
new SchemaValidator\SchemaValidator(),
Pointer\Specification::never()
);
$normalized = $normalizer->normalize($json);
The normalized version will now be structured according to the JSON schema (in this simple case, properties will be reordered as found in the schema and additional properties will be ordered by name). Internally, the SchemaNormalizer
uses justinrainbow/json-schema
to resolve schemas, as well as to ensure (before and after normalization) that the JSON document is valid.
If you have properties that you do not want to be reordered, you can use a Pointer\Specification
to specify which properties should not be reordered.
<?php
declare(strict_types=1);
use Ergebnis\Json\Normalizer;
use Ergebnis\Json\Pointer;
use Ergebnis\Json\SchemaValidator;
use JsonSchema\SchemaStorage;
$encoded = <<<'JSON'
{
"url": "https://localheinz.com",
"name": "Andreas Möller",
"open-source-projects": {
"ergebnis/data-provider": {
"downloads": {
"total": 2,
"monthly": 1
}
},
"ergebnis/composer-normalize": {
"downloads": {
"total": 5,
"monthly": 2
}
}
}
}
JSON;
$json = Json::fromString($encoded);
$normalizer = new Normalizer\SchemaNormalizer(
'file:///schema/example.json',
new SchemaStorage(),
new SchemaValidator\SchemaValidator(),
Pointer\Specification::equals(Pointer\JsonPointer::fromJsonString('/open-source-projects'))
);
$normalized = $normalizer->normalize($json);
💡 For more information about JSON schema, visit json-schema.org.
When you want to ensure that a JSON file has a single final new line, you can use the WithFinalNewLineNormalizer
.
<?php
declare(strict_types=1);
use Ergebnis\Json\Normalizer;
$encoded = <<<'JSON'
{
"name": "Andreas Möller",
"url": "https://localheinz.com"
}
JSON;
$json = Json::fromString($encoded);
$normalizer = new Normalizer\WithFinalNewLineNormalizer();
$normalized = $normalizer->normalize($json);
The normalized version will now have a single final new line.
When you want to ensure that a JSON file does not have a final new line, you can use the WithoutFinalNewLineNormalizer
.
<?php
declare(strict_types=1);
use Ergebnis\Json\Normalizer;
$encoded = <<<'JSON'
{
"name": "Andreas Möller",
"url": "https://localheinz.com"
}
JSON;
$json = Json::fromString($encoded);
$normalizer = new Normalizer\WithoutFinalNewLineNormalizer();
$normalized = $normalizer->normalize($json);
The normalized version will now not have a final new line or any whitespace at the end.
This project comes with the following vendor-specific normalizers:
The Vendor\Composer\ComposerJsonNormalizer
can be used to normalize a composer.json
file according to its underlying JSON schema.
It composes the following normalizers:
Ergebnis\Composer\Json\Normalizer\Vendor\Composer\BinNormalizer
Ergebnis\Composer\Json\Normalizer\Vendor\Composer\ConfigHashNormalizer
Ergebnis\Composer\Json\Normalizer\Vendor\Composer\PackageHashNormalizer
Ergebnis\Composer\Json\Normalizer\Vendor\Composer\RepositoriesHashNormalizer
Ergebnis\Composer\Json\Normalizer\Vendor\Composer\VersionConstraintNormalizer
Ergebnis\Composer\Json\Normalizer\Vendor\WithFinalNewLineNormalizer
When composer.json
contains an array of scripts in the bin
section, the Vendor\Composer\BinNormalizer
will sort the elements of the bin
section by value in ascending order.
When composer.json
contains configuration in the config
section, the Vendor\Composer\ConfigHashNormalizer
will sort the content of these sections by key in ascending order.
The allow-plugins
and preferred-install
configuration options support keys with wildcards and require special handling.
When these keys do not use wildcards, then these keys are sorted in ascending order. When these keys use wildcards, these keys are sorted when the wildcards are at the end of package names. Due to internal implementation details of the wildcard feature within composer
, sorting keys with wildcards in the middle is not feasible.
When composer.json
contains any configuration in the
sections, the Vendor\Composer\PackageHashNormalizer
will sort the packages in these sections.
💡 This transfers the behaviour from using the --sort-packages
or sort-packages
configuration flag in require
and require-dev
to other sections.
When composer.json
contains any configuration in the
section, the Vendor\Composer\RepositoriesHashNormalizer
will sort the repositories listed in the exclude
and only
properties of repositories.
When composer.json
contains version constraints in the
sections, the Vendor\Composer\VersionConstraintNormalizer
will ensure that
-
all version constraints are trimmed
{ "homepage": "https://getcomposer.org/doc/articles/versions.md#version-range", "require": { - "php": " ^8.2 " + "php": "^8.2" }
-
version constraints separated by a space (
,
) - treated as a logical and - are separated by a space ({ "homepage": "https://getcomposer.org/doc/articles/versions.md#version-range", "require": { - "foo/bar": "1.2.3,2.3.4", - "foo/baz": "2.3.4 3.4.5" + "foo/bar": "1.2.3 2.3.4", + "foo/baz": "2.3.4 3.4.5" }
-
version constraints separated by a single- (
|
) or double-pipe (||
) and any number of spaces before and after - treated as a logical or - are separated by a double pipe with a single space before and after (||
){ "homepage": "https://getcomposer.org/doc/articles/versions.md#version-range", "require": { - "php": "^8.1|^8.2", - "foo/bar": "^1.2.3 || ^2.3.4" + "php": "^8.1 || ^8.2", + "foo/bar": "^1.2.3 || ^2.3.4" }
-
hyphenated version ranges separated by dash (
-
) and any positive number of spaces before and after are separated by a dash with a single space before and after (-
){ "homepage": "https://getcomposer.org/doc/articles/versions.md#hyphenated-version-range-", "require": { - "foo/bar": "1.2.3 - 2.3.4" + "foo/bar": "1.2.3 - 2.3.4" }
-
duplicate constraints are removed
{ "homepage": "https://getcomposer.org/doc/articles/versions.md#version-range", "require": { - "foo/bar": "^1.0 || ^1.0 || ^2.0" + "foo/bar": "^1.0 || ^2.0" }
-
overlapping constraints are removed
{ "homepage": "https://getcomposer.org/doc/articles/versions.md#version-range", "require": { - "foo/bar": "^1.0 || ^1.1 || ^2.0 || ~2.1.0 || 2.4.5" + "foo/bar": "^1.0 || ^2.0" }
-
tilde version ranges (
~
) are preferred over wildcard (*
) version ranges{ "homepage": "https://getcomposer.org/doc/articles/versions.md#version-range", "require": { "foo/bar": "*", - "foo/baz": "1.0.*" + "foo/baz": "~1.0.0" }
-
caret version ranges (
^
) are preferred over tilde version ranges (~
){ "homepage": "https://getcomposer.org/doc/articles/versions.md#version-range", "require": { - "foo/bar": "~1", - "foo/baz": "~1.3" + "foo/bar": "^1.0", + "foo/baz": "^1.3" }
-
version numbers are sorted in ascending order
{ "homepage": "https://getcomposer.org/doc/articles/versions.md#version-range", "require": { - "foo/bar": "^2.0 || ^1.4" + "foo/bar": "^1.4 || ^2.0" }
-
extra spaces in inline aliases are removed
{ "homepage": "https://getcomposer.org/doc/articles/aliases.md#require-inline-alias", "require": { - "foo/bar": "dev-2.x as 2.0" + "foo/bar": "dev-2.x as 2.0" }
-
useless inline aliases are removed
{ "homepage": "https://getcomposer.org/doc/articles/aliases.md#require-inline-alias", "require": { - "foo/bar": "2.0 as 2.0" + "foo/bar": "2.0" }
-
leading
v
prefixes in version constraints are removed{ "require": { - "foo/bar": "^v1.2", - "foo/baz": "v1.3.7" + "foo/bar": "^1.2", + "foo/baz": "1.3.7" }
-
use of
x
orX
for wildcards is replaced with*
{ "require": { - "foo/bar": "1.x", - "foo/baz": "2.3.X", - "foo/qux": "x" + "foo/bar": "^1.0", + "foo/baz": "~2.3.0", + "foo/qux": "*" }
The maintainers of this project record notable changes to this project in a changelog.
The maintainers of this project suggest following the contribution guide.
The maintainers of this project ask contributors to follow the code of conduct.
The maintainers of this project provide limited support.
You can support the maintenance of this project by sponsoring @localheinz or requesting an invoice for services related to this project.
This project supports PHP versions with active and security support.
The maintainers of this project add support for a PHP version following its initial release and drop support for a PHP version when it has reached the end of security support.
This project has a security policy.
This project uses the MIT license.
The algorithm for sorting packages in the Vendor\Composer\PackageHashNormalizer
has been adopted from Composer\Json\JsonManipulator::sortPackages()
(originally licensed under MIT by Nils Adermann and Jordi Boggiano), which I initially contributed to composer/composer
with composer/composer#3549
and composer/composer#3872
.
Follow @localheinz and @ergebnis on Twitter.