Skip to content

Commit a8e23e7

Browse files
committed
fix(hydra): genId false schema
1 parent 5b59f89 commit a8e23e7

File tree

9 files changed

+66
-24
lines changed

9 files changed

+66
-24
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1340,7 +1340,8 @@ jobs:
13401340
tests/Fixtures/app/console api:openapi:export --yaml -o build/out/openapi/openapi_v3.yaml
13411341
- name: Validate OpenAPI documents
13421342
run: |
1343-
npx @quobix/vacuum lint -r tests/Fixtures/app/ruleset.yaml build/out/openapi/openapi_v3.yaml -d --ignore-array-circle-ref --ignore-polymorph-circle-ref -b --no-clip
1343+
npm i -g @quobix/vacuum
1344+
vacuum lint -r tests/Fixtures/app/ruleset.yaml build/out/openapi/openapi_v3.yaml -d --ignore-array-circle-ref --ignore-polymorph-circle-ref -b --no-clip
13441345
13451346
laravel:
13461347
name: Laravel (PHP ${{ matrix.php }})

src/Hydra/JsonSchema/SchemaFactory.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareI
3737
use SchemaUriPrefixTrait;
3838

3939
private const ITEM_BASE_SCHEMA_NAME = 'HydraItemBaseSchema';
40+
private const ITEM_WITHOUT_ID_BASE_SCHEMA_NAME = 'HydraItemBaseSchemaWithoutId';
4041
private const COLLECTION_BASE_SCHEMA_NAME = 'HydraCollectionBaseSchema';
42+
4143
private const BASE_PROP = [
4244
'type' => 'string',
4345
];
@@ -68,9 +70,16 @@ final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareI
6870
],
6971
],
7072
] + self::BASE_PROPS,
73+
];
74+
75+
private const ITEM_BASE_SCHEMA_WITH_ID = self::ITEM_BASE_SCHEMA + [
7176
'required' => ['@id', '@type'],
7277
];
7378

79+
private const ITEM_BASE_SCHEMA_WITHOUT_ID = self::ITEM_BASE_SCHEMA + [
80+
'required' => ['@type'],
81+
];
82+
7483
/**
7584
* @var array<string, true>
7685
*/
@@ -144,10 +153,15 @@ public function buildSchema(string $className, string $format = 'jsonld', string
144153
return $schema;
145154
}
146155

156+
$hasNoId = Schema::TYPE_OUTPUT === $type && false === ($serializerContext['gen_id'] ?? true);
147157
$baseName = self::ITEM_BASE_SCHEMA_NAME;
148158

159+
if ($hasNoId) {
160+
$baseName = self::ITEM_WITHOUT_ID_BASE_SCHEMA_NAME;
161+
}
162+
149163
if (!isset($definitions[$baseName])) {
150-
$definitions[$baseName] = self::ITEM_BASE_SCHEMA;
164+
$definitions[$baseName] = $hasNoId ? self::ITEM_BASE_SCHEMA_WITHOUT_ID : self::ITEM_BASE_SCHEMA_WITH_ID;
151165
}
152166

