tcds-io / php-jackson
A lightweight, flexible object serializer for PHP, inspired by FasterXML/jackson
Requires
- php: >=8.4
- ext-json: *
- tcds-io/php-better-generics: ^1.0
Requires (Dev)
- nesbot/carbon: ^3.10
- php-cs-fixer/shim: ^3.88
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^12.3
- symfony/var-dumper: ^6.0
This package is auto-updated.
Last update: 2026-05-06 09:01:40 UTC
README
A lightweight, flexible object serializer for PHP, inspired by Jackson.
It provides strong typing, JSON ↔ object mapping, generics support, array/object shapes, custom type mappers, and detailed error tracing.
📚 Contents
- Overview
- Integrations
- Installation
- Basic Usage
- Deserializing from JSON
- Merging Additional Data (
readValueWith) - Serializing Objects
- Generic Types (
list<t> map<k, v> shapes) - Renaming JSON keys with
#[JsonProperty] - Custom Type Mappers
- Date Handling
- Error Handling
- Development
- Summary
Overview
Main components:
- JsonObjectMapper — handles JSON strings at the boundary.
- ArrayObjectMapper — handles associative arrays at the boundary.
- Type Mappers — custom readers/writers for specific classes.
- Generic Types — support for
list<T>,map<K,V>, shapes, etc. - Date Handling — built-in support for DateTime and Carbon.
- Error Reporting — typed exceptions with full trace paths.
🚀 Installation
composer require tcds-io/php-jackson
🧩 Integrations
PHP Jackson offers first-class integrations for popular PHP frameworks and tools. Each integration extends the core mapper with framework-specific features for a smoother development experience.
Official Plugins:
- Laravel ↗ — controller injection, JSON responses, request error handling, and Eloquent casts
- Symfony ↗ — controller argument resolvers, JSON responses, and configurable request error handling
- Guzzle ↗ — typed HTTP client with request DTO mapping and async response parsing
🔧 Basic Usage
Reading JSON into typed objects
use Tcds\Io\Jackson\JsonObjectMapper; $mapper = new JsonObjectMapper(); $address = $mapper->readValue(Address::class, $json);
Equivalent array version:
use Tcds\Io\Jackson\ArrayObjectMapper; $mapper = new ArrayObjectMapper(); $address = $mapper->readValue(Address::class, $dataArray);
📥 Deserializing from JSON
$json = <<<JSON { "street": "Ocean avenue", "number": "100", "main": "true", "place": { "city": "Rio de Janeiro", "country": "Brazil", "position": { "lat": "-26.9013", "lng": "-48.6655" } } } JSON; $mapper = new JsonObjectMapper(); $address = $mapper->readValue(Address::class, $json);
The resulting object matches:
new Address( street: 'Ocean avenue', number: 100, main: true, place: new Place( city: 'Rio de Janeiro', country: 'Brazil', position: new LatLng(lat: -26.9013, lng: -48.6655), ), );
➕ Merging Additional Data (readValueWith)
Merging data is useful when the incoming payload does not contain all required values and those values must be completed from another source:
$partial = <<<JSON { "street": "Ocean avenue", "number": "100", "main": "true" } JSON; $address = $mapper->readValueWith( Address::class, $partial, [ 'place' => [ 'city' => "Rio de Janeiro", 'country' => "Brazil", 'position' => [ 'lat' => -26.9013, 'lng' => -48.6655, ] ] ] );
📤 Serializing Objects
Array output:
$mapper = new ArrayObjectMapper(); $array = $mapper->writeValue($object);
JSON output:
$mapper = new JsonObjectMapper(); $json = $mapper->writeValue($object);
📚 Generic Types (list<T>, map<K,V>, shapes)
The generic() and shape() helper functions are loaded by Composer through php-better-generics.
List example
$list = $mapper->readValue('list<LatLng>', $json);
Using generic():
$type = generic('list', [LatLng::class]); $list = $mapper->readValue($type, $json);
Map example
$type = generic('map', ['string', Address::class]); $result = $mapper->readValue($type, [ 'main' => Address::mainData(), 'other' => Address::otherData(), ]);
Array Shape Example
$type = shape('array', [ 'type' => AccountType::class, 'position' => LatLng::class, ]);
Produces:
[ 'type' => AccountType::CHECKING, 'position' => new LatLng(...), ]
Object Shape Example
$type = shape('object', [ 'type' => AccountType::class, 'position' => LatLng::class ]);
Produces a stdClass:
$object->type === AccountType::CHECKING $object->position instanceof LatLng
🏷️ Renaming JSON keys with #[JsonProperty]
PHP-Jackson maps JSON keys to PHP names 1:1 by default. When the wire format
uses a different naming convention (snake_case, kebab-case, etc.), pin the
JSON key on the constructor parameter (or property) with #[JsonProperty]:
use Tcds\Io\Jackson\Node\JsonProperty; readonly class User { public function __construct( #[JsonProperty('first_name')] public string $firstName, #[JsonProperty('last_name')] public string $lastName, public int $age, ) {} }
The attribute is honored on both directions:
$mapper = new JsonObjectMapper(); $user = $mapper->readValue(User::class, '{"first_name":"Arthur","last_name":"Dent","age":42}'); // User { firstName: "Arthur", lastName: "Dent", age: 42 } $mapper->writeValue($user); // {"first_name":"Arthur","last_name":"Dent","age":42}
Error traces and the expected payload on UnableToParseValue use the wire
key — the one users will recognize from the JSON they are sending — not the
PHP identifier.
🧩 Custom Type Mappers
Custom mappers are useful when object construction depends on complex logic or external data:
use Tcds\Io\Jackson\ArrayObjectMapper; $mapper = new ArrayObjectMapper( typeMappers: [ LatLng::class => [ 'reader' => fn(string $data) => new LatLng(...explode(',', $data)), 'writer' => fn(LatLng $data) => sprintf("%s, %s", $data->lat, $data->lng), ] ] );
This allows:
"position" => "-26.9013, -48.6655"
to become:
new LatLng(-26.9013, -48.6655)
and serialize back into:
"position" => "-26.9013, -48.6655"
Using Custom Mappers with External Context
use Tcds\Io\Jackson\ArrayObjectMapper; $mapper = new ArrayObjectMapper( typeMappers: [ User::class => [ 'reader' => fn() => Auth::user(), 'writer' => fn(User $data) => [ 'id' => $data->id, 'name' => $data->name, // 'email' intentionally omitted ], ] ] );
Mapper closures can receive any of the named arguments used internally by PHP-Jackson:
fn(mixed $data, string $type, ObjectMapper $mapper, array $path): mixed
Use only the parameters you need; ReflectionFunction::call() binds them by name.
Pinning a Mapper on the Class with #[JsonMapper]
If a class always wants the same custom (de)serialization, declare it once on the class itself instead of registering it on every mapper instance:
use Tcds\Io\Jackson\Node\JsonMapper; #[JsonMapper(reader: MoneyReader::class, writer: MoneyWriter::class)] readonly class Money { public function __construct(public int $cents) {} }
The reader and writer accept any of:
- a class string of an implementation of
Reader/Writer(instance is built with a no-arg constructor), - a class string of
StaticReader/StaticWriter(no instance — the staticread/writeis called), - a class string of any class with a matching
__invoke(treated as aMapperClosure), - an instance of
Reader/Writer(PHP 8.1newin attribute initializers), - a
ClosurematchingMapperClosure, when constructingJsonMapperprogrammatically (PHP attribute literals can't carry closures).
Resolution order on every read/write:
#[JsonMapper]attribute on the target class — declaration site, winstypeMappersconstructor argument- default reader/writer
That is, an explicit class-level mapper cannot be silently overridden by mapper-instance config — the class itself is the canonical source.
🕒 Date Handling
PHP-Jackson provides built-in support for:
- DateTime
- DateTimeImmutable
- Carbon
- CarbonImmutable
- DateTimeInterface
Dates are serialized and deserialized using ISO-8601 strings:
[ 'datetime' => '2025-10-22T11:21:31+00:00' ]
❗ Error Handling
When parsing fails, the library throws:
UnableToParseValue
Properties:
$e->trace; // ['address','place','position'] $e->expected; // expected type description $e->given; // actual given value
Example message:
Unable to parse value at .address.place.position
This makes debugging extremely easy.
🔧 Development
composer install composer tests # runs cs:check + phpstan + phpunit composer cs:fix # auto-fix code style
✅ Summary
You can:
- Read JSON → typed objects via
JsonObjectMapper - Read arrays → typed objects via
ArrayObjectMapper - Merge missing fields using
readValueWith - Write objects → JSON/arrays via
writeValue - Use generics (
list<T>,map<K,V>, shapes) - Rename wire keys per field with
#[JsonProperty('snake_case')] - Register custom mappers for any class via
typeMappersor pin them on the class itself with#[JsonMapper(reader: …, writer: …)] - Rely on strong error tracing with full path information