phpjsonconverterstyped

JSON to PHP: Generating Typed Classes with Constructor Promotion

·7 min read·Converters

The default is an untyped array, and that's the problem

json_decode($json, true) is how most PHP code reads a JSON response, and it's genuinely fine for a quick script — but it hands back a plain associative array with no type information at all. A typo in a key name ($data['emial'] instead of $data['email']) returns null silently instead of failing, and nothing — not your IDE, not PHPStan, not Psalm — can catch it before it ships, because as far as static analysis is concerned, an associative array's keys are just runtime strings.

php
$data = json_decode($json, true);
echo $data['emial']; // typo — silently null, no warning, no error, until it's in production

Constructor property promotion makes the fix cheap

Before PHP 8.0, hydrating JSON into a typed object meant writing a constructor, a full set of property declarations, and an assignment for each one — enough boilerplate that plenty of teams reasonably decided it wasn't worth it for every API response shape. Constructor property promotion collapses all of that into the parameter list itself:

php
final class User
{
    public function __construct(
        public string $name,
        public string $email,
        public int $age,
        public bool $active,
    ) {}

    public static function fromArray(array $data): self
    {
        return new self(
            name: $data['name'],
            email: $data['email'],   // typo here is now a fatal error at construction, not a silent null
            age: $data['age'],
            active: $data['active'],
        );
    }
}

$user = User::fromArray(json_decode($json, true));
echo $user->email; // IDE autocomplete works, PHPStan/Psalm both understand this fully

The same key-name typo above now fails loudly and immediately — either as an Undefined array key warning at the fromArray call, or, if the property is non-nullable and the key is genuinely missing, a TypeError. Either way, you find out at the point of hydration, not three function calls deeper when something unrelated breaks.

Nested objects and arrays

A real API response is rarely flat — nested objects become their own typed classes, and arrays of objects hydrate with array_map:

php
final class Address
{
    public function __construct(
        public string $city,
        public string $country,
    ) {}

    public static function fromArray(array $data): self
    {
        return new self(city: $data['city'], country: $data['country']);
    }
}

final class Order
{
    public function __construct(
        public string $id,
        /** @var OrderItem[] */
        public array $items,
        public Address $shippingAddress,
    ) {}

    public static function fromArray(array $data): self
    {
        return new self(
            id: $data['id'],
            items: array_map(fn(array $i) => OrderItem::fromArray($i), $data['items']),
            shippingAddress: Address::fromArray($data['shippingAddress']),
        );
    }
}

The @var OrderItem[] docblock above the items property matters more than it looks — PHP's native type system has no generic array<T> syntax, so without the docblock, static analysis tools only know items is *an* array, not an array of OrderItem. PHPStan and Psalm both read that docblock convention and will correctly flag $order->items[0]->badProperty as an error once it's there.

Generating this by hand doesn't scale past a couple of endpoints

Writing this pattern out for one or two response shapes is a few minutes of work; doing it for a 40-endpoint API by hand is not a good use of anyone's afternoon. JSONKit's JSON to PHP generator takes a real JSON sample and produces exactly this shape — typed constructor-promoted classes, nested classes for nested objects, and a fromArray() hydrator for each — directly in your browser, so you're editing and adjusting generated code rather than typing every property declaration from scratch.

Frequently asked questions

Not entirely — the generated classes give you types and IDE support, but fromArray() doesn't validate that a field is the *correct* type at runtime beyond what PHP's own type coercion does; it assigns $data['key'] ?? null and lets PHP's type system catch outright mismatches. For runtime validation with proper error messages, layer a library like cuyz/valinor or Symfony's Validator on top of the same class shape.

PHP 8.0 or later for constructor property promotion. Readonly properties (an even better fit for API response DTOs, since a hydrated response object generally shouldn't be mutated) need PHP 8.1's public readonly string $name syntax specifically.

json_decode($json) without the second argument returns stdClass objects instead of arrays, which does give you -> property access instead of ['key'] array access — but it's still fully untyped from a static analysis perspective, with the same silent-typo problem, just spelled with arrows instead of brackets.

Yes — they're plain PHP objects (POPOs) with no framework dependency, so they work as-is for DTOs in either framework. In Laravel you'd typically use them in or alongside an API Resource's toArray(); in Symfony, as the target of a custom Serializer normalizer.

Try JSONKit — JSON Developer Tools

Format, validate, convert and diff JSON. Instant, accurate and 100% private.