Skip to content

Commit 88ff9fb

Browse files
committed
fixing openapi errors :D
1 parent 7a7a135 commit 88ff9fb

File tree

10 files changed

+98
-90
lines changed

10 files changed

+98
-90
lines changed

‎.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1298,7 +1298,7 @@ jobs:
12981298
tests/Fixtures/app/console api:openapi:export --yaml -o build/out/openapi/openapi_v3.yaml
12991299
- name: Validate OpenAPI documents
13001300
run: |
1301-
npx @quobix/vacuum lint -r tests/Fixtures/app/ruleset.yaml build/out/openapi/openapi_v3.yaml -d
1301+
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
13021302
13031303
laravel:
13041304
name: Laravel (PHP ${{ matrix.php }})

‎features/openapi/docs.feature

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ Feature: Documentation support
8686
"""
8787
{
8888
"default": "male",
89-
"example": "male",
9089
"type": ["string", "null"],
9190
"enum": [
9291
"male",

‎src/Hydra/JsonSchema/SchemaFactory.php

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ public function buildSchema(string $className, string $format = 'jsonld', string
122122
return $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection);
123123
}
124124

125-
$definitionName = $this->definitionNameFactory->create($className, $format, $className, $operation, $serializerContext);
125+
$definitionName = $this->definitionNameFactory->create($className, $format, $inputOrOutputClass, $operation, $serializerContext);
126126

127127
// JSON-LD is slightly different then JSON:API or HAL
128128
// All the references that are resources must also be in JSON-LD therefore combining
@@ -134,23 +134,22 @@ public function buildSchema(string $className, string $format = 'jsonld', string
134134
$prefix = $this->getSchemaUriPrefix($schema->getVersion());
135135
$collectionKey = $schema->getItemsDefinitionKey();
136136
$key = $schema->getRootDefinitionKey() ?? $collectionKey;
137-
$name = Schema::TYPE_OUTPUT === $type ? self::ITEM_BASE_SCHEMA_NAME : self::ITEM_BASE_SCHEMA_OUTPUT_NAME;
138-
139-
if (!isset($definitions[$name])) {
140-
$definitions[$name] = Schema::TYPE_OUTPUT === $type ? self::ITEM_BASE_SCHEMA_OUTPUT : self::ITEM_BASE_SCHEMA;
141-
}
142-
143-
if (isset($definitions[$key]['description'])) {
144-
$definitions[$definitionName]['description'] = $definitions[$key]['description'];
145-
}
146137

147138
if (!$collectionKey) {
148139
if ($this->transformed[$definitionName] ?? false) {
149140
return $schema;
150141
}
151142

143+
$baseName = Schema::TYPE_OUTPUT === $type ? self::ITEM_BASE_SCHEMA_NAME : self::ITEM_BASE_SCHEMA_OUTPUT_NAME;
144+
145+
if ($this->isResourceClass($inputOrOutputClass)) {
146+
if (!isset($definitions[$baseName])) {
147+
$definitions[$baseName] = Schema::TYPE_OUTPUT === $type ? self::ITEM_BASE_SCHEMA_OUTPUT : self::ITEM_BASE_SCHEMA;
148+
}
149+
}
150+
152151
$allOf = new \ArrayObject(['allOf' => [
153-
['$ref' => $prefix.$name],
152+
['$ref' => $prefix.$baseName],
154153
$definitions[$key],
155154
]]);
156155

@@ -159,7 +158,6 @@ public function buildSchema(string $className, string $format = 'jsonld', string
159158
}
160159

161160
$definitions[$definitionName] = $allOf;
162-
163161
unset($definitions[$definitionName]['allOf'][1]['description']);
164162

165163
$schema['$ref'] = $prefix.$definitionName;
@@ -169,7 +167,6 @@ public function buildSchema(string $className, string $format = 'jsonld', string
169167
return $schema;
170168
}
171169

172-
// handle hydra:Collection
173170
if (($schema['type'] ?? '') !== 'array') {
174171
return $schema;
175172
}
@@ -262,8 +259,6 @@ public function buildSchema(string $className, string $format = 'jsonld', string
262259
];
263260
}
264261

265-
unset($schema['items']);
266-
267262
$schema['type'] = 'object';
268263
$schema['description'] = "$definitionName collection.";
269264
$schema['allOf'] = [
@@ -273,12 +268,14 @@ public function buildSchema(string $className, string $format = 'jsonld', string
273268
'properties' => [
274269
$hydraPrefix.'member' => [
275270
'type' => 'array',
276-
'items' => ['$ref' => $prefix.$definitionName],
271+
'items' => $schema['items'],
277272
],
278273
],
279274
],
280275
];
281276

277+
unset($schema['items']);
278+
282279
return $schema;
283280
}
284281

‎src/JsonApi/JsonSchema/SchemaFactory.php

Lines changed: 52 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,11 @@ final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareI
124124
],
125125
];
126126

127+
/**
128+
* @var array<string, bool>
129+
*/
130+
private $builtSchema = [];
131+
127132
public function __construct(private readonly SchemaFactoryInterface $schemaFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, private ?DefinitionNameFactoryInterface $definitionNameFactory = null)
128133
{
129134
if (!$definitionNameFactory) {
@@ -163,12 +168,12 @@ public function buildSchema(string $className, string $format = 'jsonapi', strin
163168
// We don't use the serializer context here as JSON:API doesn't leverage serializer groups for related resources.
164169
// That is done by query parameter. @see https://jsonapi.org/format/#fetching-includes
165170
$jsonApiSerializerContext = $serializerContext;
166-
if (false === ($serializerContext[self::DISABLE_JSON_SCHEMA_SERIALIZER_GROUPS] ?? true)) {
171+
if (true === ($serializerContext[self::DISABLE_JSON_SCHEMA_SERIALIZER_GROUPS] ?? true) && $inputOrOutputClass === $className) {
167172
unset($jsonApiSerializerContext['groups']);
168173
}
169174

170175
$schema = $this->schemaFactory->buildSchema($className, 'json', $type, $operation, $schema, $jsonApiSerializerContext, $forceCollection);
171-
$definitionName = $this->definitionNameFactory->create($className, $format, $className, $operation, $serializerContext);
176+
$definitionName = $this->definitionNameFactory->create($inputOrOutputClass, $format, $className, $operation, $jsonApiSerializerContext);
172177
$prefix = $this->getSchemaUriPrefix($schema->getVersion());
173178
$definitions = $schema->getDefinitions();
174179
$collectionKey = $schema->getItemsDefinitionKey();
@@ -183,11 +188,6 @@ public function buildSchema(string $className, string $format = 'jsonapi', strin
183188
$key = $schema->getRootDefinitionKey() ?? $collectionKey;
184189
$properties = $definitions[$definitionName]['properties'] ?? [];
185190

186-
// Prevent reapplying
187-
if (isset($definitions[$key]['description'])) {
188-
$definitions[$definitionName]['description'] = $definitions[$key]['description'];
189-
}
190-
191191
if (Error::class === $className && !isset($properties['errors'])) {
192192
$definitions[$definitionName]['properties'] = [
193193
'errors' => [
@@ -213,41 +213,41 @@ public function buildSchema(string $className, string $format = 'jsonapi', strin
213213
return $schema;
214214
}
215215

216-
if (($schema['type'] ?? '') === 'array') {
217-
if (!isset($definitions[self::COLLECTION_BASE_SCHEMA_NAME])) {
218-
$definitions[self::COLLECTION_BASE_SCHEMA_NAME] = [
219-
'type' => 'object',
220-
'properties' => [
221-
'links' => self::LINKS_PROPS,
222-
'meta' => self::META_PROPS,
223-
'data' => [
224-
'type' => 'array',
225-
],
226-
],
227-
'required' => ['data'],
228-
];
229-
}
230-
231-
unset($schema['items']);
232-
unset($schema['type']);
233-
234-
$properties = $this->buildDefinitionPropertiesSchema($key, $className, $format, $type, $operation, $schema, []);
235-
$properties['data']['properties']['attributes']['$ref'] = $prefix.$key;
216+
if (($schema['type'] ?? '') !== 'array') {
217+
return $schema;
218+
}
236219

237-
$schema['description'] = "$definitionName collection.";
238-
$schema['allOf'] = [
239-
['$ref' => $prefix.self::COLLECTION_BASE_SCHEMA_NAME],
240-
['type' => 'object', 'properties' => [
220+
if (!isset($definitions[self::COLLECTION_BASE_SCHEMA_NAME])) {
221+
$definitions[self::COLLECTION_BASE_SCHEMA_NAME] = [
222+
'type' => 'object',
223+
'properties' => [
224+
'links' => self::LINKS_PROPS,
225+
'meta' => self::META_PROPS,
241226
'data' => [
242227
'type' => 'array',
243-
'items' => $properties['data'],
244228
],
245-
]],
229+
],
230+
'required' => ['data'],
246231
];
247-
248-
return $schema;
249232
}
250233

234+
unset($schema['items']);
235+
unset($schema['type']);
236+
237+
$properties = $this->buildDefinitionPropertiesSchema($key, $className, $format, $type, $operation, $schema, []);
238+
$properties['data']['properties']['attributes']['$ref'] = $prefix.$key;
239+
240+
$schema['description'] = "$definitionName collection.";
241+
$schema['allOf'] = [
242+
['$ref' => $prefix.self::COLLECTION_BASE_SCHEMA_NAME],
243+
['type' => 'object', 'properties' => [
244+
'data' => [
245+
'type' => 'array',
246+
'items' => $properties['data'],
247+
],
248+
]],
249+
];
250+
251251
return $schema;
252252
}
253253

@@ -279,8 +279,23 @@ private function buildDefinitionPropertiesSchema(string $key, string $className,
279279
$inputOrOutputClass = $this->findOutputClass($relatedClassName, $type, $operation, $serializerContext);
280280
$serializerContext ??= $this->getSerializerContext($operation, $type);
281281
$definitionName = $this->definitionNameFactory->create($relatedClassName, $format, $inputOrOutputClass, $operation, $serializerContext);
282-
$ref = $this->getSchemaUriPrefix($schema->getVersion()).$definitionName;
283-
$refs[$ref] = '$ref';
282+
283+
// to avoid recursion
284+
if ($this->builtSchema[$definitionName] ?? false) {
285+
$refs[$this->getSchemaUriPrefix($schema->getVersion()).$definitionName] = '$ref';
286+
continue;
287+
}
288+
289+
if (!isset($definitions[$definitionName])) {
290+
$this->builtSchema[$definitionName] = true;
291+
$subSchema = new Schema($schema->getVersion());
292+
$subSchema->setDefinitions($schema->getDefinitions());
293+
$subSchema = $this->buildSchema($relatedClassName, $format, $type, $operation, $subSchema, $serializerContext + [self::FORCE_SUBSCHEMA => true], false);
294+
$schema->setDefinitions($subSchema->getDefinitions());
295+
$definitions = $schema->getDefinitions();
296+
}
297+
298+
$refs[$this->getSchemaUriPrefix($schema->getVersion()).$definitionName] = '$ref';
284299
}
285300
$relatedDefinitions[$propertyName] = array_flip($refs);
286301
if ($isOne) {
@@ -327,24 +342,6 @@ private function buildDefinitionPropertiesSchema(string $key, string $className,
327342
];
328343
}
329344

330-
if ($required = $definitions[$key]['required'] ?? null) {
331-
foreach ($required as $i => $require) {
332-
if (isset($relationships[$require])) {
333-
$replacement['relationships']['required'][] = $require;
334-
unset($required[$i]);
335-
}
336-
}
337-
338-
$replacement['attributes'] = [
339-
'allOf' => [
340-
$replacement['attributes'],
341-
['type' => 'object', 'required' => $required],
342-
],
343-
];
344-
345-
unset($definitions[$key]['required']);
346-
}
347-
348345
return [
349346
'data' => [
350347
'type' => 'object',

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

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ public function create(string $resourceClass, string $property, array $options =
7272
$link = (($options['schema_type'] ?? null) === Schema::TYPE_INPUT) ? $propertyMetadata->isWritableLink() : $propertyMetadata->isReadableLink();
7373
$propertySchema = $propertyMetadata->getSchema() ?? [];
7474

75+
if (null !== $propertyMetadata->getUriTemplate() || (!\array_key_exists('readOnly', $propertySchema) && false === $propertyMetadata->isWritable() && !$propertyMetadata->isInitializable())) {
76+
$propertySchema['readOnly'] = true;
77+
}
78+
7579
if (!\array_key_exists('writeOnly', $propertySchema) && false === $propertyMetadata->isReadable()) {
7680
$propertySchema['writeOnly'] = true;
7781
}
@@ -123,10 +127,6 @@ private function getTypeSchema(ApiProperty $propertyMetadata, array $propertySch
123127
$propertySchema['example'] = $example;
124128
}
125129

126-
if (!\array_key_exists('example', $propertySchema) && \array_key_exists('default', $propertySchema)) {
127-
$propertySchema['example'] = $propertySchema['default'];
128-
}
129-
130130
// never override the following keys if at least one is already set or if there's a custom openapi context
131131
if (
132132
null === $type
@@ -327,12 +327,11 @@ private function getClassSchemaDefinition(?string $className, ?bool $readableLin
327327
return ['type' => $type, 'enum' => $enumCases];
328328
}
329329

330-
// If it's a resource and links are not readable, represent as IRI string.
331-
if ($isResourceClass && true !== $readableLink) {
330+
if (false === $readableLink && $isResourceClass) {
332331
return [
333332
'type' => 'string',
334333
'format' => 'iri-reference',
335-
'example' => 'https://example.com/', // Add a generic example
334+
'example' => 'https://example.com/',
336335
];
337336
}
338337

@@ -359,10 +358,6 @@ private function getLegacyTypeSchema(ApiProperty $propertyMetadata, array $prope
359358
$propertySchema['example'] = $example;
360359
}
361360

362-
if (!\array_key_exists('example', $propertySchema) && \array_key_exists('default', $propertySchema)) {
363-
$propertySchema['example'] = $propertySchema['default'];
364-
}
365-
366361
// never override the following keys if at least one is already set or if there's a custom openapi context
367362
if (
368363
[] === $types
@@ -530,7 +525,7 @@ private function getLegacyClassType(?string $className, bool $nullable, ?bool $r
530525
];
531526
}
532527

533-
if (true !== $readableLink && $isResourceClass) {
528+
if (false === $readableLink && $isResourceClass) {
534529
return [
535530
'type' => 'string',
536531
'format' => 'iri-reference',

‎src/JsonSchema/SchemaFactory.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str
306306
}
307307

308308
$type = $propertyMetadata->getNativeType();
309-
$propertySchemaType = $propertySchema['type'] ?? false;
309+
$propertySchemaType = $propertySchema['type'] ?? Schema::UNKNOWN_TYPE;
310310
$isSchemaDefined = ($propertySchema['$ref'] ?? $propertySchema['anyOf'] ?? $propertySchema['allOf'] ?? $propertySchema['oneOf'] ?? false)
311311
|| ($propertySchemaType && 'string' !== $propertySchemaType && !(\is_array($propertySchemaType) && !\in_array('string', $propertySchemaType, true)))
312312
|| (($propertySchema['format'] ?? $propertySchema['enum'] ?? false) && $propertySchemaType);
@@ -323,11 +323,14 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str
323323
return;
324324
}
325325

326+
if ($isUnknown) {
327+
$propertySchema = [];
328+
}
329+
326330
// property schema is created in SchemaPropertyMetadataFactory, but it cannot build resource reference ($ref)
327331
// complete property schema with resource reference ($ref) if it's related to an object/resource
328332
$refs = [];
329333
$isNullable = $type?->isNullable() ?? false;
330-
$hasClassName = false;
331334

332335
if ($type) {
333336
foreach ($type instanceof CompositeTypeInterface ? $type->getTypes() : [$type] as $t) {

‎tests/Fixtures/TestBundle/ApiResource/ErrorWithOverridenStatus.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
// To make it work with 3.1, remove in 4
2424
uriVariables: ['id'],
2525
provider: [ErrorWithOverridenStatus::class, 'throw'],
26-
exceptionToStatus: [ValidationException::class => 403]
26+
exceptionToStatus: [ValidationException::class => 403],
27+
output: false
2728
)]
2829
class ErrorWithOverridenStatus
2930
{

‎tests/Fixtures/TestBundle/ApiResource/Issue6317/Issue6317.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ enum Issue6317: int
2222
case First = 1;
2323
case Second = 2;
2424

25-
#[ApiProperty(identifier: true, example: 'An example of an ID')]
25+
#[ApiProperty(identifier: true, example: 1)]
2626
public function getId(): int
2727
{
2828
return $this->value;
@@ -40,7 +40,7 @@ public function getOrdinal(): string
4040
return 1 === $this->value ? '1st' : '2nd';
4141
}
4242

43-
#[ApiProperty(openapiContext: ['example' => '42'])]
43+
#[ApiProperty(openapiContext: ['example' => 42])]
4444
public function getCardinal(): int
4545
{
4646
return $this->value;

‎tests/Functional/JsonSchema/JsonApiJsonSchemaTest.php

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

1616
use ApiPlatform\JsonSchema\Schema;
1717
use ApiPlatform\JsonSchema\SchemaFactoryInterface;
18+
use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface;
1819
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
1920
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Animal;
2021
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\AnimalObservation;
@@ -29,12 +30,14 @@ class JsonApiJsonSchemaTest extends ApiTestCase
2930
use SetupClassResourcesTrait;
3031

3132
protected SchemaFactoryInterface $schemaFactory;
33+
protected OperationMetadataFactoryInterface $operationMetadataFactory;
3234
protected static ?bool $alwaysBootKernel = false;
3335

3436
protected function setUp(): void
3537
{
3638
parent::setUp();
3739
$this->schemaFactory = self::getContainer()->get('api_platform.json_schema.schema_factory');
40+
$this->operationMetadataFactory = self::getContainer()->get('api_platform.metadata.operation.metadata_factory');
3841
}
3942

4043
/**

0 commit comments

Comments
 (0)