OpenAPI: Non-JSON formats should not generate JSON schemas · Issue #7802 · api-platform/core · GitHub
Skip to content

OpenAPI: Non-JSON formats should not generate JSON schemas #7802

Description

@soyuka

Issue Description

When an API Platform operation defines non-JSON output formats (e.g., text/html, text/xml), the OpenAPI factory generates JSON schemas for these formats, which is semantically incorrect.

Example

#[ApiResource(
    operations: [
        new Get(
            uriTemplate: '/unsubscribe/{token}',
            formats: [
                'jsonld' => ['application/ld+json'],
                'json' => ['application/json'],
                'html' => ['text/html'],  // This causes the issue
            ],
        ),
    ],
)]
class UnsubscribedEmail {}

Current Behavior

The OpenAPI export generates:

paths:
  /api/unsubscribe/{token}:
    get:
      responses:
        '200':
          content:
            application/ld+json:
              schema:
                $ref: '#/components/schemas/UnsubscribedEmail.jsonld'
            application/json:
              schema:
                $ref: '#/components/schemas/UnsubscribedEmail'
            text/html:
              schema:
                $ref: '#/components/schemas/UnsubscribedEmail.html'  # Makes no sense

components:
  schemas:
    UnsubscribedEmail.html:  # JSON schema for HTML format - meaningless
      type: object
      properties:
        status:
          type: string

Expected Behavior

Non-JSON formats should either:

  1. Not have a schema reference in OpenAPI
  2. Or be excluded from the OpenAPI spec entirely (content negotiation still works at runtime)

Root Cause

In src/OpenApi/Factory/OpenApiFactory.php, the getMimeTypes() method returns ALL output formats, and the schema generation loop creates schemas for each:

// Lines 267-273
foreach ($responseMimeTypes as $operationFormat) {
    $operationOutputSchema = $this->jsonSchemaFactory->buildSchema(
        $resourceClass,
        $operationFormat,  // <-- Includes 'html', 'xml', etc.
        Schema::TYPE_OUTPUT,
        $operation,
        $schema,
        null,
        $forceSchemaCollection
    );
    $operationOutputSchemas[$operationFormat] = $operationOutputSchema;
}

Proposed Fix

Use the existing jsonschema_formats configuration parameter to filter which formats get JSON schemas in OpenAPI.

Step 1: Inject jsonschema_formats into OpenApiFactory

In src/Symfony/Bundle/Resources/config/openapi.php:

$services->set('api_platform.openapi.factory', OpenApiFactory::class)
    ->args([
        // ... existing args ...
        '%api_platform.jsonschema_formats%',  // Add this parameter
    ]);

Step 2: Update OpenApiFactory constructor

public function __construct(
    // ... existing parameters ...
    private readonly array $jsonSchemaFormats = [],
) {
    // ...
}

Step 3: Filter formats in schema generation loop

// Around line 267
foreach ($responseMimeTypes as $mimeType => $operationFormat) {
    // Skip formats that don't have JSON schema support
    if (!isset($this->jsonSchemaFormats[$operationFormat])) {
        continue;
    }

    $operationOutputSchema = $this->jsonSchemaFactory->buildSchema(
        $resourceClass,
        $operationFormat,
        Schema::TYPE_OUTPUT,
        $operation,
        $schema,
        null,
        $forceSchemaCollection
    );
    $operationOutputSchemas[$operationFormat] = $operationOutputSchema;
}

Step 4: Update buildContent to handle missing schemas

private function buildContent(array $responseMimeTypes, array $operationSchemas): \ArrayObject
{
    $content = new \ArrayObject();

    foreach ($responseMimeTypes as $mimeType => $format) {
        // Only add content entry if we have a schema for this format
        if (isset($operationSchemas[$format])) {
            $content[$mimeType] = new MediaType(
                schema: new \ArrayObject($operationSchemas[$format]->getArrayCopy(false))
            );
        }
        // Non-JSON formats without schemas are simply not included in OpenAPI content
    }

    return $content;
}

