karriere/json-decoder
JsonDecoder maps JSON into typed PHP objects, handling nested objects, arrays, scalar casting, required fields, and custom decoders. It turns API responses and config files into predictable, IDE-friendly classes for safer, cleaner PHP code.
5.0.x release or upgrade PHP before adopting.composer require karriere/json-decoder. Check what you actually resolved with composer show karriere/json-decoder and treat the PHP constraint as part of your upgrade planning.Karriere\JsonDecoder\JsonDecoder, ClassBinding, the Bindings\* classes, and the transformer contract/classes.use Karriere\JsonDecoder\ClassBinding;
use Karriere\JsonDecoder\JsonDecoder;
class UserDto
{
public string $id;
public string $email;
public ?string $name = null;
}
$decoder = new JsonDecoder();
$decoder->register(UserDto::class, function (ClassBinding $binding) {
$binding->bindField('id');
$binding->bindField('email');
$binding->bindField('name');
});
$user = $decoder->decode($json, UserDto::class);
First real use case: put decoding at the boundary of your application. For example, decode a third-party API response or webhook payload into a typed DTO inside an adapter/service, then pass that DTO into your domain code instead of passing raw arrays around.
One DTO per payload boundary. Create a DTO for each external payload you care about: API responses, webhook bodies, queue messages, import rows. Keep the DTO close to the payload shape and keep domain entities separate.
Register bindings centrally. In Laravel, configure a singleton decoder in a service provider so bindings are not rebuilt per request.
use Karriere\JsonDecoder\ClassBinding;
use Karriere\JsonDecoder\JsonDecoder;
$this->app->singleton(JsonDecoder::class, function () {
$decoder = new JsonDecoder();
$decoder->register(OrderDto::class, function (ClassBinding $binding) {
$binding->bindField('id');
$binding->bindField('created_at', 'createdAt', new DateTimeTransformer());
$binding->bindClass('customer', 'customer', CustomerDto::class);
$binding->bindArray('items', 'items', OrderItemDto::class);
});
return $decoder;
});
Use transformers for non-trivial values. Dates, enums, money objects, booleans encoded as 0/1, and IDs that need casting should be handled in transformers rather than in controllers.
class DateTimeTransformer implements Transformer
{
public function transform($value)
{
return new DateTimeImmutable($value);
}
}
Be explicit about nested objects and collections. Do not expect the decoder to infer nested DTO types from JSON shape. Register bindClass/bindArray-style bindings for nested objects and arrays of objects. If a nested payload is polymorphic, decode to a base DTO first, inspect a discriminator field, then decode again to the concrete DTO.
Validate before decoding. The decoder maps structure; it does not validate business rules. Validate required keys, types, and formats with Laravel validation or a small boundary validator before calling decode(). Treat a decode failure as an integration failure, not as a user validation error.
Keep decoding out of controllers. Put it in a service, action, or adapter:
class StripeCustomerAdapter
{
public function __construct(private JsonDecoder $decoder) {}
public function fromApiResponse(string $body): StripeCustomerDto
{
return $this->decoder->decode($body, StripeCustomerDto::class);
}
}
Test with fixtures. Store representative JSON fixtures and assert that the decoded DTO has the expected values. Test transformers independently and add contract tests for third-party APIs so schema changes fail loudly.
Laravel integration tips. With Http::get(...), pass the raw body ($response->body()) to the decoder if the decoder expects a JSON string. For queued jobs, prefer keeping the raw payload in the job and decoding when the job handles it, unless your DTO is safely serializable.
Keep the PHP matrix aligned. Since v5.1.0 dropped 8.0/8.1, make sure your composer.json require.php, your CI matrix, your Docker base images, and any Laravel versions still supporting EOL PHP move together. Running composer why-not karriere/json-decoder 5.1 is the fastest way to see which constraint is blocking the update.
composer update fails) rather than at decode time. Budget for it as an infrastructure upgrade, not a code upgrade.NOASSERTION, so do a legal/license check before shipping in a commercial product.snake_case versus PHP camelCase is handled by the binding's property argument, e.g. bindField('created_at', 'createdAt'). Do not rename DTO properties just to match the wire format.?string $name = null) or validate required fields before decoding. Extra JSON fields are usually ignored, which can hide upstream schema changes."123" will not become an int unless the binding/transformer handles it. Use transformers for dates, enums, money, and boolean flags.JsonDecoder and re-registering bindings for every payload is unnecessary and makes debugging harder.json_decode($json, true) or log the raw payload to confirm the shape, then add bindings one field at a time. Log the raw body and endpoint on decode failures.json_decode() calls through the codebase.How can I help you explore Laravel packages today?