magicsunday / jsonmapper
Map JSON to PHP
Fund package maintenance!
paypal.me/magicsunday
Installs: 7 053
Dependents: 2
Suggesters: 0
Security: 0
Stars: 2
Watchers: 1
Forks: 0
Open Issues: 0
pkg:composer/magicsunday/jsonmapper
Requires
- php: >=8.3.0 <8.5.0
- ext-json: *
- doctrine/inflector: ^2.0
- symfony/property-access: ^7.3
- symfony/property-info: ^7.3
- symfony/type-info: ^7.3
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.65
- overtrue/phplint: ^9.0
- phpdocumentor/reflection-docblock: ^5.0
- phpstan/phpstan: ^2.0
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^11.0 || ^12.0
- rector/rector: ^2.0
Suggests
- phpdocumentor/reflection-docblock: In order to use the PhpDocExtractor this library is required too.
- dev-master
- 3.0.0
- 2.4.0
- 2.3.3
- 2.3.2
- 2.3.1
- 2.3.0
- 2.2.4
- 2.2.3
- 2.2.2
- 2.2
- 2.1.3
- 2.1.2
- 2.0
- 1.6
- 1.5
- 1.4
- 1.3
- 1.2
- 1.1
- 1.0
- dev-codex/refactor-jsonmapper-for-single-responsibility
- dev-codex/refactor-map-method-into-smaller-methods
- dev-codex/add-optional-factory-helper-for-bootstrapping
- dev-codex/verify-readme-examples-against-tests
- dev-codex/fuhre-composer-ci-test-aus-und-behebe-fehler
- dev-codex/check-example-tests-in-readme-and-docs
- dev-codex/run-tests-and-fix-errors
- dev-codex/add-and-expand-phpdoc-blocks-in-jsonmapper
- dev-codex/add-phpdoc-blocks-for-exception-classes
- dev-codex/add-phpdoc-to-mappingreport-and-mappingresult
- dev-codex/add-phpdoc-blocks-for-methods-in-typeresolver-and-classresol
- dev-codex/add-phpdoc-blocks-to-closuretypehandler-methods
- dev-codex/add-phpdoc-to-converters-and-interface
- dev-codex/add-phpdoc-block-for-constructor-in-replaceproperty
- dev-codex/verify-examples-in-readme-and-docs
- dev-codex/enhance-phpdoc-in-jsonmapper-files
- dev-codex/add-phpdoc-annotations-to-jsonmapper-classes
- dev-codex/update-phpdoc-for-jsonmapper-classes
- dev-codex/add-phpdoc-and-inline-comments-to-jsonmapper
- dev-php73
- dev-php56
- dev-generic-return-types
This package is auto-updated.
Last update: 2025-11-14 11:56:37 UTC
README
JsonMapper
This module provides a mapper to map JSON to PHP classes utilizing Symfony's property info and access packages.
Installation
Using Composer
To install using composer, just run the following command from the command line.
composer require magicsunday/jsonmapper
To remove the module run:
composer remove magicsunday/jsonmapper
Usage
Quick start
A minimal mapping run consists of two parts: a set of DTOs annotated with collection metadata and the mapper bootstrap code.
namespace App\Dto; use ArrayObject; final class Comment { public string $message; } /** * @extends ArrayObject<int, Comment> */ final class CommentCollection extends ArrayObject { } /** * @extends ArrayObject<int, Article> */ final class ArticleCollection extends ArrayObject { } final class Article { public string $title; /** * @var CommentCollection<int, Comment> */ public CommentCollection $comments; }
require __DIR__ . '/vendor/autoload.php'; use App\Dto\Article; use App\Dto\ArticleCollection; use MagicSunday\JsonMapper; // Decode a single article and a list of articles, raising on malformed JSON. $single = json_decode('{"title":"Hello world","comments":[{"message":"First!"}]}', associative: false, flags: JSON_THROW_ON_ERROR); $list = json_decode('[{"title":"Hello world","comments":[{"message":"First!"}]},{"title":"Second","comments":[]}]', associative: false, flags: JSON_THROW_ON_ERROR); // Bootstrap JsonMapper with reflection and PhpDoc extractors. $mapper = JsonMapper::createWithDefaults(); // Map a single DTO and an entire collection in one go. $article = $mapper->map($single, Article::class); $articles = $mapper->map($list, Article::class, ArticleCollection::class); // Dump the results to verify the hydrated structures. var_dump($article, $articles);
The first call produces an Article instance with a populated CommentCollection; the second call returns an ArticleCollection containing Article objects.
JsonMapper::createWithDefaults() wires the default Symfony PropertyInfoExtractor (reflection + PhpDoc) and a PropertyAccessor. When you need custom extractors, caching, or a specialised accessor you can still instantiate JsonMapper manually with your preferred services.
Test coverage: tests/JsonMapper/DocsQuickStartTest.php.
PHP classes
In order to guarantee a seamless mapping of a JSON response into PHP classes you should prepare your classes well. Annotate all properties with the requested type.
In order to ensure correct mapping of a collection, the property has to be annotated using the phpDocumentor collection annotation type. A collection is a non-scalar value capable of containing other values.
For example:
/** @var SomeCollection<DateTime> $dates */ /** @var SomeCollection<string> $labels */ /** @var Collection\\SomeCollection<App\\Entity\\SomeEntity> $entities */
Custom attributes
Sometimes its may be required to circumvent the limitations of a poorly designed API. Together with custom attributes it becomes possible to fix some API design issues (e.g. mismatch between documentation and webservice response), to create a clean SDK.
#[MagicSunday\JsonMapper\Attribute\ReplaceNullWithDefaultValue]
This attribute is used to inform the JsonMapper that an existing default value should be used when setting a property, if the value derived from the JSON is a NULL value instead of the expected property type.
This can be necessary, for example, in the case of a bad API design, if the API documentation defines a certain type (e.g. array), but the API call itself then returns NULL if no data is available for a property instead of an empty array that can be expected.
namespace App\Dto; use MagicSunday\JsonMapper\Attribute\ReplaceNullWithDefaultValue; final class AttributeExample { /** * @var array<string> */ #[ReplaceNullWithDefaultValue] public array $roles = []; }
If the mapping tries to assign NULL to the property, the default value will be used, as annotated.
#[MagicSunday\JsonMapper\Attribute\ReplaceProperty]
This attribute is used to inform the JsonMapper to replace one or more properties with another one. It's used in class context.
For instance if you want to replace a cryptic named property to a more human-readable name.
namespace App\Dto; use MagicSunday\JsonMapper\Attribute\ReplaceProperty; #[ReplaceProperty('type', replaces: 'crypticTypeNameProperty')] final class FooClass { public string $type; }
Instantiation
In order to create an instance of the JsonMapper you are required to pass some arguments to the constructor. The
constructor requires an instance of \Symfony\Component\PropertyInfo\PropertyInfoExtractor and an instance of
\Symfony\Component\PropertyAccess\PropertyAccessor. The other arguments are optional.
So first create instances of Symfony's property info extractors. Each list of extractors could contain any number of available extractors. You could also create your own extractors to adjust the process of extracting property info to your needs.
To use the PhpDocExtractor extractor you need to install the phpdocumentor/reflection-docblock library too.
require __DIR__ . '/vendor/autoload.php'; use MagicSunday\JsonMapper\Converter\CamelCasePropertyNameConverter; use MagicSunday\JsonMapper; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; final class SdkFoo { } final class Foo { } // Gather Symfony extractors that describe available DTO properties. $propertyInfo = new PropertyInfoExtractor( listExtractors: [new ReflectionExtractor()], typeExtractors: [new PhpDocExtractor()], ); // Build a property accessor so JsonMapper can read and write DTO values. $propertyAccessor = PropertyAccess::createPropertyAccessor(); // Convert snake_case JSON keys into camelCase DTO properties. $nameConverter = new CamelCasePropertyNameConverter(); // Provide explicit class-map overrides when API classes differ from DTOs. $classMap = [ SdkFoo::class => Foo::class, ]; // Finally create the mapper with the configured dependencies. $mapper = new JsonMapper( $propertyInfo, $propertyAccessor, $nameConverter, $classMap, );
To handle custom or special types of objects, add them to the mapper. For instance to perform special treatment if an object of type Bar should be mapped:
You may alternatively implement \MagicSunday\JsonMapper\Value\TypeHandlerInterface to package reusable handlers.
require __DIR__ . '/vendor/autoload.php'; use DateTimeImmutable; use MagicSunday\JsonMapper; use MagicSunday\JsonMapper\Value\ClosureTypeHandler; use stdClass; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; final class Bar { public function __construct(public string $name) { } } final class Wrapper { public Bar $bar; public DateTimeImmutable $createdAt; } // Describe DTO properties through Symfony extractors. $propertyInfo = new PropertyInfoExtractor( listExtractors: [new ReflectionExtractor()], typeExtractors: [new PhpDocExtractor()], ); $propertyAccessor = PropertyAccess::createPropertyAccessor(); $mapper = new JsonMapper($propertyInfo, $propertyAccessor); // Register a handler that hydrates Bar value objects from nested stdClass payloads. $mapper->addTypeHandler( new ClosureTypeHandler( Bar::class, static function (stdClass $value): Bar { // Convert the decoded JSON object into a strongly typed Bar instance. return new Bar($value->name); }, ), ); // Register a handler for DateTimeImmutable conversion using ISO-8601 timestamps. $mapper->addTypeHandler( new ClosureTypeHandler( DateTimeImmutable::class, static function (string $value): DateTimeImmutable { return new DateTimeImmutable($value); }, ), ); // Decode the JSON payload while throwing on malformed input. $payload = json_decode('{"bar":{"name":"custom"},"createdAt":"2024-01-01T10:00:00+00:00"}', associative: false, flags: JSON_THROW_ON_ERROR); // Map the payload into the Wrapper DTO. $result = $mapper->map($payload, Wrapper::class); var_dump($result);
Convert a JSON string into a JSON array/object using PHPs built in method json_decode
// Decode the JSON document while propagating parser errors. $json = json_decode('{"title":"Sample"}', associative: false, flags: JSON_THROW_ON_ERROR); // Inspect the decoded representation. var_dump($json);
Call method map to do the actual mapping of the JSON object/array into PHP classes. Pass the initial class name
and optional the name of a collection class to the method.
require __DIR__ . '/vendor/autoload.php'; use ArrayObject; use MagicSunday\JsonMapper; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; final class FooCollection extends ArrayObject { } final class Foo { public string $name; } // Decode a JSON array into objects and throw on malformed payloads. $json = json_decode('[{"name":"alpha"},{"name":"beta"}]', associative: false, flags: JSON_THROW_ON_ERROR); // Configure JsonMapper with reflection and PHPDoc metadata. $propertyInfo = new PropertyInfoExtractor( listExtractors: [new ReflectionExtractor()], typeExtractors: [new PhpDocExtractor()], ); $propertyAccessor = PropertyAccess::createPropertyAccessor(); $mapper = new JsonMapper($propertyInfo, $propertyAccessor); // Map the collection into Foo instances stored inside FooCollection. $mappedResult = $mapper->map($json, Foo::class, FooCollection::class); var_dump($mappedResult);
A complete set-up may look like this:
require __DIR__ . '/vendor/autoload.php'; use MagicSunday\JsonMapper\Converter\CamelCasePropertyNameConverter; use MagicSunday\JsonMapper; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; /** * Bootstrap a JsonMapper instance with Symfony extractors and optional class-map overrides. * * @param array<class-string, class-string> $classMap Override source classes with DTO replacements. */ function createJsonMapper(array $classMap = []): JsonMapper { // Cache property metadata to avoid repeated reflection work. $propertyInfo = new PropertyInfoExtractor( listExtractors: [new ReflectionExtractor()], typeExtractors: [new PhpDocExtractor()], ); // Return a mapper configured with a camelCase converter and optional overrides. return new JsonMapper( $propertyInfo, PropertyAccess::createPropertyAccessor(), new CamelCasePropertyNameConverter(), $classMap, ); } $mapper = createJsonMapper();
Type converters and custom class maps
Custom types should implement MagicSunday\\JsonMapper\\Value\\TypeHandlerInterface and can be registered once via JsonMapper::addTypeHandler(). For lightweight overrides you may still use addType() with a closure, but new code should prefer dedicated handler classes.
Use JsonMapper::addCustomClassMapEntry() when the target class depends on runtime data. The resolver receives the decoded JSON payload and may inspect a MappingContext when you need additional state.
require __DIR__ . '/vendor/autoload.php'; use MagicSunday\JsonMapper; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; final class SdkFoo { } final class FooBar { } final class FooBaz { } // Build the dependencies shared by all mapping runs. $propertyInfo = new PropertyInfoExtractor( listExtractors: [new ReflectionExtractor()], typeExtractors: [new PhpDocExtractor()], ); $propertyAccessor = PropertyAccess::createPropertyAccessor(); $mapper = new JsonMapper($propertyInfo, $propertyAccessor); // Route SDK payloads to specific DTOs based on runtime discriminator data. $mapper->addCustomClassMapEntry(SdkFoo::class, static function (array $payload): string { // Decide which DTO to instantiate by inspecting the payload type. return $payload['type'] === 'bar' ? FooBar::class : FooBaz::class; });
Error handling strategies
The mapper operates in a lenient mode by default. Switch to strict mapping when every property must be validated:
require __DIR__ . '/vendor/autoload.php'; use MagicSunday\JsonMapper\Configuration\JsonMapperConfiguration; use MagicSunday\JsonMapper; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; final class Article { public string $title; } // Decode the JSON payload that should comply with the DTO schema. $payload = json_decode('{"title":"Strict example"}', associative: false, flags: JSON_THROW_ON_ERROR); // Prepare the mapper with Symfony metadata extractors. $propertyInfo = new PropertyInfoExtractor( listExtractors: [new ReflectionExtractor()], typeExtractors: [new PhpDocExtractor()], ); $propertyAccessor = PropertyAccess::createPropertyAccessor(); $mapper = new JsonMapper($propertyInfo, $propertyAccessor); // Enable strict validation and collect every encountered error. $config = JsonMapperConfiguration::strict()->withCollectErrors(true); // Map while receiving a result object that contains the mapped DTO and issues. $result = $mapper->mapWithReport($payload, Article::class, configuration: $config); var_dump($result->getMappedValue());
For tolerant APIs combine JsonMapperConfiguration::lenient() with ->withIgnoreUnknownProperties(true) or ->withTreatNullAsEmptyCollection(true) to absorb schema drifts.
Performance hints
Type resolution is the most expensive part of a mapping run. Provide a PSR-6 cache pool to the constructor to reuse computed Type metadata:
require __DIR__ . '/vendor/autoload.php'; use MagicSunday\JsonMapper; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; // Assemble the reflection and PHPDoc extractors once. $propertyInfo = new PropertyInfoExtractor( listExtractors: [new ReflectionExtractor()], typeExtractors: [new PhpDocExtractor()], ); $propertyAccessor = PropertyAccess::createPropertyAccessor(); // Cache resolved Type metadata between mapping runs. $cache = new ArrayAdapter(); $mapper = new JsonMapper($propertyInfo, $propertyAccessor, nameConverter: null, classMap: [], typeCache: $cache);
Reuse a single JsonMapper instance across requests to share the cached metadata and registered handlers.
Additional documentation
- API reference
- Recipes
Development
Testing
composer update composer ci:cgl composer ci:test composer ci:test:php:phplint composer ci:test:php:phpstan composer ci:test:php:rector composer ci:test:php:cpd composer ci:test:php:unit