Awesome
json-normalizer
This project provides a composer
package with generic and vendor-specific normalizers for normalizing JSON documents.
Installation
Run
composer require ergebnis/json-normalizer
Usage
This project comes with
Generic normalizers
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
:bulb: All of these normalizers implement the Ergebnis\Json\Normalizer\Normalizer
.
CallableNormalizer
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.
ChainNormalizer
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.
:bulb: Be careful with the order of the normalizers, as one normalizer might override changes a previous normalizer applied.
FormatNormalizer
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
.
IndentNormalizer
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.
JsonEncodeNormalizer
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
.
:bulb: For reference, see json_encode()
and the corresponding JSON constants.
SchemaNormalizer
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);
:bulb: For more information about JSON schema, visit json-schema.org.
WithFinalNewLineNormalizer
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.
WithoutFinalNewLineNormalizer
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.
Vendor-specific normalizers
This project comes with the following vendor-specific normalizers:
Vendor\Composer\ComposerJsonNormalizer
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
Vendor\Composer\BinNormalizer
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.
Vendor\Composer\ConfigHashNormalizer
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.
Vendor\Composer\PackageHashNormalizer
When composer.json
contains any configuration in the
sections, the Vendor\Composer\PackageHashNormalizer
will sort the packages in these sections.
:bulb: This transfers the behaviour from using the --sort-packages
or sort-packages
configuration flag in require
and require-dev
to other sections.
Vendor\Composer\RepositoriesHashNormalizer
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.
Vendor\Composer\VersionConstraintNormalizer
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": "*" }
Changelog
The maintainers of this project record notable changes to this project in a changelog.
Contributing
The maintainers of this project suggest following the contribution guide.
Code of Conduct
The maintainers of this project ask contributors to follow the code of conduct.
General Support Policy
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.
PHP Version Support Policy
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.
Security Policy
This project has a security policy.
License
This project uses the MIT license.
Credits
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
.
Social
Follow @localheinz and @ergebnis on Twitter.