Skip to content

fix(openapi): emit Draft 4 boolean exclusive bounds for spec 3.0.0#8222

Merged
soyuka merged 4 commits into
api-platform:4.3from
soyuka:fix/7936-openapi-3.0-draft4-exclusive
Jun 2, 2026
Merged

fix(openapi): emit Draft 4 boolean exclusive bounds for spec 3.0.0#8222
soyuka merged 4 commits into
api-platform:4.3from
soyuka:fix/7936-openapi-3.0-draft4-exclusive

Conversation

@soyuka

@soyuka soyuka commented Jun 2, 2026

Copy link
Copy Markdown
Member

Summary

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 (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 any property with Assert\Positive, Assert\GreaterThan, Assert\LessThan, or Assert\Range with strict bounds.

BackwardCompatibleSchemaFactory (introduced in #6098 closing #6041) was supposed to handle the conversion but only fires when the serializer context contains draft_4 => true — a flag set today only by ApiTestAssertionsTrait. OpenApiFactory's buildSchema call sites passed serializerContext: null, so the BC conversion never activated in the export / HTTP paths.

Changes

  1. OpenApiFactory — derive a schema serializer context from $context['spec_version'] once per collectPaths() call and thread it through every buildSchema() invocation (output, input, error responses). addOperationErrors() gains an optional $serializerContext parameter for the error schemas.

  2. BackwardCompatibleSchemaFactory — guard the swap against schemas where exclusiveMinimum / exclusiveMaximum is already boolean. The same property ArrayObject instance 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.

  3. OpenApiCommand — thread --spec-version into the OpenApiFactory::__invoke context, 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 for api:openapi:export --spec-version=3.0.0. The HTTP path was unaffected because SerializerContextBuilder already injects spec_version into the OpenApiProvider context.

Observed vs expected

Before:

"positive": { "minimum": 0, "exclusiveMinimum": 0, "type": "integer" }

After:

"positive": { "minimum": 0, "exclusiveMinimum": true, "type": "integer" }

Closes #7936
Closes #8176

Test plan

  • New functional test OpenApiTest::testOpenApi30EmitsBooleanExclusiveBoundsForNumericConstraints exercises /docs.jsonopenapi?spec_version=3.0.0 against the existing NumericValidated fixture (Assert\GreaterThan, Assert\LessThan, Assert\Positive); fails on the base branch (exclusiveMinimum: 10 number / corruption to minimum: true), green after the fix.
  • New OpenApiCommandTest::testSpecVersion30EmitsDraft4BooleanExclusiveBounds exercises api:openapi:export --spec-version=3.0.0 against the same fixture; fails without the OpenApiCommand plumb (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::testInvoke failure is pre-existing on upstream/4.3 ('Unprocessable entity' vs 'An error occurred' — unrelated 422 description drift).

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
@soyuka soyuka force-pushed the fix/7936-openapi-3.0-draft4-exclusive branch from 2f8b4c1 to e42501b Compare June 2, 2026 13:55
soyuka added 3 commits June 2, 2026 15:59
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
@soyuka soyuka merged commit abef010 into api-platform:4.3 Jun 2, 2026
107 of 108 checks passed
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.

1 participant