Skip to content

Align the docs with the implementation, and centralise references on Reference:: - #22

Merged
tymondesigns merged 13 commits into
mainfrom
cursor/align-openapi-docs-6953
Sep 16, 2026
Merged

tymondesigns merged 13 commits into
mainfrom
cursor/align-openapi-docs-6953

Conversation

@tymondesigns

@tymondesigns tymondesigns commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Audit of the docs against the current implementation. Where the honest answer would have been a workaround, I fixed the thing that made the workaround necessary instead of writing it down.

Includes #23, merged into this branch, plus main merged in for PHP 8.4, Pest 5, and json-schema 2.

Code

Every reference is built through Reference::. The X::ref() shortcuts covered nine of the ten component registries and could never cover the tenth: schemas go through Reference::schema(), because Schema belongs to cortexphp/json-schema, which knows nothing about #/components. A family that advertises uniformity while omitting its most-used member is what makes Schema::ref('User') look reasonable when it cannot exist. Before the change there were 25 X::ref() call sites against 97 Reference:: ones, 61 of those Reference::schema(). All nine shortcuts were one-line delegations, so nothing is lost:

Response::ref('NotFound')     →  Reference::response('NotFound')
Parameter::ref('PetId')       →  Reference::parameter('PetId')
RequestBody::ref('Create')    →  Reference::requestBody('Create')
Header::ref('RateLimit')      →  Reference::header('RateLimit')
Example::ref('AdminUser')     →  Reference::example('AdminUser')
Link::ref('GetPet')           →  Reference::link('GetPet')
Callback::ref('OnEvent')      →  Reference::callback('OnEvent')
PathItem::ref('LegacyPets')   →  Reference::pathItem('LegacyPets')
SecurityScheme::ref('OAuth2') →  Reference::securityScheme('OAuth2')

Breaking, done now rather than after 1.0. The nine per-class tests went with them, since ReferenceTest already asserts all ten registries.

responses() accepts a referenced response. It keys each entry from getStatusCode(), and a bare Reference has none, so a referenced response could not go in. This is the only place in the package with that problem — Operation::parameters() and PathItem::parameters() take Parameter|Reference happily, because they build a list and derive no key. Removing the static freed the name for the fluent form:

Operation::get()->responses(
    Response::ok()->json(Reference::schema('User')),
    Response::notFound()->ref('NotFound'),
);

Byte-identical to ->response(404, Reference::response('NotFound')), asserted in a test rather than eyeballed. A referenced response emits the $ref alone, because a Reference Object has nowhere to put content — last-call-wins, matching responses() and content(), and pinned by a test.

Nested schemas leaked the JSON Schema $schema URI. JSON Schema 2020-12 core §8.1.1 allows it only at the root of a schema resource, and OAS 3.1 §Schema Object repeats the restriction, so every nested occurrence the builder can produce is invalid output. BuildsArray now strips it at every depth, skipping maps whose keys are user-chosen so a property literally named $schema survives, and leaving raw array schemas alone as the way to declare a dialect deliberately.

One for the json-schema side: 2.0 fixed this for items but contains still emits the URI. Of its ~20 nested serialization call sites those two were the only ones not passing includeSchemaRef: false by hand, and the fix reached one of them. A propagating rule rather than an argument repeated per keyword would have caught both.

Docs

New page: Validation & Output. The "Built-in Validation" and "Multiple Output Formats" cards both linked to /openapi/validation-and-output, which was never written. It covers validate(), the structured errors() payload keyed by JSON pointer, what the meta-schema does and does not catch, and the three output formats. It also decodes the anyOf error that reports one rule as three missing-property lines.

Page Was Now
Introduction OpenApi::v310() / v311() OpenApi::create(OpenApiVersion::V3_1_1)
Introduction "$schema and title are stripped" Builder names are dropped, an explicit ->title() is published, $schema never appears
Components, Webhooks allOf(Reference::schema('BaseEntity')) — a TypeError allOf(Schema::typeless()->ref(...)); Schema::object()->ref() emits a stray "type": "object" beside the $ref
Info & Metadata Tags referenced "by name string" tags() takes Tag objects, strings, or a mix
Request Bodies ->style(string) ->style() also takes the Style enum
Installation YAML needs symfony/yaml; toYaml() throws without it
README Info::create()->title(...)->version(...) Info::create('Example API', '1.0.0')

Also added: Operation::callback(), jsonSchemaDialect(), and Reference::example(), plus missing use statements in the Security and Webhooks samples. The quickstart's "Working with References" section is now the single place references are taught.

