Skip to content

Commit 978975e

Browse files
fix(jsonschema): hashmaps produces invalid openapi schema (#6830)
* fix(jsonschema): hashmaps produces invalid openapi schema * fix --------- Co-authored-by: soyuka <soyuka@users.noreply.github.com>
1 parent 985a9a0 commit 978975e

File tree

5 files changed

+118
-8
lines changed

5 files changed

+118
-8
lines changed

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

Lines changed: 9 additions & 5 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

@@ -198,6 +200,8 @@ private function typeToArray(Type $type, ?bool $readableLink = null): array
198200
* 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.
199201
*
200202
* Note: if the class is not part of exceptions listed above, any class is considered as a resource.
203+
*
204+
* @throws PropertyNotFoundException
201205
*/
202206
private function getClassType(?string $className, bool $nullable, ?bool $readableLink): array
203207
{
@@ -240,7 +244,8 @@ private function getClassType(?string $className, bool $nullable, ?bool $readabl
240244
];
241245
}
242246

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

246251
$type = \is_string($enumCases[0] ?? '') ? 'string' : 'integer';
@@ -255,15 +260,14 @@ private function getClassType(?string $className, bool $nullable, ?bool $readabl
255260
];
256261
}
257262

258-
if (true !== $readableLink && $this->isResourceClass($className)) {
263+
if (true !== $readableLink && $isResourceClass) {
259264
return [
260265
'type' => 'string',
261266
'format' => 'iri-reference',
262267
'example' => 'https://example.com/',
263268
];
264269
}
265270

266-
// TODO: add propertyNameCollectionFactory and recurse to find the underlying schema? Right now SchemaFactory does the job so we don't compute anything here.
267271
return ['type' => Schema::UNKNOWN_TYPE];
268272
}
269273

src/JsonSchema/SchemaFactory.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,8 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str
183183
$propertySchemaType = $propertySchema['type'] ?? false;
184184

185185
$isUnknown = Schema::UNKNOWN_TYPE === $propertySchemaType
186-
|| ('array' === $propertySchemaType && Schema::UNKNOWN_TYPE === ($propertySchema['items']['type'] ?? null));
186+
|| ('array' === $propertySchemaType && Schema::UNKNOWN_TYPE === ($propertySchema['items']['type'] ?? null))
187+
|| ('object' === $propertySchemaType && Schema::UNKNOWN_TYPE === ($propertySchema['additionalProperties']['type'] ?? null));
187188

188189
if (
189190
!$isUnknown && (
@@ -241,8 +242,9 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str
241242
}
242243

243244
if ($isCollection) {
244-
$propertySchema['items']['$ref'] = $subSchema['$ref'];
245-
unset($propertySchema['items']['type']);
245+
$key = ($propertySchema['type'] ?? null) === 'object' ? 'additionalProperties' : 'items';
246+
$propertySchema[$key]['$ref'] = $subSchema['$ref'];
247+
unset($propertySchema[$key]['type']);
246248
break;
247249
}
248250

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 <dunglas@gmail.com>
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 <dunglas@gmail.com>
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: 55 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,58 @@ 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+
'$ref' => '#/definitions/Foo.jsonld',
381+
],
382+
],
383+
];
384+
yield 'Array of Foo objects using Foo[] syntax' => [
385+
'fooOtherSyntax',
386+
[
387+
'type' => 'array',
388+
'items' => [
389+
'$ref' => '#/definitions/Foo.jsonld',
390+
],
391+
],
392+
];
393+
yield 'Hashmap of Foo objects using array<string, Foo> syntax' => [
394+
'fooHashmaps',
395+
[
396+
'type' => 'object',
397+
'additionalProperties' => [
398+
'$ref' => '#/definitions/Foo.jsonld',
399+
],
400+
],
401+
];
402+
}
348403
}

0 commit comments

Comments
 (0)