Skip to content
Back to Blog
By JSONConvert Team··7 min read

JSON to PHP: Generate Typed Classes Fast

PHP 8 finally gave you the tools to model JSON properly. Typed properties, constructor promotion, readonly, enums, and union types let you write data classes that look almost like records from any other modern language. The catch is that json_decode does not hydrate your classes for you. You still write the boilerplate, or you generate it once and forget about it. Here is how the mapping works, where PHP trips you up, and how to keep the resulting code tight.

JSON to PHP Type Mapping

JSON TypePHP TypeNotes
"string"stringUTF-8, no length limit at the language level
123int64-bit on 64-bit platforms, 32-bit on 32-bit builds
1.5floatIEEE 754 double, same precision pitfalls as everywhere else
truebool
nullnullable (?type)Required for any field that can be null or missing
[1, 2, 3]arrayNo generics, document the element type in PHPDoc
{}custom classNested DTO
"2026-01-15T10:30Z"DateTimeImmutableParse explicitly, do not store as a string
"550e8400-..."string or UUID libraryPHP has no built-in UUID type
PHP's int is platform-dependent. On a 64-bit system it covers the full ±9.2 × 10^18 range, which is enough for Twitter-style snowflake IDs. On a 32-bit build it tops out near 2.1 billion and silently promotes to float past that, losing precision. If your code may run on shared hosting or older Docker images, treat large external IDs as string to be safe.

A Basic Typed Class

Given this API response:

The PHP 8.2 class:

Three things to notice. The class is readonly, meaning every property is immutable after construction, which is exactly what you want for a value object built from an API response. Constructor property promotion declares and assigns properties in one place, so there is no separate property list above the constructor. The class is final, blocking inheritance because data classes have no business being subclassed.

Hydrating From JSON

json_decode returns either a stdClass object or an associative array, not your class:

Named arguments make this readable even with ten fields. The explicit (float) cast covers the case where the API sends 98 instead of 98.5. Without the cast, json_decode returns an int and the typed constructor throws a TypeError. PHP's type coercion is strict for constructor properties unless you opt out per file with declare(strict_types=0), which you should not.

Pass JSON_THROW_ON_ERROR so a malformed payload throws JsonException instead of silently returning null:

A static factory keeps the hydration logic with the class:

Nullable Fields

Any field that can be null or absent needs a nullable type and a default:

The ? prefix allows null. The = null default covers keys that are missing entirely, which is a different case than a key present with value null. Without the default, an absent key in the payload throws ArgumentCountError. With the default and the null-coalescing operator in the factory, both cases are handled:

Notice the snake_case to camelCase rename. PHP has no automatic key mapping like Jackson or Codable, so you do it in the factory.

Nested Classes

Nested objects become nested DTOs:

The @var string[] PHPDoc on $tags is not a runtime check, but PHPStan, Psalm, and PhpStorm read it for static analysis. Without it, the array type tells you nothing about what is inside.

Arrays of Objects

PHP has no generic types, so an array of Post objects is still just array. Map over the raw data in the factory:

array_map with the arrow function syntax is the cleanest way to convert a list of associative arrays into a list of DTOs. The PHPDoc above the assignment is what lets your IDE autocomplete $posts[0]->author->name.

Dates

JSON has no date type, so APIs ship dates as strings or integers. Parse them with DateTimeImmutable so the result is read-only:

DateTimeImmutable accepts ISO 8601 strings directly. For Unix timestamps, use (new \DateTimeImmutable())->setTimestamp($data['created_at']). Never store the raw string and parse it later, because you will eventually pass it somewhere that expects a real datetime and watch it stringify into the wrong timezone.

Generating the Class

For an unfamiliar API response, paste the JSON into the JSON to PHP converter to get a starter class with constructor promotion, nullable types, and nested classes. It handles the boring parts so you can focus on the date fields and the few int vs string calls the type inference cannot make for you.

If the payload is rejected by json_decode, paste it into the JSON validator first to catch a trailing comma or stray newline before blaming PHP. The JSON formatter is useful for spotting the structure of a deeply nested response by eye.

One Last Tip

json_decode without the associative flag returns stdClass and the -> arrow operator works on it. That looks tempting until you try to feed the result to a typed constructor and realize stdClass does not implement ArrayAccess. Always pass associative: true for DTO hydration. Save the stdClass form for one-off scripts where you just need to read three fields and exit.

Related Tools