openapi-tools / generator-schema
Schema (+ contract + error) generator
Package info
github.com/php-openapi-tools/generator-schema
Language:Makefile
pkg:composer/openapi-tools/generator-schema
Fund package maintenance!
Requires
- php: ^8.4
- ext-json: ^8.4
- eventsauce/object-hydrator: ^1.5
- nikic/php-parser: ^5.0
- openapi-tools/contract: ^0.1.0
- openapi-tools/generator-utils: ^0.1.0
- openapi-tools/representation: ^0.1.0
- openapi-tools/utils: ^0.1.0
Requires (Dev)
- openapi-tools/configuration: ^0.1.0
- openapi-tools/gatherer: ^0.1.0
- openapi-tools/test-data: dev-main
- wyrihaximus/async-test-utilities: ^13.5.1
- wyrihaximus/makefiles: ^0.13.3
This package is auto-updated.
Last update: 2026-08-31 23:29:58 UTC
README
FileGenerator for OpenAPI Tools that turns gathered schema metadata into PHP source files: readonly value objects, contract interfaces, throwable error wrappers, and property caster attributes for union-typed fields.
Requirements
- PHP
^8.4 ext-json
Installation
composer require openapi-tools/generator-schema
Where it fits
This package runs during step 4 of the OpenAPI Tools pipeline — after gatherer has built a representation and class names have been resolved with Representation::namespace():
flowchart LR spec[OpenAPI spec] --> gatherer[Gatherer] gatherer --> rep[Representation] rep --> ns["namespace()"] ns --> gen[Schema generator] gen --> contract[Contract interfaces] gen --> error[Error classes] gen --> schema[Schema classes] gen --> casters[Union PropertyCasters]Loading
Register Schema before generator-hydrator. Hydrators reflect on the schema classes this generator emits, and the generator run loop include_onces each written file so later generators can depend on freshly emitted types.
Components
| Class | Purpose |
|---|---|
Schema |
Entry-point FileGenerator; delegates to the internal generators below |
Internal\Contract |
Emits deduplicated contract interfaces with @property PHPDoc |
Internal\Error |
Emits one final \Error subclass per schema |
Internal\Schema |
Emits readonly schema classes, alias classes, and union caster attributes |
Internal\Schema\SingleCastUnionToType |
PropertyCaster for a single union-typed property |
Internal\Schema\MultipleCastUnionToType |
PropertyCaster for an array whose items are a union |
Type resolution and PHPDoc formatting delegate to generator-utils (PropertyTypeResolver, UnionTypeUtils, DocBlockBuilder).
Usage
Schema takes a shared nikic/php-parser BuilderFactory and is attached to a package's generators list:
use OpenAPITools\Generator\Schema\Schema; use PhpParser\BuilderFactory; $builderFactory = new BuilderFactory(); // inside Package(..., generators: [ new Schema($builderFactory), ... ])
A full configuration example lives in openapi-tools/generator. At minimum you need gathering with schema aliasing enabled when your spec contains structurally identical inline objects:
new Gathering\Schemas( allowDuplication: true, useAliasesForDuplication: true, ),
Direct invocation
Useful in tests and custom tooling:
use OpenAPITools\Generator\Schema\Schema; use PhpParser\BuilderFactory; $generator = new Schema(new BuilderFactory()); foreach ($generator->generate($package, $representation->namespace($package->namespace)) as $file) { // $file->pathPrefix — e.g. "src" // $file->fqcn — e.g. "Schema\\Basic" // $file->contents — PhpParser Node or pre-rendered string }
Generation order
Schema::generate() yields files in a fixed order for every schema in the representation:
- Contracts — one interface per unique contract, deduplicated by FQCN across all schemas.
- Errors — one
\Errorsubclass at{namespace}\Schema\Error\{SchemaName}. - Schema classes — the readonly value object, any alias classes, and per-property union caster attributes.
Union caster classes are emitted as part of step 3 when a property needs them.
Output layout
Given namespace ApiClients\Client\Example and a component schema named Basic:
| Relative path | Kind |
|---|---|
Contract/Basic.php |
Interface with @property lines |
Schema/Basic.php |
Final readonly schema class |
Schema/Error/Basic.php |
Final error class |
Schema/AliasAbstract/T{hash}/…php |
Abstract base when aliases exist |
Schema/{AliasName}.php |
Final readonly alias extending the abstract base |
Internal/Attribute/CastUnionToType/Single/…php |
Union property caster |
Internal/Attribute/CastUnionToType/Multiple/…php |
Array-of-union property caster |
The {hash} in alias abstract class names is derived from the schema's serialized JSON so structurally identical schemas share one abstract base.
Generated code
Contract interfaces
Interfaces carry @property PHPDoc from the contract properties gathered for that shape. The interface body is empty; schemas implement it:
namespace ApiClients\Client\Example\Contract; /** * @property string $id * @property string $name */ interface Basic { }
Schema classes
Every schema class is readonly, implements its contract(s), and exposes metadata through constants:
| Constant | Visibility | Purpose |
|---|---|---|
SCHEMA_JSON |
private |
Pretty-printed JSON Schema fragment for runtime validation |
SCHEMA_TITLE |
public |
OpenAPI title |
SCHEMA_DESCRIPTION |
public |
OpenAPI description |
SCHEMA_EXAMPLE_DATA |
private |
Pretty-printed example payload assembled during gathering |
Constructor parameters are public promoted properties. Property names in PHP may differ from JSON keys — when they do, EventSauce ObjectHydrator #[MapFrom('json_key')] is attached.
namespace ApiClients\Client\Example\Schema; final readonly class Basic implements \ApiClients\Client\Example\Contract\Basic { private const SCHEMA_JSON = '{ "type": "object", ... }'; public const SCHEMA_TITLE = 'basic'; public const SCHEMA_DESCRIPTION = ''; private const SCHEMA_EXAMPLE_DATA = '[ ... ]'; public function __construct( public string $id, public string $name, ) { } }
Downstream code (webhook middleware, validators) reads SCHEMA_JSON for JSON Schema validation without shipping a separate schema file.
Nested objects and arrays
| OpenAPI shape | Generated PHP |
|---|---|
| Property referencing another component schema | Constructor parameter typed to that schema class |
array of schema objects |
#[CastListToType(OtherSchema::class)] on the parameter |
| Scalar / simple union | Native PHP union type on the parameter (string|int|float) |
| Object union (no discriminator) | Generated PropertyCaster attribute class on the parameter |
| Array of object union items | Pair of Single + Multiple caster classes |
For scalar unions such as Jira-style issue field values, duplicate type tokens are collapsed so you get string\|int\|float, not string\|int\|float\|int:
// from DoubleUseOfTypes.yaml — type: [null, string, number, integer] + anyOf public function __construct(public string|int|float|null $value) { }
Object unions without discriminator
When a property resolves to multiple object schemas, the generator emits a PropertyCaster that fingerprints incoming arrays by sorted key names (and enum values when present), then hydrates the matching schema class:
// Simplified behaviour of generated SingleCastUnionToType classes if (is_array($value)) { $signature = implode('|', sort(array_unique(array_keys($value)))); if ($signature === 'preferred|value') { /* hydrate PreferredName */ } if ($signature === 'value') { /* hydrate FirstName */ } } return $value;
Caster classes live under Internal\Attribute\CastUnionToType\ and are referenced as parameter attributes on the schema constructor.
Schema aliases
When registry records structurally identical inline schemas as aliases (useAliasesForDuplication: true), the generator:
- Moves the shared implementation into an abstract readonly class under
Schema\AliasAbstract\. - Emits one final readonly alias class per registered name, each extending the abstract base.
The canonical class name from gathering becomes the last alias; the abstract base holds the constructor, constants, and union casters.
Error classes
Each schema gets a final error class extending \Error. Instances carry the HTTP status code and a hydrated copy of the schema:
namespace ApiClients\Client\Example\Schema\Error; final class Basic extends \Error { public function __construct( public int $status, public \ApiClients\Client\Example\Schema\Basic $error, ) { } }
These are used for error responses registered during gathering (ThrowableSchema in the registry). The representation also tracks an errorClassNameAliased path for deduplicated error shapes; this generator emits the primary Schema\Error\{Name} class.
Supported patterns
Behaviour is locked down through shared fixtures in openapi-tools/test-data. Each YAML file has a matching assertion class under tests/DataTests/.
| Fixture | What it exercises |
|---|---|
Basic |
Minimal object, $ref, UUID format, response headers |
ExampleData |
Scalars, formats, patterns, arrays, nullable unions, example constants |
Aliases |
Structurally identical inline objects → abstract base + alias classes |
NestedSchema |
Inline nested objects without $ref |
NestedReferenceSchema |
Nested objects via component $ref |
TripleNestedSchema |
Deep nesting with $ref on nested schema keyword |
DoubleUseOfTypes |
OpenAPI 3.1 type array combined with anyOf on one property |
Run the suite:
make unit-testing
For the full fixture roadmap and situation coverage matrix, see test-data/src/DataSets/PLAN.md.
Related packages
| Package | Relationship |
|---|---|
contract |
FileGenerator and Package interfaces |
representation |
Input model (Namespaced\Schema, Contract, Property) |
gatherer |
Builds the representation this generator consumes |
registry |
Schema deduplication and alias registration during gathering |
generator-utils |
AST builders and type resolution helpers |
generator-hydrator |
Typically runs after this package |
generator |
CLI and run loop that orchestrates all generators |
Contributing
Please see CONTRIBUTING for details.
License
The MIT License (MIT)
Copyright (c) 2026 Cees-Jan Kiewiet
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.