OpenAPI Schema Generation
Overview
Section titled “Overview”Whity Core automatically generates OpenAPI 3.0 schemas from discovered plugins. This enables type-safe TypeScript client generation and API documentation.
What is OpenAPI?
Section titled “What is OpenAPI?”OpenAPI (formerly Swagger) is a specification for describing HTTP APIs. It enables:
- Automated client code generation
- Interactive API documentation
- API testing tools integration
- Schema validation
Learn more: https://spec.openapis.org/oas/v3.0.3
Generating the Schema
Section titled “Generating the Schema”Command
Section titled “Command”php public/index.php generate:openapiThis generates public/openapi.json containing the complete API specification.
Output
Section titled “Output”The generated openapi.json includes:
- Paths: All routes registered with the Router the generator is given. The
generate:openapicommand currently registers PLUGIN routes only; the core admin resources join the spec with #167 (the mechanism —Router::registerwith aschemaargument — is already in place for them) - Methods: HTTP method for each endpoint (GET, POST, PATCH, DELETE, etc.)
- Security: Bearer token authentication configuration
- Typed bodies (WC-166): routes that declare a
schemaget arequestBodyand per-statusresponsesreferencing namedcomponents.schemasvia$ref - Responses: the declared per-status success shape (or a
200default) PLUS the standard error surface injected into every operation (see below) - Tags: declared, or derived from the path
Generation is deterministic (paths, methods, and component schemas are
sorted — regenerating over the same routes is byte-identical) and
self-validating: the command refuses to write a spec with dangling $refs
or response-less operations (exit 1 with the errors listed).
Declaring typed request/response bodies (WC-166)
Section titled “Declaring typed request/response bodies (WC-166)”Any route — core (Router::register(..., schema:)) or plugin (the optional
'schema' key in the route array, SDK ≥ 1.1.1) — can declare its contract:
'schema' => [ 'summary' => 'Create a widget', 'tags' => ['widgets'], 'request' => 'WidgetCreate', // component name => $ref, or inline JSON-Schema array 'responses' => [ 201 => 'Widget', // component name => $ref'd application/json body 400 => ['description' => 'Validation failed'], // raw response object ], 'components' => [ // schemas this route contributes to components.schemas 'WidgetCreate' => ['type' => 'object', 'required' => ['name'], 'properties' => ['name' => ['type' => 'string']]], 'Widget' => ['type' => 'object', 'properties' => ['id' => ['type' => 'integer'], 'name' => ['type' => 'string']]], ],]Identical component contributions from multiple routes are idempotent; a
CONFLICTING redefinition keeps the first definition and logs a warning. The
shipped plugins/HelloWorld declares its /api/hello response (Greeting)
as a working reference.
Integration with Plugins
Section titled “Integration with Plugins”When you create a new plugin, the schema generator automatically includes it:
- Plugin implements
PluginInterface - Run
php public/index.php generate:openapi - New endpoint appears in
public/openapi.json
Example Plugin
Section titled “Example Plugin”<?phpnamespace Whity\Plugins;use Whity\Sdk\PluginInterface;use Whity\Core\Request;use Whity\Core\Response;
class UserList implements PluginInterface{ public function getRoute(): string { return '/api/users'; } public function getMethod(): string { return 'GET'; } public function getRequiredRole(): ?string { return null; } public function handle(Request $request): Response { // ... implementation }}Schema Features
Section titled “Schema Features”Route Detection
Section titled “Route Detection”Routes extracted from PluginInterface::getRoute() support:
- Simple paths:
/api/users - Parameterized paths:
/api/users/{id}
HTTP Methods
Section titled “HTTP Methods”All standard HTTP methods supported:
- GET — Retrieve resource
- POST — Create resource
- PATCH — Update resource
- DELETE — Delete resource
- PUT — Replace resource
Authentication
Section titled “Authentication”Endpoints with getRequiredRole() returning non-null are marked as requiring Bearer token authentication.
Endpoints with getRequiredRole() === null are public (no auth required).
Response Codes
Section titled “Response Codes”Standard error surface (WC-216)
Section titled “Standard error surface (WC-216)”The generator injects the application’s uniform error envelope into every
operation’s responses, so clients and MCP see the full error surface. Each
injected response references the shared Error component
(#/components/schemas/Error → { "error": string, "details"?: object },
the body produced by Response::error()).
Injected codes:
- Always (transport-level):
404Not found,405Method not allowed,500Internal server error. - Only when the operation requires authentication (it carries
security: [{bearerAuth: []}]):401Unauthorized. - Only when the operation is role/permission gated:
403Forbidden. - Only when the operation declares a request body:
400Invalid request body.
422/429 are not blanket-injected — they are owned by explicit route
declarations.
Injection is merge-not-clobber: an explicitly declared response for a
status code always wins (a route declaring a richer 403, a 422, etc. keeps
its own object). Public routes therefore no longer falsely advertise 401/403.
Response keys are emitted in ascending status-code order for byte-stable
regeneration.
Development
Section titled “Development”The schema generator is in src/OpenAPI/:
SchemaGenerator.php— Main generator classSchemaBuilder.php— OpenAPI spec builder helper
Tests are in tests/OpenAPI/ and tests/Console/.
Typed Frontend Client (WC-168)
Section titled “Typed Frontend Client (WC-168)”The spec is the generation input for the frontend’s typed API client:
cd web && npm run generate:apiregeneratesweb/lib/api/schema.d.tsfrompublic/openapi.jsonvia openapi-typescript. The committed file must match a fresh generation — CI fails otherwise (webjob drift check).web/lib/api/client.tswraps the schema with openapi-fetch and preserves the platform auth behavior as middleware:credentials: 'include', theX-Requested-WithCSRF header (WC-160), and the 401 → silent refresh → single retry flow. The retry bypasses the middleware, so refresh loops are structurally impossible.web/__tests__/typed-api-client.test.tspins this contract.- Screens import the singleton:
import { api } from '@/lib/api/client'and callapi.GET('/api/users'),api.POST('/api/delegations', { body }),api.PATCH('/api/users/{id}', { params: { path: { id } }, body })— request bodies, path params, query params and responses are all typed from the spec. - Feature
types.tsfiles derive their shapes from the schema (components['schemas']['Delegation']) instead of hand-mirroring the API.
Changing an endpoint therefore means: update the handler + CoreApiSchemas,
run php public/index.php generate:openapi, then npm run generate:api, and
commit all three artifacts together.