Left alone

  • discriminator and xml have no builder method. They are OpenAPI fields rather than JSON Schema ones. Documented as spreading a built schema into the array form, instead of hand-writing the whole array.
  • A mis-targeted reference still ships silently. ->response(200, Reference::schema('User')) type-checks, since every reference is the same class, and validate() passes it because pointers are never resolved. Catching that needs a reference-integrity pass over the finished document — separate work, and the same pass would close the dangling-$ref gap the new page has to document as a blind spot.
  • No $id-aware exception in the $schema strip. Unreachable through the builder, and it would survive mutation testing as dead code.

Verification

All 13 CI checks pass: PHP 8.4 and 8.5, prefer-lowest and prefer-stable, Linux and Windows, plus format, phpstan, type-coverage, and CodeQL.

Locally on PHP 8.4 with json-schema 2.0: 193 tests, PHPStan level 10, ECS and Rector clean, 100% type coverage, mutation score 83.46%. Every sample on the touched pages was executed against the package, and the README example builds and validates.

The $schema tests assert only that no nested URI survives, rather than asserting upstream's bug as a precondition — which is precisely what broke when json-schema 2.0 fixed items.

Open in Web Open in Cursor 

cursoragent and others added 6 commits September 16, 2026 08:00
Correct schema title embedding, version constructors, Tag|string
operation tags, and samples that passed Reference into JsonSchema
or Operation::responses().

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>
Drop keyword()/property() (not in cortexphp/json-schema 1.3.0).
Document that nested items() still emit $schema, and attach
discriminator/xml via raw array schema slots.

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>
Clarify that $ref composition uses Schema::object()->ref(), not a
static Schema::ref(). Correct README to describe bundled opis
meta-schema validation.

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>
Inline OpenAPI schemas must not carry the JSON Schema $schema URI, but the
rule was only applied to the schema handed to a slot. Subschemas that
cortexphp/json-schema serializes itself (items, and anything below it) still
emitted the URI, so Schema::array()->items(...) leaked it into the document.

