@@ -107,16 +107,36 @@ Example array output:
107107 'description' => [
108108 'title' => 'Description',
109109 'description' => 'The description of the movie',
110- 'type' => ['string', 'null'],
110+ 'oneOf' => [
111+ ['type' => 'null'],
112+ ['type' => 'string'],
113+ ],
111114 ],
112- 'director' => [
113- 'type' => ['string', 'null'],
115+ 'director' => [
116+ 'oneOf' => [
117+ ['type' => 'null'],
118+ ['type' => 'string'],
119+ ],
114120 ],
115121 'releaseStatus' => [
116122 'title' => 'Release Status',
117123 'description' => 'The release status of the movie',
118- 'type' => ['string', 'null']
119- 'enum' => ['Released', 'Rumored', 'Post Production', 'In Production', 'Planned', 'Canceled'],
124+ 'oneOf' => [
125+ [
126+ 'type' => 'null',
127+ ],
128+ [
129+ 'type' => 'string',
130+ 'enum' => [
131+ 'Released',
132+ 'Rumored',
133+ 'Post Production',
134+ 'In Production',
135+ 'Planned',
136+ 'Canceled',
137+ ],
138+ ],
139+ ],
120140 ],
121141 ],
122142 'required' => [
@@ -142,7 +162,7 @@ final class Actor
142162 /**
143163 * @var array<Movie >
144164 */
145- public readonly array $movies = [] ,
165+ public readonly ? array $movies = null ,
146166 public readonly ?Movie $bestMovie = null;
147167 ) {
148168 }
@@ -175,21 +195,27 @@ Example array output:
175195 'type' => 'string',
176196 ],
177197 'movies' => [
178- 'type' => 'array',
179- 'items' => [
180- '$ref' => '#/definitions/Movie',
198+ 'oneOf' => [
199+ [
200+ 'type' => 'null',
201+ ],
202+ [
203+ 'type' => 'array',
204+ 'items' => [
205+ '$ref' => '#/definitions/Movie',
206+ ],
207+ ],
181208 ],
182- 'default' => [],
183209 ],
184210 'bestMovie' => [
185211 'title' => 'Best Movie',
186212 'description' => 'The best movie of the actor',
187213 'oneOf' => [
188214 [
189- '$ref ' => '#/definitions/Movie ',
215+ 'type ' => 'null ',
190216 ],
191217 [
192- 'type ' => 'null ',
218+ '$ref ' => '#/definitions/Movie ',
193219 ],
194220 ],
195221 ],
@@ -215,15 +241,24 @@ Example array output:
215241 'description' => [
216242 'title' => 'Description',
217243 'description' => 'The description of the movie',
218- 'type' => ['string', 'null'],
244+ 'oneOf' => [
245+ ['type' => 'null'],
246+ ['type' => 'string'],
247+ ],
219248 ],
220249 'director' => [
221- 'type' => ['string', 'null'],
250+ 'oneOf' => [
251+ ['type' => 'null'],
252+ ['type' => 'string'],
253+ ],
222254 ],
223255 'releaseStatus' => [
224256 'title' => 'Release Status',
225257 'description' => 'The release status of the movie',
226- 'type' => ['string', 'null']
258+ 'oneOf' => [
259+ ['type' => 'null'],
260+ ['type' => 'string'],
261+ ],
227262 'enum' => ['Released', 'Rumored', 'Post Production', 'In Production', 'Planned', 'Canceled'],
228263 ],
229264 ],
@@ -236,6 +271,163 @@ Example array output:
236271];
237272```
238273
274+ ## Polymorphic Arrays (anyOf)
275+ The generator also supports arrays that contain different types of DTOs using PHPDoc annotations like ** @var list<Movie|Series>** .
276+ For example, if an actor can have a filmography that includes both movies and TV series, you can define it like this:
277+ ``` php
278+ namespace App\DTO;
279+
280+ use Spiral\JsonSchemaGenerator\Attribute\Field;
281+
282+ final class Actor
283+ {
284+ public function __construct(
285+ public readonly string $name,
286+ public readonly int $age,
287+
288+ #[Field(title: 'Biography', description: 'The biography of the actor')]
289+ public readonly ?string $bio = null,
290+
291+ /**
292+ * @var list<Movie |Series >|null
293+ */
294+ #[Field(title: 'Filmography', description: 'List of movies and series featuring the actor')]
295+ public readonly ?array $filmography = null,
296+
297+ #[Field(title: 'Best Movie', description: 'The best movie of the actor')]
298+ public readonly ?Movie $bestMovie = null,
299+
300+ #[Field(title: 'Best Series', description: 'The most prominent series of the actor')]
301+ public readonly ?Series $bestSeries = null,
302+ ) {}
303+ }
304+ ```
305+ The generated schema will reflect this with an anyOf definition in the items section:
306+ ``` php
307+ [
308+ 'properties' => [
309+ 'filmography' => [
310+ 'title' => 'Filmography',
311+ 'description' => 'List of movies and series featuring the actor',
312+ 'oneOf' => [
313+ ['type' => 'null'],
314+ [
315+ 'type' => 'array',
316+ 'items' => [
317+ 'anyOf' => [
318+ ['$ref' => '#/definitions/Movie'],
319+ ['$ref' => '#/definitions/Series'],
320+ ],
321+ ],
322+ ],
323+ ],
324+ ],
325+ ],
326+ 'definitions' => [
327+ 'Movie' => [/* ... */],
328+ 'Series' => [/* ... */],
329+ ],
330+ ];
331+ ```
332+ ## Example DTO: Series
333+ Here's what the Series class might look like:
334+ ``` php
335+ namespace App\DTO;
336+
337+ use Spiral\JsonSchemaGenerator\Attribute\Field;
338+ use Spiral\JsonSchemaGenerator\Attribute\Format;
339+
340+ final class Series
341+ {
342+ public function __construct(
343+ #[Field(title: 'Title', description: 'The title of the series')]
344+ public readonly string $title,
345+
346+ #[Field(title: 'First Air Year', description: 'The year the series first aired')]
347+ public readonly int $firstAirYear,
348+
349+ #[Field(title: 'Description', description: 'The description of the series')]
350+ public readonly ?string $description = null,
351+
352+ #[Field(title: 'Creator', description: 'The creator or showrunner of the series')]
353+ public readonly ?string $creator = null,
354+
355+ #[Field(title: 'Series Status', description: 'The current status of the series')]
356+ public readonly ?SeriesStatus $status = null,
357+
358+ #[Field(title: 'First Air Date', description: 'The original release date of the series', format: Format::Date)]
359+ public readonly ?string $firstAirDate = null,
360+
361+ #[Field(title: 'Last Air Date', description: 'The most recent air date of the series', format: Format::Date)]
362+ public readonly ?string $lastAirDate = null,
363+
364+ #[Field(title: 'Seasons', description: 'Number of seasons released')]
365+ public readonly ?int $seasons = null,
366+ ) {}
367+ }
368+ ```
369+ > ** Note**
370+ > When using polymorphic arrays, make sure all referenced DTOs (e.g., Movie, Series)
371+ > are also annotated properly so their definitions can be generated correctly.
372+
373+ ## Union Types
374+ The JSON Schema Generator supports native PHP union types (introduced in PHP 8.0), including nullable and multi-type definitions.
375+ Here's an example DTO using union types:
376+ ``` php
377+ namespace App\DTO;
378+
379+ use Spiral\JsonSchemaGenerator\Attribute\Field;
380+
381+ final class FlexibleValue
382+ {
383+ public function __construct(
384+ #[Field(title: 'Value', description: 'Can be either string or integer')]
385+ public readonly string|int $value,
386+
387+ #[Field(title: 'Optional Flag', description: 'Boolean or null')]
388+ public readonly bool|null $flag = null,
389+
390+ #[Field(title: 'Flexible Field', description: 'Can be string, int, or null')]
391+ public readonly string|int|null $flex = null,
392+ ) {}
393+ }
394+ ```
395+ The generated schema will include a ` oneOf ` section to reflect the union types:
396+ ``` php
397+ [
398+ 'properties' => [
399+ 'value' => [
400+ 'title' => 'Value',
401+ 'description' => 'Can be either string or integer',
402+ 'oneOf' => [
403+ ['type' => 'string'],
404+ ['type' => 'integer'],
405+ ],
406+ ],
407+ 'flag' => [
408+ 'title' => 'Optional Flag',
409+ 'description' => 'Boolean or null',
410+ 'oneOf' => [
411+ ['type' => 'null'],
412+ ['type' => 'boolean'],
413+ ],
414+ ],
415+ 'flex' => [
416+ 'title' => 'Flexible Field',
417+ 'description' => 'Can be string, int, or null',
418+ 'oneOf' => [
419+ ['type' => 'null'],
420+ ['type' => 'string'],
421+ ['type' => 'integer'],
422+ ],
423+ ],
424+ ],
425+ 'required' => ['value'],
426+ ]
427+ ```
428+ > ** Note**
429+ > All supported types are automatically resolved from native PHP type declarations and reflected in the JSON Schema output using oneOf.
430+
239431## Testing
240432
241433``` bash
0 commit comments