Skip to content

feat: add ArrayShapeDescriber for precise array shape documentation#2696

Open
lacatoire wants to merge 12 commits intonelmio:5.xfrom
lacatoire:feat/array-shape-describer
Open

feat: add ArrayShapeDescriber for precise array shape documentation#2696
lacatoire wants to merge 12 commits intonelmio:5.xfrom
lacatoire:feat/array-shape-describer

Conversation

@lacatoire
Copy link
Copy Markdown
Contributor

@lacatoire lacatoire commented Feb 20, 2026

Summary

  • Add a dedicated ArrayShapeDescriber for Symfony's ArrayShapeType (7.3+) that generates OpenAPI object schemas with explicit properties, required fields, and additionalProperties based on the array shape definition
  • Previously, ArrayShapeType was handled by DictionaryDescriber (since it extends CollectionType), producing only a generic object with additionalProperties — losing all shape information (property names, types, optionality)
  • Exclude ArrayShapeType from DictionaryDescriber and ListDescriber to prevent incorrect handling
  • Conditionally register the service only when ArrayShapeType class exists (Symfony 7.3+)

Example

For array{name: string, age: int, email?: string}, the describer now generates:

{
    "type": "object",
    "required": ["name", "age"],
    "properties": {
        "name": { "type": "string" },
        "age": { "type": "integer" },
        "email": { "type": "string" }
    },
    "additionalProperties": false
}

Closes #2675

Add a dedicated TypeDescriber for Symfony's ArrayShapeType (7.3+) that
generates OpenAPI object schemas with explicit properties, required
fields, and additionalProperties based on the array shape definition.

Previously, ArrayShapeType was handled by DictionaryDescriber (as it
extends CollectionType), which only produced a generic object with
additionalProperties, losing all shape information.

- Add ArrayShapeDescriber with support for sealed/unsealed shapes
- Exclude ArrayShapeType from DictionaryDescriber and ListDescriber
- Conditionally register service only when ArrayShapeType class exists
- Add functional test with fixture controller and entity

Closes nelmio#2675
@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 20, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.70%. Comparing base (212d490) to head (1f7a3e8).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##              5.x    #2696      +/-   ##
==========================================
+ Coverage   95.68%   95.70%   +0.02%     
==========================================
  Files          94       95       +1     
  Lines        3079     3097      +18     
==========================================
+ Hits         2946     2964      +18     
  Misses        133      133              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI review requested due to automatic review settings April 10, 2026 18:06
Auto-generated fixture validating the OpenAPI output of the
ArrayShapeDescriber for sealed and unsealed array shapes.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds first-class OpenAPI schema generation for Symfony TypeInfo ArrayShapeType (Symfony 7.3+), so array-shape PHPDoc produces explicit object properties/required fields instead of a generic dictionary schema.

Changes:

  • Introduce ArrayShapeDescriber to convert ArrayShapeType into an OpenAPI object schema with properties, required, and additionalProperties.
  • Exclude ArrayShapeType from DictionaryDescriber and ListDescriber so it isn’t described as a generic collection.
  • Add functional coverage (controller + entity + conditional test case) and conditionally register the describer service when ArrayShapeType exists.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/Functional/Entity/ArrayShapeTypes.php Adds an entity with array-shape PHPDoc to drive functional schema generation.
tests/Functional/ControllerTest.php Adds a conditional functional test case for array-shape handling on Symfony 7.3+.
tests/Functional/Controller/ArrayShapeController.php Adds a functional endpoint that returns a model using array-shape properties.
src/TypeDescriber/ListDescriber.php Attempts to prevent list handling from matching ArrayShapeType.
src/TypeDescriber/DictionaryDescriber.php Attempts to prevent dictionary handling from matching ArrayShapeType.
src/TypeDescriber/ArrayShapeDescriber.php New describer implementing precise OpenAPI schema generation for ArrayShapeType.
phpstan-baseline.neon Adds new PHPStan baseline entries related to ArrayShapeType availability/typing.
config/services.php Conditionally registers the new describer service when ArrayShapeType is present.

public function supports(Type $type, array $context = []): bool
{
return $type instanceof CollectionType
&& !$type instanceof Type\ArrayShapeType
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

The instanceof Type\ArrayShapeType check will throw a runtime Error on Symfony versions where Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType doesn't exist (because instanceof against a missing class is fatal). Use a safe check (e.g., class_exists(...) && is_a($type, 'Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType')) or compare against a string class name to avoid hard-referencing a non-existent class.

Suggested change
&& !$type instanceof Type\ArrayShapeType
&& !(class_exists(Type\ArrayShapeType::class)
&& is_a($type, Type\ArrayShapeType::class))

Copilot uses AI. Check for mistakes.
public function supports(Type $type, array $context = []): bool
{
return $type instanceof CollectionType
&& !$type instanceof Type\ArrayShapeType
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

The instanceof Type\ArrayShapeType exclusion is a runtime-fatal check when the ArrayShapeType class isn't present (Symfony < 7.3). Replace it with a guarded class_exists + is_a(...) check, or use the fully-qualified class name string, so the describer remains compatible across supported Symfony versions.

Suggested change
&& !$type instanceof Type\ArrayShapeType
&& (
!class_exists(Type\ArrayShapeType::class)
|| !is_a($type, Type\ArrayShapeType::class)
)

Copilot uses AI. Check for mistakes.
Comment thread phpstan-baseline.neon
Comment on lines +96 to +107

-
message: '#^Class Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType not found\.$#'
identifier: class.notFound
count: 1
path: src/TypeDescriber/DictionaryDescriber.php

-
message: '#^Class Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType not found\.$#'
identifier: class.notFound
count: 1
path: src/TypeDescriber/ListDescriber.php
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

The new baseline entries for DictionaryDescriber/ListDescriber are currently required only because the code hard-references Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType. Once the describers use a safe string-based/guarded check, these ignores should be removed to avoid permanently masking real analysis issues in those files.

Suggested change
-
message: '#^Class Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType not found\.$#'
identifier: class.notFound
count: 1
path: src/TypeDescriber/DictionaryDescriber.php
-
message: '#^Class Symfony\\Component\\TypeInfo\\Type\\ArrayShapeType not found\.$#'
identifier: class.notFound
count: 1
path: src/TypeDescriber/ListDescriber.php

Copilot uses AI. Check for mistakes.
- Remove `?? false` fallback on `optional` key since
  ArrayShapeType::getShape() guarantees its presence
- Add test cases for nested shapes, numeric keys, and nullable values
- Regenerate fixture with new test entities
…ne entry

- Use 'symfony-7.3' fixture suffix for Symfony 7.3 where the unsealed
  array shape extra value type is resolved differently (oneOf vs nullable)
- Remove stale LegacyTypeConverter baseline entry that was erroneously
  added and doesn't exist on 5.x
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.

[Feature]: ArrayShape describer

3 participants