Alternative Consideration

The buildContent method could instead add the content type without a schema reference:

if (isset($operationSchemas[$format])) {
    $content[$mimeType] = new MediaType(schema: new \ArrayObject($operationSchemas[$format]->getArrayCopy(false)));
} else {
    // Include the content type but without a schema (valid in OpenAPI 3.1)
    $content[$mimeType] = new MediaType();
}

This preserves the information that the endpoint supports HTML responses, just without a JSON schema.

Testing

Add a test case to tests/OpenApi/Factory/OpenApiFactoryTest.php:

public function testNonJsonFormatsDoNotGenerateSchemas(): void
{
    // Create a resource with html format
    // Assert that UnsubscribedEmail.html schema does not exist
    // Assert that text/html content type either has no schema or is not present
}

Configuration Reference

The jsonschema_formats is already computed in ApiPlatformExtension.php:

$jsonSchemaFormats = $config['jsonschema_formats'];

if (!$jsonSchemaFormats) {
    foreach (array_merge(array_keys($formats), array_keys($errorFormats)) as $f) {
        // Only JSON-based formats get schemas by default
        if (str_starts_with($f, 'json')) {
            $jsonSchemaFormats[$f] = true;
        }
    }
}

This logic already correctly identifies JSON formats - it just needs to be used in OpenApiFactory.

Activity

  1. added theissue type on May 29, 2026
  2. Amoifr commented on Sep 24, 2026

    @Amoifr
    Contributor

    Before picking this up: step 1 relies on %api_platform.jsonschema_formats%, but since 79edced ("share invariable sub-schemas") the extension sets that parameter to [] unconditionally, and the formats computed just above it are no longer used anywhere. So injecting it as-is would filter out every format, JSON ones included, and the api_platform.jsonschema_formats config option currently has no effect.

    Would you rather restore the computed value into the parameter (which would also make the option meaningful again), or filter the formats in OpenApiFactory some other way? Happy to send the PR either way. 🙂

  3. dunglas commented on Sep 25, 2026

    @dunglas
    Member

    XML is supported by OpenAPI. Maybe should we improve our support to allow describing XML APIs?

    https://swagger.io/docs/specification/v3_0/data-models/representing-xml/

  4. Amoifr commented on Sep 27, 2026

    @Amoifr
    Contributor

    Good point, and it actually narrows this one down nicely: an OpenAPI Schema Object is perfectly valid for application/xml, it just needs the xml hints (name, attribute, wrapped) to describe the payload accurately.

    So I'd split it in two:

    1. this issue: drop the schema only for formats with no structured representation (text/html and friends), and leave XML alone;
    2. a follow-up for proper XML description (xml.name from the short name, wrapped for collections, attributes), which is a feature of its own.

    Does that split work for you? For part 1, the question about jsonschema_formats above still stands. 🙂

  5. soyuka commented on Sep 29, 2026

    @soyuka
    MemberAuthor

    Maybe we can widen to formats instead of jsonschema_formats ?

  6. Amoifr commented on Sep 30, 2026

    @Amoifr
    Contributor

    Makes sense, and it would even be free: OpenApiFactory already receives %api_platform.formats%, it just never uses it.

    One catch though: by default formats only holds jsonld, so filtering the operation formats against it would also drop the schema of the json in the example above, since that one is only declared on the operation. Anyone who sets formats per operation would lose schemas they have today.

    Two ways around it that I can see:

    1. filter against the global formats anyway, and document that a format needs to be declared there to get a schema;
    2. decide per operation format instead: keep the schema for the JSON-based ones (the json* naming the extension already relies on) and for xml, and drop it for the rest (html, csv, …).

    I'd lean towards 2, since it doesn't change anything for existing setups except the html case, but happy to go either way. Should it target 4.4 as a fix, or main?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions