The two objects did not share a shape. Ada's row had name. The next row had email and no name. I pasted the array into the JSON converter and ran it in the browser. TypeScript drafted export interface Root from the first object, so email stayed off the interface. The PHP DTO used that same first object. Laravel still walked both rows, and CSV still collected every top-level key.
[{"id":1,"name":"Ada"},{"id":2,"email":"linus@example.com"}] Laravel keeps the second row
Laravel prints the whole array. The second PHP array still contains email.
[ [ 'id' => 1, 'name' => 'Ada', ], [ 'id' => 2, 'email' => 'linus@example.com', ], ] The draft follows Ada
TypeScript uses the first object when the root value is an array. The interface has id and name. The root name is Root.
export interface Root { id: number; name: string; } The PHP DTO is built from that first object as well. The class name stays DataTransferObject. email is not a property. Each assignment that does exist uses ?? null.
<?php declare(strict_types=1); class DataTransferObject { public int $id; public string $name; public function __construct(array $data = []) { $this->id = $data['id'] ?? null; $this->name = $data['name'] ?? null; } } CSV still adds the column
CSV builds the header from every top-level key it sees. email is a column. A missing key is an empty cell, so the first data row ends after Ada and the second data row has a blank name.
id,name,email 1,Ada, 2,,linus@example.com One tip
Later rows still appear in the Laravel array, and a key that shows up only on a later row still joins the CSV header. The type and the PHP class do not grow to match that header.
The nested-cell limits and the rest of the questions are in the guide: https://kepttools.com/guides/json-to-typescript-php-dto-laravel
