Skip to content

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

@soyuka

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions