Skip to content

Commit 520c232

Browse files
committed
fix(jsonschema): hashmaps produces invalid openapi schema
1 parent 717c7e5 commit 520c232

File tree

4 files changed

+157
-3
lines changed

4 files changed

+157
-3
lines changed

src/JsonSchema/Metadata/Property/Factory/SchemaPropertyMetadataFactory.php

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ final class SchemaPropertyMetadataFactory implements PropertyMetadataFactoryInte
3434

3535
public const JSON_SCHEMA_USER_DEFINED = 'user_defined_schema';
3636

37-
public function __construct(ResourceClassResolverInterface $resourceClassResolver, private readonly ?PropertyMetadataFactoryInterface $decorated = null)
38-
{
37+
public function __construct(
38+
ResourceClassResolverInterface $resourceClassResolver,
39+
private readonly ?PropertyMetadataFactoryInterface $decorated = null,
40+
) {
3941
$this->resourceClassResolver = $resourceClassResolver;
4042
}
4143

@@ -171,9 +173,18 @@ private function getType(Type $type, ?bool $readableLink = null): array
171173
$subType = ($type->getCollectionValueTypes()[0] ?? null) ?? new Type($type->getBuiltinType(), false, $type->getClassName(), false);
172174

173175
if (null !== $keyType && Type::BUILTIN_TYPE_STRING === $keyType->getBuiltinType()) {
176+
// If the value is an object, treat as an object
177+
if (null !== $subType->getClassName()) {
178+
return $this->addNullabilityToTypeDefinition([
179+
'type' => 'object',
180+
'additionalProperties' => $this->getType($keyType, $readableLink),
181+
], $type);
182+
}
183+
184+
// Otherwise, treat as a hashmap with a generic value
174185
return $this->addNullabilityToTypeDefinition([
175186
'type' => 'object',
176-
'additionalProperties' => $this->getType($subType, $readableLink),
187+
'items' => $this->getType($subType, $readableLink),
177188
], $type);
178189
}
179190

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6800;
15+
16+
class Foo
17+
{
18+
public string $bar;
19+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6800;
15+
16+
use ApiPlatform\Metadata\Get;
17+
18+
#[Get]
19+
class TestApiDocHashmapArrayObjectIssue
20+
{
21+
/** @var array<Foo> */
22+
public array $foos;
23+
24+
/** @var Foo[] */
25+
public array $fooOtherSyntax;
26+
27+
/** @var array<string, Foo> */
28+
public array $fooHashmaps;
29+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\JsonSchema;
15+
16+
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6800\TestApiDocHashmapArrayObjectIssue;
17+
use Symfony\Bundle\FrameworkBundle\Console\Application;
18+
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
19+
use Symfony\Component\Console\Tester\ApplicationTester;
20+
21+
final class SchemaPropertyMetadataOpenApiTest extends KernelTestCase
22+
{
23+
private const RESOURCE_NAME_JSONLD = 'TestApiDocHashmapArrayObjectIssue.jsonld';
24+
private const FOO_JSONLD = 'Foo.jsonld';
25+
private ApplicationTester $tester;
26+
27+
protected function setUp(): void
28+
{
29+
self::bootKernel();
30+
31+
$application = new Application(self::$kernel);
32+
$application->setCatchExceptions(true);
33+
$application->setAutoExit(false);
34+
35+
$this->tester = new ApplicationTester($application);
36+
}
37+
38+
/**
39+
* @dataProvider arrayPropertyTypeSyntaxProvider
40+
*/
41+
public function testOpenApiSchemaGenerationForArrayProperty(string $propertyName, array $expectedProperties): void
42+
{
43+
$this->tester->run([
44+
'command' => 'api:json-schema:generate',
45+
'resource' => TestApiDocHashmapArrayObjectIssue::class,
46+
'--operation' => '_api_/test_api_doc_hashmap_array_object_issues{._format}_get',
47+
'--type' => 'output',
48+
'--format' => 'jsonld',
49+
]);
50+
51+
$result = $this->tester->getDisplay();
52+
$json = json_decode($result, true);
53+
54+
$this->assertArrayHasKey(self::RESOURCE_NAME_JSONLD, $json['definitions']);
55+
$this->assertArrayHasKey(self::FOO_JSONLD, $json['definitions']);
56+
$this->assertEquals('object', $json['definitions'][self::RESOURCE_NAME_JSONLD]['type']);
57+
$this->assertArrayHasKey($propertyName, $json['definitions'][self::RESOURCE_NAME_JSONLD]['properties']);
58+
59+
$this->assertEquals($expectedProperties, $json['definitions'][self::RESOURCE_NAME_JSONLD]['properties'][$propertyName]);
60+
$this->assertEquals('object', $json['definitions'][self::FOO_JSONLD]['type']);
61+
$this->assertArrayHasKey('bar', $json['definitions'][self::FOO_JSONLD]['properties']);
62+
$this->assertEquals(['type' => 'string'], $json['definitions'][self::FOO_JSONLD]['properties']['bar']);
63+
}
64+
65+
public static function arrayPropertyTypeSyntaxProvider(): \Generator
66+
{
67+
yield 'Array of Foo objects using array<Foo> syntax' => [
68+
'foos',
69+
[
70+
'type' => 'array',
71+
'items' => [
72+
'$ref' => '#/definitions/'.self::FOO_JSONLD,
73+
],
74+
],
75+
];
76+
yield 'Array of Foo objects using Foo[] syntax' => [
77+
'fooOtherSyntax',
78+
[
79+
'type' => 'array',
80+
'items' => [
81+
'$ref' => '#/definitions/'.self::FOO_JSONLD,
82+
],
83+
],
84+
];
85+
yield 'Hashmap of Foo objects using array<string, Foo> syntax' => [
86+
'fooHashmaps',
87+
[
88+
'type' => 'object',
89+
'additionalProperties' => [
90+
'type' => 'string',
91+
],
92+
],
93+
];
94+
}
95+
}

0 commit comments

Comments
 (0)