ergebnis/json-normalizer

Provides generic and vendor-specific normalizers for normalizing JSON documents.

4.5.0 2024-01-30 09:10 UTC

README

Integrate Merge Release Renew Update

Code Coverage Type Coverage

Latest Stable Version Total Downloads Monthly Downloads

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:

💡 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.

💡 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.

💡 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);

💡 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:

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.

💡 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 ( ) or comma (,) - treated as a logical and - are separated by a space ( ) instead

     {
       "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 or X 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.