fix(openapi): emit Draft 4 boolean exclusive bounds for spec 3.0.0#8222
Merged
soyuka merged 4 commits intoJun 2, 2026
Merged
Conversation
OpenAPI 3.0 inherits JSON Schema Draft 4 semantics where `exclusiveMinimum` / `exclusiveMaximum` MUST be booleans qualifying `minimum` / `maximum`. The runtime SchemaFactory emits the JSON Schema 2020-12 form with these keywords as numbers, so `api:openapi:export --spec-version=3.0.0` and `GET /docs.jsonopenapi?spec_version=3.0.0` produced invalid 3.0 documents for properties with Assert\Positive, Assert\GreaterThan, Assert\LessThan, or Assert\Range. BackwardCompatibleSchemaFactory was supposed to handle the conversion (introduced in api-platform#6098 closing api-platform#6041) but only fires when the serializer context contains draft_4 => true, a flag set today only by ApiTestAssertionsTrait. OpenApiFactory's buildSchema calls passed serializerContext: null. Wire spec_version === '3.0.0' from OpenApiFactory::__invoke into the schema serializer context for all three buildSchema call sites (output, input, error responses). Thread the context as a new optional argument on addOperationErrors(). Also guard BackwardCompatibleSchemaFactory against re-running on schemas where exclusiveMinimum/exclusiveMaximum is already boolean — the same property ArrayObject can be reached through multiple definitions (one per format), and the previous unconditional swap turned `{minimum: 10, exclusiveMinimum: true}` into `{minimum: true, exclusiveMinimum: true}` on the second pass. Closes api-platform#7936
2f8b4c1 to
e42501b
Compare
api:openapi:export --spec-version=3.0.0 passed spec_version only to the normalizer, not to OpenApiFactory::__invoke. collectPaths therefore never set the BackwardCompatibleSchemaFactory::SCHEMA_DRAFT4_VERSION flag on the schema serializer context, so the CLI emitted JSON Schema 2020-12 numeric exclusiveMinimum/exclusiveMaximum for properties with Assert\Positive, Assert\GreaterThan, Assert\LessThan, or Assert\Range even when 3.0.0 was requested. HTTP path was unaffected because SerializerContextBuilder already injects spec_version into the OpenApiProvider context. Closes api-platform#8176
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
OpenAPI 3.0 inherits JSON Schema Draft 4 semantics where
exclusiveMinimum/exclusiveMaximumMUST be booleans qualifyingminimum/maximum. The runtime SchemaFactory emits the JSON Schema 2020-12 form (these keywords as numbers), soapi:openapi:export --spec-version=3.0.0andGET /docs.jsonopenapi?spec_version=3.0.0produced invalid 3.0 documents for any property withAssert\Positive,Assert\GreaterThan,Assert\LessThan, orAssert\Rangewith strict bounds.BackwardCompatibleSchemaFactory(introduced in #6098 closing #6041) was supposed to handle the conversion but only fires when the serializer context containsdraft_4 => true— a flag set today only byApiTestAssertionsTrait.OpenApiFactory'sbuildSchemacall sites passedserializerContext: null, so the BC conversion never activated in the export / HTTP paths.Changes
OpenApiFactory— derive a schema serializer context from$context['spec_version']once percollectPaths()call and thread it through everybuildSchema()invocation (output, input, error responses).addOperationErrors()gains an optional$serializerContextparameter for the error schemas.BackwardCompatibleSchemaFactory— guard the swap against schemas whereexclusiveMinimum/exclusiveMaximumis already boolean. The same propertyArrayObjectinstance is reachable through multiple definitions (one per supported format), and the previous unconditional rewrite turned{minimum: 10, exclusiveMinimum: true}into{minimum: true, exclusiveMinimum: true}on the second pass.OpenApiCommand— thread--spec-versioninto theOpenApiFactory::__invokecontext, not only into the normalizer. Before this,collectPaths()never saw the requested spec version on the CLI path, so the Draft 4 conversion above never activated forapi:openapi:export --spec-version=3.0.0. The HTTP path was unaffected becauseSerializerContextBuilderalready injectsspec_versioninto theOpenApiProvidercontext.Observed vs expected
Before:
After:
Closes #7936
Closes #8176
Test plan
OpenApiTest::testOpenApi30EmitsBooleanExclusiveBoundsForNumericConstraintsexercises/docs.jsonopenapi?spec_version=3.0.0against the existingNumericValidatedfixture (Assert\GreaterThan,Assert\LessThan,Assert\Positive); fails on the base branch (exclusiveMinimum: 10number / corruption tominimum: true), green after the fix.OpenApiCommandTest::testSpecVersion30EmitsDraft4BooleanExclusiveBoundsexercisesapi:openapi:export --spec-version=3.0.0against the same fixture; fails without theOpenApiCommandplumb (exclusiveMinimum: 10), green after the fix.BackwardCompatibleSchemaFactoryTest(existing) — still 4/4 green.tests/Functional/OpenApiTest— 21/21 green.tests/OpenApi/Command/OpenApiCommandTest— 6/6 green.ApiTestCaseTest::testAssertMatchesResourceItemAndCollectionJsonSchemaOutputWithRangeAssertions— still green; idempotency guard does not affect the existing draft-4 test trait path.OpenApiFactoryTest::testInvokefailure is pre-existing onupstream/4.3('Unprocessable entity'vs'An error occurred'— unrelated 422 description drift).