diff --git a/src/Type/TypeInfoTypeResolver.php b/src/Type/TypeInfoTypeResolver.php index b5d4526de..e22756998 100644 --- a/src/Type/TypeInfoTypeResolver.php +++ b/src/Type/TypeInfoTypeResolver.php @@ -148,19 +148,37 @@ protected function setSchemaType(OA\Schema $schema, Type $type, Analysis $analys } elseif ($type instanceof ExplicitType) { $schema->type = $type->getTypeIdentifier()->value; } elseif ($type instanceof CollectionType) { - $schema->type = 'array'; - - if (Generator::isDefault($schema->items)) { - $schema->items = new OA\Items(['_context' => new Context(['generated' => true], $schema->_context)]); - $this->setSchemaType($schema->items, $type->getCollectionValueType(), $analysis); - $this->type2ref($schema->items, $analysis); - $analysis->addAnnotation($schema->items, $schema->items->_context); - } elseif (Generator::isDefault($schema->items->type, $schema->items->oneOf, $schema->items->allOf, $schema->items->anyOf)) { - $this->setSchemaType($schema->items, $type->getCollectionValueType(), $analysis); - $this->type2ref($schema->items, $analysis); - } + if ($type->isList() || $type->getCollectionKeyType() instanceof UnionType) { + // list, array, T[] → ordered list + $schema->type = 'array'; + + if (Generator::isDefault($schema->items)) { + $schema->items = new OA\Items(['_context' => new Context(['generated' => true], $schema->_context)]); + $this->setSchemaType($schema->items, $type->getCollectionValueType(), $analysis); + $this->type2ref($schema->items, $analysis); + $analysis->addAnnotation($schema->items, $schema->items->_context); + } elseif (Generator::isDefault($schema->items->type, $schema->items->oneOf, $schema->items->allOf, $schema->items->anyOf)) { + $this->setSchemaType($schema->items, $type->getCollectionValueType(), $analysis); + $this->type2ref($schema->items, $analysis); + } - $this->mapNativeType($schema->items, $schema->items->type); + $this->mapNativeType($schema->items, $schema->items->type); + } else { + // explicit key type (e.g. array) → map + $schema->type = 'object'; + + if (Generator::isDefault($schema->additionalProperties)) { + $schema->additionalProperties = new OA\AdditionalProperties(['_context' => new Context(['generated' => true], $schema->_context)]); + $this->setSchemaType($schema->additionalProperties, $type->getCollectionValueType(), $analysis); + $this->type2ref($schema->additionalProperties, $analysis); + $analysis->addAnnotation($schema->additionalProperties, $schema->additionalProperties->_context); + } elseif (Generator::isDefault($schema->additionalProperties->type, $schema->additionalProperties->oneOf, $schema->additionalProperties->allOf, $schema->additionalProperties->anyOf)) { + $this->setSchemaType($schema->additionalProperties, $type->getCollectionValueType(), $analysis); + $this->type2ref($schema->additionalProperties, $analysis); + } + + $this->mapNativeType($schema->additionalProperties, $schema->additionalProperties->type); + } } } diff --git a/tests/Fixtures/PHP/DocblockAndTypehintTypes.php b/tests/Fixtures/PHP/DocblockAndTypehintTypes.php index afd5e7671..da44e0f34 100644 --- a/tests/Fixtures/PHP/DocblockAndTypehintTypes.php +++ b/tests/Fixtures/PHP/DocblockAndTypehintTypes.php @@ -234,4 +234,16 @@ public function blah( new OAT\Schema(type: 'string'), ]))] public array $nestedOneOfWithItems; + + /** + * @var array + */ + #[OAT\Property] + public array $stringMap; + + /** + * @var array + */ + #[OAT\Property] + public array $intKeyedMap; } diff --git a/tests/Type/TypeResolverTest.php b/tests/Type/TypeResolverTest.php index 136939fe5..5a17689d2 100644 --- a/tests/Type/TypeResolverTest.php +++ b/tests/Type/TypeResolverTest.php @@ -43,8 +43,8 @@ public static function resolverAugmentCases(): iterable 'intrange' => '{ "type": "integer", "maximum": 10, "minimum": -9223372036854775808, "property": "intRange" }', 'positiveint' => '{ "type": "integer", "maximum": 9223372036854775807, "minimum": 1, "property": "positiveInt" }', 'nonzeroint' => '{ "type": "integer", "not": { "enum": [ 0 ] }, "property": "nonZeroInt" }', - 'arrayshape' => '{ "type": "array", "items": { "type": "boolean" }, "property": "arrayShape" }', - 'uniontype' => '{ "property": "unionType" }', + 'legacy:arrayshape' => '{ "type": "array", "items": { "type": "boolean" }, "property": "arrayShape" }', + 'type-info:arrayshape' => '{ "type": "object", "additionalProperties": { "type": "boolean" }, "property": "arrayShape" }', 'promotedstring' => '{ "type": "string", "property": "promotedString" }', 'legacy:mixedunion' => '{ "example": "My value", "property": "mixedUnion" }', 'type-info:mixedunion' => '{ "example": "My value", "oneOf": [ { "type": "string" }, { "type": "array", "items": { "type": "mixed" } } ], "property": "mixedUnion" }', @@ -58,7 +58,7 @@ public static function resolverAugmentCases(): iterable 'legacy:nullabletypedlistunion' => '{ "nullable": true, "property": "nullableTypedListUnion" }', 'type-info:nullabletypedlistunion' => '{ "nullable": true, "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } } ], "property": "nullableTypedListUnion" }', 'legacy:nullablenestedtypedlistunion' => '{ "nullable": true, "property": "nullableNestedTypedListUnion" }', - '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" }', + '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" }', 'reflectionvalue' => '{ "example": true, "nullable": true, "property": "reflectionValue" }', 'legacy:intersectionvar' => '{ "property": "intersectionVar" }', 'type-info:intersectionvar' => '{ "allOf": [ { "$ref": "#/components/schemas/FirstInterface" }, { "$ref": "#/components/schemas/SecondInterface" } ], "property": "intersectionVar" }', @@ -66,6 +66,8 @@ public static function resolverAugmentCases(): iterable 'type-info:nestedoneof' => '{ "oneOf": [ { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } }, { "type": "array", "items": { "type": "string" } } ], "property": "nestedOneOf" }', 'legacy:nestedoneofwithitems' => '{ "type": "array", "items": { "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "string" } ] }, "property": "nestedOneOfWithItems" }', 'type-info:nestedoneofwithitems' => '{ "type": "array", "items": { "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "string" } ] }, "property": "nestedOneOfWithItems" }', + 'type-info:stringmap' => '{ "type": "object", "additionalProperties": { "type": "string" }, "property": "stringMap" }', + 'type-info:intkeyedmap' => '{ "type": "object", "additionalProperties": { "type": "string" }, "property": "intKeyedMap" }', ], OA\OpenApi::VERSION_3_1_0 => [ 'nothing' => '{ "property": "nothing" }', @@ -87,8 +89,8 @@ public static function resolverAugmentCases(): iterable 'intrange' => '{ "type": "integer", "maximum": 10, "minimum": -9223372036854775808, "property": "intRange" }', 'positiveint' => '{ "type": "integer", "maximum": 9223372036854775807, "minimum": 1, "property": "positiveInt" }', 'nonzeroint' => '{ "type": "integer", "not": { "const": 0 }, "property": "nonZeroInt" } ', - 'arrayshape' => '{ "type": "array", "items": { "type": "boolean" }, "property": "arrayShape" }', - 'legacy:uniontype' => '{ "property": "unionType" }', + 'legacy:arrayshape' => '{ "type": "array", "items": { "type": "boolean" }, "property": "arrayShape" }', + 'type-info:arrayshape' => '{ "type": "object", "additionalProperties": { "type": "boolean" }, "property": "arrayShape" }', 'type-info:uniontype' => '{ "type": [ "integer", "string" ], "property": "unionType" }', 'promotedstring' => '{ "type": "string", "property": "promotedString" }', 'legacy:mixedunion' => '{ "example": "My value", "property": "mixedUnion" }', @@ -103,7 +105,7 @@ public static function resolverAugmentCases(): iterable 'legacy:nullabletypedlistunion' => '{ "property": "nullableTypedListUnion" }', 'type-info:nullabletypedlistunion' => '{ "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } }, { "type": "null" } ], "property": "nullableTypedListUnion" }', 'legacy:nullablenestedtypedlistunion' => '{ "property": "nullableNestedTypedListUnion" }', - '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" }', + '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" }', 'legacy:reflectionvalue' => '{ "example": true, "property": "reflectionValue" }', 'type-info:reflectionvalue' => '{ "type": [ "boolean", "integer", "null" ], "example": true, "property": "reflectionValue" }', 'legacy:intersectionvar' => '{ "property": "intersectionVar" }', @@ -112,6 +114,8 @@ public static function resolverAugmentCases(): iterable 'type-info:nestedoneof' => '{ "oneOf": [ { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } }, { "type": "array", "items": { "type": "string" } } ], "property": "nestedOneOf" }', 'legacy:nestedoneofwithitems' => '{ "type": "array", "items": { "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "string" } ] }, "property": "nestedOneOfWithItems" }', 'type-info:nestedoneofwithitems' => '{ "type": "array", "items": { "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "string" } ] }, "property": "nestedOneOfWithItems" }', + 'type-info:stringmap' => '{ "type": "object", "additionalProperties": { "type": "string" }, "property": "stringMap" }', + 'type-info:intkeyedmap' => '{ "type": "object", "additionalProperties": { "type": "string" }, "property": "intKeyedMap" }', ], ];