153167
$allOf = new \ArrayObject(['allOf' => [

src/JsonSchema/DefinitionNameFactory.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ public function create(string $className, string $format = 'json', ?string $inpu
6363
$name = $groups ? \sprintf('%s-%s', $prefix, implode('_', $groups)) : $prefix;
6464
}
6565

66+
if (false === ($serializerContext['gen_id'] ?? true)) {
67+
$name .= '_noid';
68+
}
69+
6670
return $this->encodeDefinitionName($name);
6771
}
6872

src/JsonSchema/SchemaFactory.php

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -245,23 +245,20 @@ private function buildLegacyPropertySchema(Schema $schema, string $definitionNam
245245
}
246246

247247
$subSchemaFactory = $this->schemaFactory ?: $this;
248-
$subSchema = $subSchemaFactory->buildSchema($className, $format, $parentType, null, $subSchema, $serializerContext + [self::FORCE_SUBSCHEMA => true], false);
248+
$subSchema = $subSchemaFactory->buildSchema(
249+
$className,
250+
$format,
251+
$parentType,
252+
null,
253+
$subSchema,
254+
$serializerContext + [self::FORCE_SUBSCHEMA => true, 'gen_id' => $propertyMetadata->getGenId() ?? true],
255+
false,
256+
);
257+
249258
if (!isset($subSchema['$ref'])) {
250259
continue;
251260
}
252261

253-
if (false === $propertyMetadata->getGenId()) {
254-
$subDefinitionName = $this->definitionNameFactory->create($className, $format, $className, null, $serializerContext);
255-
256-
if (isset($subSchema->getDefinitions()[$subDefinitionName])) {
257-
// @see https://github.com/api-platform/core/issues/7162
258-
// Need to rebuild the definition without @id property and set it back to the sub-schema
259-
$subSchemaDefinition = $subSchema->getDefinitions()[$subDefinitionName]->getArrayCopy();
260-
unset($subSchemaDefinition['properties']['@id']);
261-
$subSchema->getDefinitions()[$subDefinitionName] = new \ArrayObject($subSchemaDefinition);
262-
}
263-
}
264-
265262
if ($isCollection) {
266263
$key = ($propertySchema['type'] ?? null) === 'object' ? 'additionalProperties' : 'items';
267264
$propertySchema[$key]['$ref'] = $subSchema['$ref'];
@@ -371,18 +368,19 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str
371368
$subSchemaInstance = new Schema($version);
372369
$subSchemaInstance->setDefinitions($schema->getDefinitions());
373370
$subSchemaFactory = $this->schemaFactory ?: $this;
374-
$subSchemaResult = $subSchemaFactory->buildSchema($className, $format, $parentType, null, $subSchemaInstance, $serializerContext + [self::FORCE_SUBSCHEMA => true], false);
371+
$subSchemaResult = $subSchemaFactory->buildSchema(
372+
$className,
373+
$format,
374+
$parentType,
375+
null,
376+
$subSchemaInstance,
377+
$serializerContext + [self::FORCE_SUBSCHEMA => true, 'gen_id' => $propertyMetadata->getGenId() ?? true],
378+
false,
379+
);
375380
if (!isset($subSchemaResult['$ref'])) {
376381
continue;
377382
}
378383

379-
if (false === $propertyMetadata->getGenId()) {
380-
$subDefinitionName = $this->definitionNameFactory->create($className, $format, $className, null, $serializerContext);
381-
if (isset($subSchemaResult->getDefinitions()[$subDefinitionName]['properties']['@id'])) {
382-
unset($subSchemaResult->getDefinitions()[$subDefinitionName]['properties']['@id']);
383-
}
384-
}
385-
386384
if ($isCollection) {
387385
$key = ($propertySchema['type'] ?? null) === 'object' ? 'additionalProperties' : 'items';
388386
if (!isset($propertySchema['type'])) {

tests/Fixtures/TestBundle/ApiResource/AggregateRating.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@
1515

1616
use ApiPlatform\Metadata\ApiProperty;
1717
use ApiPlatform\Metadata\ApiResource;
18+
use Symfony\Component\Serializer\Attribute\Groups;
1819

1920
#[ApiResource(types: 'https://schema.org/AggregateRating', operations: [])]
2021
final class AggregateRating
2122
{
2223
public function __construct(
24+
#[Groups(['with_aggregate_rating'])]
2325
#[ApiProperty(iris: ['https://schema.org/ratingValue'])]
2426
public float $ratingValue,
27+
#[Groups(['with_aggregate_rating'])]
2528
#[ApiProperty(iris: ['https://schema.org/reviewCount'])]
2629
public int $reviewCount,
2730
) {

tests/Fixtures/TestBundle/ApiResource/Product.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
use ApiPlatform\Metadata\ApiProperty;
1717
use ApiPlatform\Metadata\Get;
18+
use ApiPlatform\Metadata\GetCollection;
19+
use Symfony\Component\Serializer\Attribute\Groups;
1820

1921
#[Get(
2022
types: ['https://schema.org/Product'],
@@ -23,12 +25,19 @@
2325
provider: [self::class, 'provide'],
2426
jsonStream: true
2527
)]
28+
#[GetCollection(
29+
types: ['https://schema.org/Product'],
30+
uriTemplate: '/json-stream-products',
31+
provider: [self::class, 'provide'],
32+
normalizationContext: ['groups' => ['with_aggregate_rating'], 'hydra_prefix' => false]
33+
)]
2634
class Product
2735
{
2836
#[ApiProperty(identifier: true)]
2937
public string $code;
3038

3139
#[ApiProperty(genId: false, iris: ['https://schema.org/aggregateRating'])]
40+
#[Groups(['with_aggregate_rating'])]
3241
public AggregateRating $aggregateRating;
3342

3443
#[ApiProperty(property: 'name', iris: ['https://schema.org/name'])]

tests/Fixtures/app/ruleset.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
extends: [[spectral:oas, recommended]]
22
rules:
3+
camel-case-properties: false
34
circular-references: false
45
operation-success-response: false
56
oas3-parameter-description: false

tests/Functional/JsonSchema/JsonSchemaTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@
1717
use ApiPlatform\JsonSchema\SchemaFactoryInterface;
1818
use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface;
1919
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
20+
use ApiPlatform\Symfony\Bundle\Test\Constraint\MatchesJsonSchema;
21+
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\AggregateRating;
2022
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5452\Book;
2123
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5501\BrokenDocs;
2224
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5501\Related;
25+
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Product;
2326
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\ResourceWithEnumProperty;
2427
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5793\BagOfTests;
2528
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5793\TestEntity;
@@ -61,6 +64,8 @@ public static function getResources(): array
6164
Book::class,
6265
JsonSchemaResource::class,
6366
JsonSchemaResourceRelated::class,
67+
Product::class,
68+
AggregateRating::class,
6469
];
6570
}
6671

@@ -212,4 +217,10 @@ public function testReadOnlySchema(): void
212217
$schema = $this->schemaFactory->buildSchema(JsonSchemaResource::class, 'json', Schema::TYPE_OUTPUT);
213218
$this->assertTrue($schema['definitions']['Resource']['properties']['resourceRelated']['readOnly'] ?? false);
214219
}
220+
221+
public function testGenIdFalse()
222+
{
223+
$schema = $this->schemaFactory->buildSchema(Product::class, 'jsonld', Schema::TYPE_OUTPUT, $this->operationMetadataFactory->create('_api_/json-stream-products_get_collection'));
224+
$this->assertThat(['member' => [['aggregateRating' => ['ratingValue' => '1.0', 'reviewCount' => 1]]]], new MatchesJsonSchema($schema));
225+
}
215226
}

tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,13 +174,14 @@ public function testBackedEnumExamplesAreNotLost(): void
174174

175175
/**
176176
* Test feature #6716.
177+
* Note: in this test the embed object is not a resource, the behavior is different from where the embeded is an ApiResource.
177178
*/
178179
public function testGenId(): void
179180
{
180181
$this->tester->run(['command' => 'api:json-schema:generate', 'resource' => 'ApiPlatform\Tests\Fixtures\TestBundle\Entity\DisableIdGeneration', '--type' => 'output', '--format' => 'jsonld']);
181182
$result = $this->tester->getDisplay();
182183
$json = json_decode($result, associative: true);
183-
$this->assertArrayNotHasKey('@id', $json['definitions']['DisableIdGenerationItem.jsonld']['properties']);
184+
$this->assertArrayNotHasKey('@id', $json['definitions']['DisableIdGenerationItem.jsonld_noid']['properties']);
184185
}
185186

186187
#[DataProvider('arrayPropertyTypeSyntaxProvider')]

0 commit comments

Comments
 (0)