Map JSON data to strongly-typed PHP classes using Symfony's PropertyInfo and PropertyAccess components.
JsonMapper is a PHP library that maps JSON data to strongly-typed PHP classes (DTOs, value objects, entities) using reflection and PHPDoc annotations. It leverages Symfony's PropertyInfo and PropertyAccess components to provide flexible, extensible JSON-to-PHP object mapping.
| Key | Value |
|---|---|
| Package | magicsunday/jsonmapper |
| PHP | ^8.3 |
| Main API | MagicSunday\JsonMapper |
| Output | Mapped PHP objects + optional MappingReport |
JsonMapper takes decoded JSON (via json_decode) and hydrates typed PHP objects, including nested objects, collections, enums, DateTime values, and custom types. It supports both lenient and strict mapping modes with detailed error reporting.
Mapping API responses or configuration payloads to typed PHP classes is a common task that involves repetitive boilerplate. JsonMapper automates this with a clean, extensible architecture based on Symfony components, supporting advanced scenarios like polymorphic APIs, custom name conversion, and recursive collection handling.
composer require magicsunday/jsonmappernamespace 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;
$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);
$mapper = JsonMapper::createWithDefaults();
$article = $mapper->map($single, Article::class);
$articles = $mapper->map($list, Article::class, ArticleCollection::class);JsonMapper::createWithDefaults() wires the default Symfony PropertyInfoExtractor (reflection + PhpDoc) and a PropertyAccessor. For custom extractors, caching, or a specialised accessor see Manual instantiation.
Annotate all properties with the requested type. For collections, use the phpDocumentor collection annotation type:
/** @var SomeCollection<DateTime> $dates */
/** @var SomeCollection<string> $labels */
/** @var Collection\\SomeCollection<App\\Entity\\SomeEntity> $entities */Classes whose constructor declares promoted or otherwise required parameters are hydrated
through their constructor β the mapped values are passed as constructor arguments β instead of
being assigned after an argument-less instantiation. This makes final readonly value objects
first-class mapping targets:
final readonly class Money
{
public function __construct(
public int $amount,
public string $currency,
) {
}
}
$money = $mapper->map(['amount' => 500, 'currency' => 'EUR'], Money::class);Every payload value is converted exactly once, through the same pipeline as a property (nested
objects, collections, enums, DateTime, custom types, name conversion, ReplaceProperty and
ReplaceNullWithDefaultValue all apply); the constructor parameters are then filled from those
converted values and any remaining property is assigned afterwards, so a class that mixes promoted
constructor parameters with additional settable properties loses nothing. A missing argument falls
back to its constructor default, then to null for a nullable parameter; a missing required,
non-nullable argument raises a MissingConstructorArgumentException. Plain mutable classes keep the
existing property-assignment behaviour unchanged.
- API reference
- Recipes
- Manual instantiation β custom extractors, name converters, class maps, collection mapping
- Type converters and custom class maps β custom type handlers, runtime class resolution
- Error handling strategies β strict vs. lenient mode, error collection
- Performance hints β PSR-6 type caching
- Using mapper attributes β ReplaceProperty, ReplaceNullWithDefaultValue, UnknownPropertyCollector
- Mapping JSON to PHP enums
- Mapping nested collections
- Using a custom name converter
Prerequisites:
- PHP
^8.3 - Extensions:
json - Node.js (for the copy-paste detection gate, run via
npx)
Install dependencies:
composer installRun the mandatory quality gate:
composer ci:testci:test includes:
- Linting (
phplint) - Unit tests (
phpunit) - Static analysis (
phpstan) - Refactoring dry-run (
rector --dry-run) - Coding standards dry-run (
php-cs-fixer --dry-run) - Copy/paste detection (
jscpd)
See CONTRIBUTING.md for contributor workflow and minimal setup.
If contributions are prepared or modified by an LLM/agent, follow AGENTS.md.