php-openapi/openapi
PHP library for reading, validating, and writing OpenAPI 3.0 specifications. Parse JSON or YAML into an object model, resolve references, inspect paths and schemas, and generate updated spec files—useful for API tooling, docs, and governance.
Install the parser:
composer require php-openapi/openapi
For YAML specs you also need symfony/yaml (JSON specs have no extra dependency). Laravel already ships with it in many projects.
The single entry point you'll use 90% of the time is OpenApi\Reader:
use OpenApi\Reader;
$spec = Reader::readFromYamlFile(base_path('openapi.yaml'));
// also: readFromJsonFile(), readFromYaml(), readFromJson()
if (! $spec->validate()) {
foreach ($spec->getErrors() as $error) {
$this->error($error);
}
return 1;
}
Where to look first:
OpenApi\Reader — the factory methods for loading a spec from a file, string, URL.$spec->paths, $spec->components->schemas, $spec->info — the object graph you'll navigate.$spec->validate() + $spec->getErrors() — the built-in structural validator.$spec->resolveReferences() — resolves every $ref into the real object instance.First use case: a php artisan openapi:lint command that parses the spec and fails CI when validation errors exist. This is the fastest way to prove the package works in your app before you build anything more sophisticated on top of it.
class LintOpenApiCommand extends Command
{
protected $signature = 'openapi:lint {file=openapi.yaml}';
public function handle()
{
try {
$spec = Reader::readFromYamlFile(base_path($this->argument('file')));
} catch (\Throwable $e) {
$this->error("Could not parse spec: {$e->getMessage()}");
return self::FAILURE;
}
if ($spec->validate()) {
$this->info('Spec is valid.');
return self::SUCCESS;
}
foreach ($spec->getErrors() as $error) {
$this->error($error);
}
return self::FAILURE;
}
}
Bind the parsed spec as a singleton. Parsing and validation are not free — do it once per request lifecycle and reuse it via the container.
// AppServiceProvider::register()
$this->app->singleton(OpenApi::class, function () {
$spec = Reader::readFromYamlFile(base_path('openapi.yaml'));
$spec->resolveReferences();
return $spec;
});
After resolveReferences() the object graph is fully walkable — $spec->paths['/users']->get->responses['200']->content['application/json']->schema returns the concrete Schema, not a Reference.
Walk operations to generate route stubs, docs, or Postman collections. The verbs are individual nullable properties on each PathItem:
$verbs = ['get','post','put','patch','delete','head','options','trace'];
foreach ($spec->paths as $path => $pathItem) {
foreach ($verbs as $verb) {
$operation = $pathItem->{$verb};
if ($operation === null) {
continue;
}
$operationId = $operation->operationId ?? "{$verb} {$path}";
// $operation->parameters, $operation->requestBody, $operation->responses
}
}
Extract a JSON Schema from a component. Useful for emitting TypeScript interfaces or validating payloads client-side:
$schema = $spec->components->schemas['User'];
$jsonSchema = json_encode($schema); // round-trips to a plain JSON Schema doc
Use it as the parser behind request/response contract tests. php-openapi deliberately does not validate HTTP messages. Pair it with league/openapi-psr7-validator and convert the Laravel request to PSR-7 first:
use League\OpenAPIValidation\PSR7\ValidatorBuilder;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
$validator = (new ValidatorBuilder)->fromYamlFile(base_path('openapi.yaml'))->getRequestValidator();
$psrRequest = app(PsrHttpFactory::class)->createRequest(request());
$validator->validate($psrRequest);
Detect breaking changes between two spec revisions. Parse both, walk components->schemas and paths, and diff the operation/response key sets. $spec->info->version gives you the spec's own semantic version for tagging. This is a cheap alternative to heavier schema-diffing tools when you only care about "did we remove an endpoint or required field?"
Generate a TypeScript client-side API surface by iterating components->schemas, mapping Schema::$type (string, integer, object, array) plus $schema->required to a .d.ts file. Because everything is a plain PHP object, this is trivial templating.
validate() returns a bool, it never throws. The failure mode is silent — you must call getErrors() to see what's wrong. A valid-looking spec can still be flagged for things like missing info or malformed $ref targets.resolveReferences() is explicit, not automatic. Without it, traversing into a components/schemas/* entry gives you a Reference wrapper object, not the real Schema. Any code that reflects on $schema->type or $schema->properties will silently get null until you resolve.$refs will blow up resolution. If you have self-referencing schemas (tree nodes, linked lists), call resolveReferences() with the recursion flag off ($recursive = false) or pass a resolutionCallback that keeps them lazy for the parts of the tree you don't need.$ref: './common.yaml#/components/schemas/Error' resolves relative to the original file's directory. When reading a spec from a string or a stream, supply a ReferenceContext pointing at the right base path, or the resolution fails with an unresolvable-reference exception.readFromYamlFile / readFromJsonFile in try/catch — a syntax error in YAML will throw before you ever reach validate().$schema, webhooks). Pin the package and add a test around the features you actually rely on.league/openapi-psr7-validator for runtime checks and use this for everything static.resolveReferences() on a large spec is expensive. If you generate docs or types at build time, do it in a deploy step to a JSON artifact rather than at request time.$spec->info, $spec->paths, etc. are typed objects, not arrays. They implement ArrayAccess and Countable, so foreach and isset work naturally, but array_map does not — use iterator_to_array() if you need to pass to array-expecting code.Schema implements JsonSerializable, json_encode($schema) is the cleanest way to bridge into any other JSON-Schema-aware library — the output is standard draft-04-compatible JSON Schema and most tools will accept it without massaging.How can I help you explore Laravel packages today?