-
Notifications
You must be signed in to change notification settings - Fork 15
REST API OpenAPI Standards
This page describes how the OpenAPI document is generated, validated and verified. It is written for developers evaluating whether the specification can be trusted as a contract.
The document is produced on each request by a generator that reads three sources already maintained as part of the API:
- the route table β the authoritative list of paths, methods, permissions and handler functions the front controller dispatches;
- the documentation catalogue β endpoint summaries, descriptions, parameters, request examples and endpoint-specific errors, shared with the interactive docs page;
- the typed schemas β reusable component schemas describing every response object.
Because it is derived from the live route table, the specification describes the API the install is actually running. There is no separate file to fall out of date.
The document validates against the official OpenAPI 3.0 meta-schema with zero errors. Conformance is verified two ways:
- a bundled validator checks the generated document against a local copy of the official meta-schema (
api/v1/dev/jsonschema4.php), so it can be re-run offline at any time; - the same document loads in Swagger UI, Redoc, Postman and Insomnia, and drives the client generators (openapi-generator, swagger-codegen).
The validator is proven to reject malformed documents β a document with a missing openapi version, a missing responses object, or an out-of-range version string all fail it β so a clean pass is a meaningful result rather than a validator that passes everything.
For teams that run linters in CI, the document passes Spectral's recommended ruleset and swagger-cli validate in a Node environment.
Every response schema is validated against a live response from a running install. A verification tool (api/v1/dev/openapi_verify.php) fetches each GET endpoint, resolves any id parameters from a sibling collection, and checks the returned data against its schema field-by-field β including type and nullability, and flagging any response field the schema fails to document. The schemas were derived from the serializers and then reconciled against live data until this reported zero mismatches.
The types are read directly from the serializers, which cast every value explicitly ((int), (bool), dates through a shared ISO-8601 helper, nested objects, arrays), so the declared types reflect what the code emits rather than an inference from a single sample.
A self-check (api/v1/lib/openapi_check.php) verifies a set of invariants and exits non-zero on any failure, so it can gate CI:
- drift β every route in the table has a specification entry and every specification entry maps to a real route (a clean one-to-one match);
-
references β every
$refresolves to a defined component schema; - operationIds β present and unique across all operations;
- responses β every operation declares at least one response;
- shape β no object-typed field is emitted as an empty array.
- Version 3.0.3. Targeted for the widest tooling compatibility. It imports into current and older Swagger UI, Redoc, Postman, Insomnia and the mainstream client generators.
- Two formats, one document. JSON is canonical; YAML is generated from the same structure with conservative quoting, so scalars are never coerced to the wrong type.
- Dynamic payloads are typed as open objects. Workflow conditions and actions, form field definitions, and workflow-execution payloads and step logs vary in shape by design; they are described as objects rather than given a fixed field list that would misrepresent them.
- Request bodies are documented with worked examples. The API validates request fields at call time; response bodies additionally carry full typed schemas. A small number of endpoints that require a specific nested id are covered by their derived schema but are not part of the automated live-verification sweep.
The tooling needs only PHP β no Node, Python or Composer:
# invariants (drift, refs, operationIds, responses, shape)
php api/v1/lib/openapi_check.php
# conformance to the official 3.0 meta-schema
curl -s http://localhost/freeitsm-app/api/v1/openapi.json > /tmp/o.json
php api/v1/dev/jsonschema4.php /tmp/o.json api/v1/dev/oas-3.0-schema.json
# response schemas vs live responses (needs a read key)
php api/v1/dev/openapi_verify.php <read_key>See api/v1/dev/README.md for the full workflow, including how to bring the typed schemas back in line after changing a module.
The document and its checks were built against a running install, and a few problems only surfaced through validation β first the built-in checks, then an external linter. They are recorded here because each one changed the tooling, and the same traps catch most hand-built OpenAPI documents.
An OpenAPI document is defined by a meta-schema (itself a JSON Schema), and validating against it is the formal definition of "conforms to the standard". But the meta-schema is deliberately permissive β it allows constructs the specification's prose forbids and that linters reject. So "validates against the meta-schema with zero errors" is a true and meaningful statement, but it is not the same as "no tool will complain". The tooling therefore does both: meta-schema validation and a self-check that encodes the linter-style rules the meta-schema leaves out. When an external validator later flags something, the first question is which of the two it is β a genuine meta-schema violation, or a stricter linter opinion β and the fix in the second case is to add that rule to the self-check so it cannot recur.
In OpenAPI 3.0, nullable only has meaning beside a type; a schema that is nullable with no type is meaningless, and most linters reject it. The meta-schema permits it, so it passed meta-schema validation and the first version of the self-check missed it entirely. It arose two different ways, each fixed at the source:
-
Fields that were
nullin the sampled data. The schema fixer infers a field's type from a live value; when the only value it saw wasnull, it wrotenullable: truewith no type. Two changes fixed this: the fixer now upgrades a typeless node to a concrete type the moment any non-null value appears (so a list with many rows reveals most types), and the fields that arenullacross all sampled data were typed from their serializers β ids tointeger, money tonumber, tri-state flags toboolean, lookup sub-objects toobject, dates and text tostring, and genuinely polymorphic values (a CMDB property value, a form answer) to an untyped "any". -
The nullable-reference idiom. A field that references another schema but can also be null is commonly written
{allOf: [{$ref: β¦}], nullable: true}, because in 3.0 a$refignores its siblings so{$ref, nullable}cannot work. ButallOfis not atypeeither, so a strict linter still rejects it. The fix is the documented one: give it the composed type,{type: object, allOf: [{$ref: β¦}], nullable: true}.
The self-check's first nullable rule skipped any node that carried allOf/anyOf/oneOf, on the assumption that a composition keyword supplied the type. It does not β which is exactly why the nullable-reference idiom above slipped past. The rule now flags nullable without type unconditionally, matching how a linter reads it. The general lesson: a lint-style guard that is even slightly looser than the real linter produces false all-clears, so each guard is confirmed with an independent recount using the linter's own predicate rather than trusting the guard that is being written.
An "object" with no properties is an empty array in PHP, and json_encode turns an empty PHP array into [], not {}. Anywhere the specification requires an object β a schema's properties, an operation's responses, a media type's content β an accidental [] is invalid. This appeared when the fixer inferred a schema for a field whose sampled value was an empty list, leaving items: []. A first check for it used a spaced grep and missed the compact JSON; the self-check now inspects the encoded document and fails on any object-typed field emitted as [], and empty items/properties are stripped at the source.
Verifying a schema against one response has two failure modes the tooling had to handle. A field that is null in the sampled record looks untyped even though the code clearly types it β resolved by sampling across every row of a list and by falling back to the serializer for always-null fields. And a genuinely polymorphic field looks wrongly concrete: a CMDB object's property value is a string on a text CI and an integer on a numeric one, so a single text sample typed it string until a numeric CI's response contradicted it. Live verification caught the contradiction, and the field is now typed as an untyped "any" with a description, which is the honest representation.
| Tool | Role, and how it changed |
|---|---|
openapi_verify.php |
Fetches every GET endpoint and checks the live data against its schema. Gained OpenAPI-aware handling of $ref, allOf and nullable, resolution of {id} parameters from sibling collections, and detection of response fields the schema fails to document. |
openapi_fix.php |
Auto-patches the schemas from live responses. Its original null handling was the source of Pitfall 1; it now upgrades typeless nodes to concrete types on the first real value, and strips the empty items/properties behind Pitfall 3. |
jsonschema4.php |
A dependency-free draft-04 validator for checking the document against the official meta-schema without Node or Python. Kept honest by first proving it rejects deliberately broken documents (missing openapi, missing responses, an out-of-range version), so a clean pass is meaningful. |
openapi_check.php |
The permanent self-check. Grew from five invariants to six as the nullable rule was added, and the sixth rule itself was tightened once (Pitfall 2) to stop excluding composition keywords. |
Every one of these traps is now caught by openapi_check.php or the meta-schema validator, so re-running the checks after a change surfaces a regression rather than shipping it.
See also: REST API: OpenAPI specification (getting and using it) Β· REST API (how the API works).
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- Theming & Dark Mode
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
- MobileβFriendly
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- Problem Management
- Tasks
- Assets
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
- Forms
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)