Strip it at every depth, skipping maps whose keys are user-chosen names so a
property literally called $schema survives. Raw array schemas are left alone
as an escape hatch for declaring a dialect deliberately.

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>
Two feature cards linked to /openapi/validation-and-output, which was never
written. Document validate(), the structured errors() payload, what the
meta-schema does and does not catch, and the three output formats, instead of
pointing the cards somewhere else.

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>
The earlier pass explained failure modes ("cannot be passed to", "returns a
Reference, which responses() does not accept") where the reader only needs the
working pattern. Lead with that instead, drop the embedding caveats the nested
$schema fix made obsolete, and keep the quickstart free of asides.

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>
@cursor cursor Bot changed the title docs: align OpenAPI pages with current builder APIs Align the OpenAPI docs with the implementation Sep 16, 2026
…chema

JSON Schema 2020-12 core 8.1.1 allows $schema only at a schema resource root,
so every nested occurrence the builder can produce is invalid. items and
contains are the two call sites in cortexphp/json-schema 1.3.0 that emit it.

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>
cursoragent and others added 4 commits September 16, 2026 11:27
Reference::response() and Response::ref() return the same object; the target
class shortcut says which registry it points at without reading the pointer.
The equivalence is already spelled out in the quickstart cheat sheet.

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>
…fluent form (#23)

* feat: add Response::refTo() so referenced responses work in responses()

Operation::responses() keys each response from getStatusCode(), so the static
Response::ref() — a Reference, which has no status code — could not be passed
to it. Every other variadic takes Parameter|Reference because it builds a list
and derives no key; responses() is the only one that does.

refTo() sets the reference on the response itself, keeping the status code from
the named constructor, and serializes to the reference alone. Output is
byte-identical to ->response(404, Response::ref('NotFound')).

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>

* refactor!: build every reference through Reference::

The X::ref() shortcuts covered nine of the ten component registries and could
never cover the tenth: schemas are referenced through Reference::schema()
because Schema belongs to cortexphp/json-schema, which knows nothing about
#/components. A family that advertises uniformity while omitting the most-used
member invites calls like Schema::ref('User'), which cannot exist.

Drop the nine delegating statics. Reference:: already has a named constructor
for every registry and was the dominant spelling anyway.

With the static gone, Response can use the name for the fluent form, so the
refTo() added earlier on this branch becomes ref().

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>
json-schema 2.0 fixed the items leak but not contains, so the assertions that
documented upstream's behaviour as a precondition broke. Assert only that no
nested $schema survives, which holds whichever side strips it, and compare
whole arrays so the checks do not drill into mixed.

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>
@cursor cursor Bot changed the title Align the OpenAPI docs with the implementation Align the docs with the implementation, and centralise references on Reference:: Sep 16, 2026
@tymondesigns
tymondesigns marked this pull request as ready for review September 16, 2026 22:48
@tymondesigns
tymondesigns requested a balanced review from Copilot September 16, 2026 22:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Recursive schema sanitization can corrupt arbitrary values containing a $schema property.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Aligns documentation with the implemented API, centralizes component references, supports referenced responses, and sanitizes nested schemas.

Changes:

  • Replaces object-specific reference shortcuts with Reference::*.
  • Adds fluent referenced responses while retaining status codes.
  • Expands documentation and nested-schema handling.
File summaries
File Description
README.md Corrects validation and Info examples.
src/Concerns/BuildsArray.php Recursively removes nested $schema keys.
src/Objects/Callback.php Removes the callback reference shortcut.
src/Objects/Example.php Removes the example reference shortcut.
src/Objects/Header.php Removes the header reference shortcut.
src/Objects/Link.php Removes the link reference shortcut.
src/Objects/Parameter.php Removes the parameter reference shortcut.
src/Objects/PathItem.php Removes the path-item reference shortcut.
src/Objects/RequestBody.php Removes the request-body reference shortcut.
src/Objects/Response.php Adds status-preserving fluent references.
src/Objects/SecurityScheme.php Removes the security-scheme reference shortcut.
tests/Unit/Concerns/BuildsArrayTest.php Tests nested schema sanitization.
tests/Unit/Objects/CallbackTest.php Removes shortcut coverage.
tests/Unit/Objects/ExampleTest.php Removes shortcut coverage.
tests/Unit/Objects/HeaderTest.php Removes shortcut coverage.
tests/Unit/Objects/LinkTest.php Removes shortcut coverage.
tests/Unit/Objects/OperationTest.php Tests referenced responses and callbacks.
tests/Unit/Objects/ParameterTest.php Removes shortcut coverage.
tests/Unit/Objects/PathItemTest.php Removes shortcut coverage.
tests/Unit/Objects/RequestBodyTest.php Removes shortcut coverage.
tests/Unit/Objects/ResponseTest.php Tests fluent response references.
tests/Unit/Objects/SecuritySchemeTest.php Removes shortcut coverage.
docs/docs.json Adds the validation page to navigation.
docs/openapi/components.mdx Updates component reference guidance.
docs/openapi/info-and-metadata.mdx Clarifies operation tag inputs.
docs/openapi/installation.mdx Documents the optional YAML dependency.
docs/openapi/introduction.mdx Corrects schema and version guidance.
docs/openapi/paths-and-operations.mdx Documents tags and response attachment.
docs/openapi/quickstart.mdx Centralizes reference documentation.
docs/openapi/request-bodies.mdx Updates styles and references.
docs/openapi/responses.mdx Documents fluent referenced responses.
docs/openapi/security.mdx Adds a missing import.
docs/openapi/validation-and-output.mdx Adds validation and serialization guidance.
docs/openapi/webhooks-and-callbacks.mdx Corrects schema references and callback examples.
Review details

Suppressed comments (3)

tests/Unit/Objects/ResponseTest.php:94

  • The test description still names refTo() even though the exercised API is ref().
    tests/Unit/Objects/ResponseTest.php:98
  • Rename the stale refTo() reference to ref() so test output identifies the public method correctly.
    tests/Unit/Objects/ResponseTest.php:107
  • This test description refers to the discarded refTo() name rather than the implemented ref() method.
  • Files reviewed: 34/34 changed files
  • Comments generated: 2
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/Concerns/BuildsArray.php
Comment thread tests/Unit/Objects/ResponseTest.php Outdated
cursoragent and others added 2 commits September 16, 2026 22:55
Recursing into every nested array treated default/examples/const/enum as
schemas, so an instance payload like default(['$schema' => 'literal']) lost
that key. Limit recursion to JSON Schema applicator and definition keywords.

Also rename the Response tests that still said refTo().

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>
The Copilot-driven allowlist of every applicator created ~20 untested
RemoveArrayItem mutants and dropped mutation score to 79.6%, below the
CI gate. Skipping default, enum, and examples is the actual constraint:
those hold instance data. Everything else that is an array is walked as
a nested schema or a list of them.

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>
@tymondesigns
tymondesigns merged commit d9b31e0 into main Sep 16, 2026
13 checks passed
@tymondesigns
tymondesigns deleted the cursor/align-openapi-docs-6953 branch September 16, 2026 23:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants