Skip to content

Commit 835adb7

Browse files
committed
Map array<K, V> to type: object + additionalProperties (#2001)
Distinguish between ordered lists and key-value maps in TypeInfoTypeResolver by checking CollectionType::isList() and keyType. Types with explicit key types (e.g. `array<string, string>`) now produce `type: object` + `additionalProperties` instead of `type: array` + `items`.
1 parent 2cc7177 commit 835adb7

3 files changed

Lines changed: 52 additions & 18 deletions

File tree

src/Type/TypeInfoTypeResolver.php

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -148,19 +148,37 @@ protected function setSchemaType(OA\Schema $schema, Type $type, Analysis $analys
148148
} elseif ($type instanceof ExplicitType) {
149149
$schema->type = $type->getTypeIdentifier()->value;
150150
} elseif ($type instanceof CollectionType) {
151-
$schema->type = 'array';
152-
153-
if (Generator::isDefault($schema->items)) {
154-
$schema->items = new OA\Items(['_context' => new Context(['generated' => true], $schema->_context)]);
155-
$this->setSchemaType($schema->items, $type->getCollectionValueType(), $analysis);
156-
$this->type2ref($schema->items, $analysis);
157-
$analysis->addAnnotation($schema->items, $schema->items->_context);
158-
} elseif (Generator::isDefault($schema->items->type, $schema->items->oneOf, $schema->items->allOf, $schema->items->anyOf)) {
159-
$this->setSchemaType($schema->items, $type->getCollectionValueType(), $analysis);
160-
$this->type2ref($schema->items, $analysis);
161-
}
151+
if ($type->isList() || $type->getCollectionKeyType() instanceof UnionType) {
152+
// list<T>, array<T>, T[] → ordered list
153+
$schema->type = 'array';
154+
155+
if (Generator::isDefault($schema->items)) {
156+
$schema->items = new OA\Items(['_context' => new Context(['generated' => true], $schema->_context)]);
157+
$this->setSchemaType($schema->items, $type->getCollectionValueType(), $analysis);
158+
$this->type2ref($schema->items, $analysis);
159+
$analysis->addAnnotation($schema->items, $schema->items->_context);
160+
} elseif (Generator::isDefault($schema->items->type, $schema->items->oneOf, $schema->items->allOf, $schema->items->anyOf)) {
161+
$this->setSchemaType($schema->items, $type->getCollectionValueType(), $analysis);
162+
$this->type2ref($schema->items, $analysis);
163+
}
162164

163-
$this->mapNativeType($schema->items, $schema->items->type);
165+
$this->mapNativeType($schema->items, $schema->items->type);
166+
} else {
167+
// explicit key type (e.g. array<string, string>) → map
168+
$schema->type = 'object';
169+
170+
if (Generator::isDefault($schema->additionalProperties)) {
171+
$schema->additionalProperties = new OA\AdditionalProperties(['_context' => new Context(['generated' => true], $schema->_context)]);
172+
$this->setSchemaType($schema->additionalProperties, $type->getCollectionValueType(), $analysis);
173+
$this->type2ref($schema->additionalProperties, $analysis);
174+
$analysis->addAnnotation($schema->additionalProperties, $schema->additionalProperties->_context);
175+
} elseif (Generator::isDefault($schema->additionalProperties->type, $schema->additionalProperties->oneOf, $schema->additionalProperties->allOf, $schema->additionalProperties->anyOf)) {
176+
$this->setSchemaType($schema->additionalProperties, $type->getCollectionValueType(), $analysis);
177+
$this->type2ref($schema->additionalProperties, $analysis);
178+
}
179+
180+
$this->mapNativeType($schema->additionalProperties, $schema->additionalProperties->type);
181+
}
164182
}
165183
}
166184

tests/Fixtures/PHP/DocblockAndTypehintTypes.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,4 +234,16 @@ public function blah(
234234
new OAT\Schema(type: 'string'),
235235
]))]
236236
public array $nestedOneOfWithItems;
237+
238+
/**
239+
* @var array<string, string>
240+
*/
241+
#[OAT\Property]
242+
public array $stringMap;
243+
244+
/**
245+
* @var array<int, string>
246+
*/
247+
#[OAT\Property]
248+
public array $intKeyedMap;
237249
}

