JSON to PHP Class Converter

Paste a JSON payload and get typed data-transfer objects for Laravel, Symfony or plain PHP, with a factory that builds them from json_decode. Generation happens in your browser, so the sample stays private.

JSON → PHP

Input

Settings

History

Load from URL

Typed objects instead of nested arrays

json_decode($json, true) gives you associative arrays, and every $data['customer']['address']['city'] lookup is a silent typo waiting to happen. Typed classes let your IDE autocomplete fields, let PHPStan or Psalm check them, and make a changed API fail loudly at the boundary instead of deep inside your code. This generator infers those classes from a sample: every distinct object shape gets a final class, nested objects are named after their keys, and array items take the singular form of the array’s name, so "orders": [...] holds Order objects.

The output starts with declare(strict_types=1);, so a value of the wrong type throws a TypeError when the object is built rather than being coerced.

Type mapping

  • Strings, integers, decimals and booleans become string, int, float and bool. Integer values are accepted where float is declared, which strict mode allows, so 79 and 129.9 can share a float property.
  • A key missing from some objects or null in some becomes nullable (?string), and fromArray() reads it with ?? null so a missing key is not an error.
  • A position holding several JSON types becomes a union type such as int|string; with null as well, int|string|null.
  • Arrays are declared array, the only native option, and a docblock gives the element type for static analysers: /** @var list<Order> */ or array<string, int>. Nested arrays nest the same way: list<list<int>>.
  • A position that is only null, or an array that is always empty, gives no type to infer and is declared mixed.
  • PHP integers are 64-bit. An integer beyond that range would become a lossy float in json_decode, so its property is typed int|string and the usage comment adds JSON_BIGINT_AS_STRING, which makes PHP return such numbers as exact strings.
  • Classes that would collide with reserved words (List, Class, Default) get a Model suffix.

Options

Root type name names the top-level class, in PascalCase. A JSON array at the root generates the element class, and the usage comment shows the array_map call that builds a list of them.

Namespace adds a namespace declaration such as App\Dto, matching your PSR-4 autoloading; leave it empty for none. An invalid value is reported and left out rather than producing code that does not parse.

readonly properties (on by default) declares every promoted property public readonly, so objects are immutable after construction, which suits data that came from an API. Turn it off for mutable models.

fromArray() factory adds public static function fromArray(array $data): self, which calls the constructor with named arguments, converts nested arrays to their classes (including lists of objects and nullable children, using isset), and leaves scalars as decoded. Without it you get plain classes for a serializer such as Symfony Serializer or Valinor to hydrate.

camelCase property names (on by default) turns user_id into $userId; fromArray() still reads the original key, so nothing else needs mapping. With it off, keys that are valid PHP variable names are kept and invalid characters become underscores.

Using the classes

Decode with json_decode($json, true, 512, JSON_THROW_ON_ERROR) so malformed JSON throws, then pass the array to Root::fromArray(). Keys present in the JSON but not in the class are ignored, so additive API changes do not break anything. For calling the same API from PHP, the cURL to PHP converter writes the request; tidy edited classes with the PHP formatter.

Examples

Customer with addresses

customerId maps back to customer_id in fromArray; addresses is a list of Address objects and postcode is ?string because one value is null.

Input
{
  "customer_id": 1042,
  "email": "[email protected]",
  "vip": true,
  "addresses": [
    { "line1": "1 Raffles Place", "city": "Singapore", "postcode": "048616" },
    { "line1": "22 Marina Way", "city": "Singapore", "postcode": null }
  ]
}
Output
<?php

declare(strict_types=1);

namespace App\Dto;

// $value = Customer::fromArray(json_decode($json, true, 512, JSON_THROW_ON_ERROR));

final class Customer
{
    public function __construct(
        public readonly int $customerId,
        public readonly string $email,
        public readonly bool $vip,
        /** @var list<Address> */
        public readonly array $addresses,
    ) {
    }

    /** @param array<string, mixed> $data */
    public static function fromArray(array $data): self
    {
        return new self(
            customerId: $data['customer_id'],
            email: $data['email'],
            vip: $data['vip'],
            addresses: array_map(static fn (array $item): Address => Address::fromArray($item), $data['addresses']),
        );
    }
}

final class Address
{
    public function __construct(
        public readonly string $line1,
        public readonly string $city,
        public readonly ?string $postcode,
    ) {
    }

    /** @param array<string, mixed> $data */
    public static function fromArray(array $data): self
    {
        return new self(
            line1: $data['line1'],
            city: $data['city'],
            postcode: $data['postcode'],
        );
    }
}
Open this example in the tool

Plain mutable classes for a serializer

Without readonly and fromArray the classes only declare typed promoted properties, ready for a hydrator.

Input
{
  "orderNumber": "SO-2207",
  "total": 129.9,
  "lines": [{ "sku": "KB-104", "qty": 1 }],
  "meta": {}
}
Output
<?php

declare(strict_types=1);

// $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR); // then pass its fields to new Order(...)

final class Order
{
    public function __construct(
        public string $orderNumber,
        public float $total,
        /** @var list<Line> */
        public array $lines,
        public Meta $meta,
    ) {
    }
}

final class Line
{
    public function __construct(
        public string $sku,
        public int $qty,
    ) {
    }
}

final class Meta
{
    public function __construct()
    {
    }
}
Open this example in the tool

Mixed values and a big integer

The root array produces a RootElement class; id is int|string with JSON_BIGINT_AS_STRING in the usage comment, and value is int|string.

Input
[
  { "id": 98765432109876543210, "value": 1 },
  { "id": 5, "value": "high" }
]
Output
<?php

declare(strict_types=1);

// The JSON root is list<RootElement>:
// $items = array_map(static fn (array $item): RootElement => RootElement::fromArray($item), json_decode($json, true, 512, JSON_THROW_ON_ERROR | JSON_BIGINT_AS_STRING));

final class RootElement
{
    public function __construct(
        public readonly int|string $id,
        public readonly int|string $value,
    ) {
    }

    /** @param array<string, mixed> $data */
    public static function fromArray(array $data): self
    {
        return new self(
            id: $data['id'],
            value: $data['value'],
        );
    }
}
Open this example in the tool

Common errors and how to fix them

ErrorCauseFix
Trailing comma before '}'
Explained
The sample has a comma after the last property, which JavaScript allows and JSON does not.Remove the comma, or let the JSON formatter fix the sample first.
Some integers do not fit PHP's 64-bit int; they are typed int|string.
Explained
A warning: an ID or amount in the sample is larger than PHP_INT_MAX.Decode with JSON_BIGINT_AS_STRING as the usage comment shows, so those values arrive as exact strings.
TypeError: Argument #1 ($id) must be of type int, string givenA real response sends a type the sample did not contain, and strict_types refuses to convert it.Widen the property type, for example to int|string, or add such a value to the sample and regenerate.
Undefined array key "…"A key that was in every sample object is missing from real data, but fromArray() reads it as required.Make the property nullable and read the key with ?? null, or add an object without that key to the sample.

Frequently asked questions

Which PHP version do I need?

PHP 8.1 or later, for readonly properties. With the readonly option turned off the classes also run on PHP 8.0, which introduced constructor promotion, union types and mixed.

Why are arrays typed as array with a docblock?

PHP has no generic array types, so the native type can only say array. The @var list<Item> docblock tells PHPStan, Psalm and IDEs what the elements are.

Does it work with Laravel?

Yes. The classes are framework-free; build them from $request->json()->all() or Http::get(…)->json() with fromArray().

Is my JSON uploaded?

No. The sample is parsed and the classes are written by JavaScript running in this tab; nothing goes over the network.

Related tools