Skip to content

Commit 6119f8d

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

File tree

5 files changed

+150
-5
lines changed

5 files changed

+150
-5
lines changed

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

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use ApiPlatform\Metadata\Util\ResourceClassInfoTrait;
2222
use Doctrine\Common\Collections\ArrayCollection;
2323
use Ramsey\Uuid\UuidInterface;
24+
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
2425
use Symfony\Component\PropertyInfo\Type;
2526
use Symfony\Component\Uid\Ulid;
2627
use Symfony\Component\Uid\Uuid;
@@ -34,8 +35,11 @@ final class SchemaPropertyMetadataFactory implements PropertyMetadataFactoryInte
3435

3536
public const JSON_SCHEMA_USER_DEFINED = 'user_defined_schema';
3637

37-
public function __construct(ResourceClassResolverInterface $resourceClassResolver, private readonly ?PropertyMetadataFactoryInterface $decorated = null)
38-
{
38+
public function __construct(
39+
ResourceClassResolverInterface $resourceClassResolver,
40+
private readonly ?PropertyMetadataFactoryInterface $decorated = null,
41+
private readonly ?PropertyInfoExtractorInterface $propertyInfo = null,
42+
) {
3943
$this->resourceClassResolver = $resourceClassResolver;
4044
}
4145

@@ -198,6 +202,8 @@ private function typeToArray(Type $type, ?bool $readableLink = null): array
198202
* Gets the JSON Schema document which specifies the data type corresponding to the given PHP class, and recursively adds needed new schema to the current schema if provided.
199203
*
200204
* Note: if the class is not part of exceptions listed above, any class is considered as a resource.
205+
*
206+
* @throws PropertyNotFoundException
201207
*/
202208
private function getClassType(?string $className, bool $nullable, ?bool $readableLink): array
203209
{
@@ -240,7 +246,8 @@ private function getClassType(?string $className, bool $nullable, ?bool $readabl
240246
];
241247
}
242248

243-
if (!$this->isResourceClass($className) && is_a($className, \BackedEnum::class, true)) {
249+
$isResourceClass = $this->isResourceClass($className);
250+
if (!$isResourceClass && is_a($className, \BackedEnum::class, true)) {
244251
$enumCases = array_map(static fn (\BackedEnum $enum): string|int => $enum->value, $className::cases());
245252

246253
$type = \is_string($enumCases[0] ?? '') ? 'string' : 'integer';
@@ -263,8 +270,17 @@ private function getClassType(?string $className, bool $nullable, ?bool $readabl
263270
];
264271
}
265272

266-
// TODO: add propertyNameCollectionFactory and recurse to find the underlying schema? Right now SchemaFactory does the job so we don't compute anything here.
267-
return ['type' => Schema::UNKNOWN_TYPE];
273+
if (!$this->propertyInfo) {
274+
return ['type' => 'object'];
275+
}
276+
277+
$properties = $this->propertyInfo->getProperties($className, ['serializer_groups' => null]);
278+
$propertiesSchema = [];
279+
foreach ($properties as $property) {
280+
$propertiesSchema[$property] = $this->create($className, $property)->getSchema();
281+
}
282+
283+
return ['type' => 'object', 'properties' => $propertiesSchema];
268284
}
269285

270286
/**

src/Symfony/Bundle/Resources/config/json_schema.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
<service id="api_platform.json_schema.metadata.property.metadata_factory.schema" decorates="api_platform.metadata.property.metadata_factory" decoration-priority="10" class="ApiPlatform\JsonSchema\Metadata\Property\Factory\SchemaPropertyMetadataFactory" public="false">
3737
<argument type="service" id="api_platform.resource_class_resolver" />
3838
<argument type="service" id="api_platform.json_schema.metadata.property.metadata_factory.schema.inner" />
39+
<argument type="service" id="api_platform.property_info" on-invalid="null" />
3940
</service>
4041

4142
<service id="api_platform.json_schema.backward_compatible_schema_factory" decorates="api_platform.json_schema.schema_factory" decoration-priority="-2" class="ApiPlatform\JsonSchema\BackwardCompatibleSchemaFactory">
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
public int $baz;
20+
}
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+
}

tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\Tests\JsonSchema\Command;
1515

16+
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6800\TestApiDocHashmapArrayObjectIssue;
1617
use ApiPlatform\Tests\Fixtures\TestBundle\Document\Dummy as DocumentDummy;
1718
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;
1819
use Symfony\Bundle\FrameworkBundle\Console\Application;
@@ -345,4 +346,82 @@ public function testGenId(): void
345346
$json = json_decode($result, associative: true);
346347
$this->assertArrayNotHasKey('@id', $json['definitions']['DisableIdGenerationItem.jsonld']['properties']);
347348
}
349+
350+
/**
351+
* @dataProvider arrayPropertyTypeSyntaxProvider
352+
*/
353+
public function testOpenApiSchemaGenerationForArrayProperty(string $propertyName, array $expectedProperties): void
354+
{
355+
$this->tester->run([
356+
'command' => 'api:json-schema:generate',
357+
'resource' => TestApiDocHashmapArrayObjectIssue::class,
358+
'--operation' => '_api_/test_api_doc_hashmap_array_object_issues{._format}_get',
359+
'--type' => 'output',
360+
'--format' => 'jsonld',
361+
]);
362+
363+
$result = $this->tester->getDisplay();
364+
$json = json_decode($result, true);
365+
$definitions = $json['definitions'];
366+
$ressourceDefinitions = $definitions['TestApiDocHashmapArrayObjectIssue.jsonld'];
367+
368+
$this->assertArrayHasKey('TestApiDocHashmapArrayObjectIssue.jsonld', $definitions);
369+
$this->assertEquals('object', $ressourceDefinitions['type']);
370+
$this->assertEquals($expectedProperties, $ressourceDefinitions['properties'][$propertyName]);
371+
}
372+
373+
public static function arrayPropertyTypeSyntaxProvider(): \Generator
374+
{
375+
yield 'Array of Foo objects using array<Foo> syntax' => [
376+
'foos',
377+
[
378+
'type' => 'array',
379+
'items' => [
380+
'type' => 'object',
381+
'properties' => [
382+
'bar' => [
383+
'type' => 'string',
384+
],
385+
'baz' => [
386+
'type' => 'integer',
387+
],
388+
],
389+
],
390+
],
391+
];
392+
yield 'Array of Foo objects using Foo[] syntax' => [
393+
'fooOtherSyntax',
394+
[
395+
'type' => 'array',
396+
'items' => [
397+
'type' => 'object',
398+
'properties' => [
399+
'bar' => [
400+
'type' => 'string',
401+
],
402+
'baz' => [
403+
'type' => 'integer',
404+
],
405+
],
406+
],
407+
],
408+
];
409+
yield 'Hashmap of Foo objects using array<string, Foo> syntax' => [
410+
'fooHashmaps',
411+
[
412+
'type' => 'object',
413+
'additionalProperties' => [
414+
'type' => 'object',
415+
'properties' => [
416+
'bar' => [
417+
'type' => 'string',
418+
],
419+
'baz' => [
420+
'type' => 'integer',
421+
],
422+
],
423+
],
424+
],
425+
];
426+
}
348427
}

0 commit comments

Comments
 (0)