tests/Type/TypeResolverTest.php

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ public static function resolverAugmentCases(): iterable
4343
'intrange' => '{ "type": "integer", "maximum": 10, "minimum": -9223372036854775808, "property": "intRange" }',
4444
'positiveint' => '{ "type": "integer", "maximum": 9223372036854775807, "minimum": 1, "property": "positiveInt" }',
4545
'nonzeroint' => '{ "type": "integer", "not": { "enum": [ 0 ] }, "property": "nonZeroInt" }',
46-
'arrayshape' => '{ "type": "array", "items": { "type": "boolean" }, "property": "arrayShape" }',
47-
'uniontype' => '{ "property": "unionType" }',
46+
'legacy:arrayshape' => '{ "type": "array", "items": { "type": "boolean" }, "property": "arrayShape" }',
47+
'type-info:arrayshape' => '{ "type": "object", "additionalProperties": { "type": "boolean" }, "property": "arrayShape" }',
4848
'promotedstring' => '{ "type": "string", "property": "promotedString" }',
4949
'legacy:mixedunion' => '{ "example": "My value", "property": "mixedUnion" }',
5050
'type-info:mixedunion' => '{ "example": "My value", "oneOf": [ { "type": "string" }, { "type": "array", "items": { "type": "mixed" } } ], "property": "mixedUnion" }',
@@ -58,14 +58,16 @@ public static function resolverAugmentCases(): iterable
5858
'legacy:nullabletypedlistunion' => '{ "nullable": true, "property": "nullableTypedListUnion" }',
5959
'type-info:nullabletypedlistunion' => '{ "nullable": true, "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } } ], "property": "nullableTypedListUnion" }',
6060
'legacy:nullablenestedtypedlistunion' => '{ "nullable": true, "property": "nullableNestedTypedListUnion" }',
61-
'type-info:nullablenestedtypedlistunion' => '{ "nullable": true, "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } }, { "type": "array", "items": { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } } } ], "property": "nullableNestedTypedListUnion" }',
61+
'type-info:nullablenestedtypedlistunion' => '{ "nullable": true, "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } }, { "type": "array", "items": { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } } } ], "property": "nullableNestedTypedListUnion" }',
6262
'reflectionvalue' => '{ "example": true, "nullable": true, "property": "reflectionValue" }',
6363
'legacy:intersectionvar' => '{ "property": "intersectionVar" }',
6464
'type-info:intersectionvar' => '{ "allOf": [ { "$ref": "#/components/schemas/FirstInterface" }, { "$ref": "#/components/schemas/SecondInterface" } ], "property": "intersectionVar" }',
6565
'legacy:nestedoneof' => '{ "property": "nestedOneOf" }',
6666
'type-info:nestedoneof' => '{ "oneOf": [ { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } }, { "type": "array", "items": { "type": "string" } } ], "property": "nestedOneOf" }',
6767
'legacy:nestedoneofwithitems' => '{ "type": "array", "items": { "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "string" } ] }, "property": "nestedOneOfWithItems" }',
6868
'type-info:nestedoneofwithitems' => '{ "type": "array", "items": { "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "string" } ] }, "property": "nestedOneOfWithItems" }',
69+
'type-info:stringmap' => '{ "type": "object", "additionalProperties": { "type": "string" }, "property": "stringMap" }',
70+
'type-info:intkeyedmap' => '{ "type": "object", "additionalProperties": { "type": "string" }, "property": "intKeyedMap" }',
6971
],
7072
OA\OpenApi::VERSION_3_1_0 => [
7173
'nothing' => '{ "property": "nothing" }',
@@ -87,8 +89,8 @@ public static function resolverAugmentCases(): iterable
8789
'intrange' => '{ "type": "integer", "maximum": 10, "minimum": -9223372036854775808, "property": "intRange" }',
8890
'positiveint' => '{ "type": "integer", "maximum": 9223372036854775807, "minimum": 1, "property": "positiveInt" }',
8991
'nonzeroint' => '{ "type": "integer", "not": { "const": 0 }, "property": "nonZeroInt" } ',
90-
'arrayshape' => '{ "type": "array", "items": { "type": "boolean" }, "property": "arrayShape" }',
91-
'legacy:uniontype' => '{ "property": "unionType" }',
92+
'legacy:arrayshape' => '{ "type": "array", "items": { "type": "boolean" }, "property": "arrayShape" }',
93+
'type-info:arrayshape' => '{ "type": "object", "additionalProperties": { "type": "boolean" }, "property": "arrayShape" }',
9294
'type-info:uniontype' => '{ "type": [ "integer", "string" ], "property": "unionType" }',
9395
'promotedstring' => '{ "type": "string", "property": "promotedString" }',
9496
'legacy:mixedunion' => '{ "example": "My value", "property": "mixedUnion" }',
@@ -103,7 +105,7 @@ public static function resolverAugmentCases(): iterable
103105
'legacy:nullabletypedlistunion' => '{ "property": "nullableTypedListUnion" }',
104106
'type-info:nullabletypedlistunion' => '{ "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } }, { "type": "null" } ], "property": "nullableTypedListUnion" }',
105107
'legacy:nullablenestedtypedlistunion' => '{ "property": "nullableNestedTypedListUnion" }',
106-
'type-info:nullablenestedtypedlistunion' => '{ "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } }, { "type": "array", "items": { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } } }, { "type": "null" } ], "property": "nullableNestedTypedListUnion" }',
108+
'type-info:nullablenestedtypedlistunion' => '{ "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } }, { "type": "array", "items": { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } } }, { "type": "null" } ], "property": "nullableNestedTypedListUnion" }',
107109
'legacy:reflectionvalue' => '{ "example": true, "property": "reflectionValue" }',
108110
'type-info:reflectionvalue' => '{ "type": [ "boolean", "integer", "null" ], "example": true, "property": "reflectionValue" }',
109111
'legacy:intersectionvar' => '{ "property": "intersectionVar" }',
@@ -112,6 +114,8 @@ public static function resolverAugmentCases(): iterable
112114
'type-info:nestedoneof' => '{ "oneOf": [ { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } }, { "type": "array", "items": { "type": "string" } } ], "property": "nestedOneOf" }',
113115
'legacy:nestedoneofwithitems' => '{ "type": "array", "items": { "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "string" } ] }, "property": "nestedOneOfWithItems" }',
114116
'type-info:nestedoneofwithitems' => '{ "type": "array", "items": { "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "string" } ] }, "property": "nestedOneOfWithItems" }',
117+
'type-info:stringmap' => '{ "type": "object", "additionalProperties": { "type": "string" }, "property": "stringMap" }',
118+
'type-info:intkeyedmap' => '{ "type": "object", "additionalProperties": { "type": "string" }, "property": "intKeyedMap" }',
115119
],
116120
];
117121

0 commit comments

Comments
 (0)