Skip to content

Commit 7f5dc5b

Browse files
committed
fix(openapi): document error outputs using json-schemas
1 parent 6cc70d3 commit 7f5dc5b

File tree

13 files changed

+430
-170
lines changed

13 files changed

+430
-170
lines changed

src/JsonApi/JsonSchema/SchemaFactory.php

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2323
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
2424
use ApiPlatform\Metadata\ResourceClassResolverInterface;
25+
use ApiPlatform\State\ApiResource\Error;
2526

2627
/**
2728
* Decorator factory which adds JSON:API properties to the JSON Schema document.
@@ -31,6 +32,14 @@
3132
final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareInterface
3233
{
3334
use ResourceMetadataTrait;
35+
36+
/**
37+
* As JSON:API recommends using [includes](https://jsonapi.org/format/#fetching-includes) instead of groups
38+
* this flag allows to force using groups to generate the JSON:API JSONSchema. Defaults to true, use it in
39+
* a serializer context.
40+
*/
41+
public const DISABLE_JSON_SCHEMA_SERIALIZER_GROUPS = 'disable_json_schema_serializer_groups';
42+
3443
private const LINKS_PROPS = [
3544
'type' => 'object',
3645
'properties' => [
@@ -124,14 +133,28 @@ public function buildSchema(string $className, string $format = 'jsonapi', strin
124133
}
125134
// We don't use the serializer context here as JSON:API doesn't leverage serializer groups for related resources.
126135
// That is done by query parameter. @see https://jsonapi.org/format/#fetching-includes
127-
$schema = $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, [], $forceCollection);
136+
$operation ??= $this->findOperation($className, $type, $operation, $serializerContext, $format);
137+
$serializerContext ??= $this->getSerializerContext($operation, $type);
138+
$jsonApiSerializerContext = !($serializerContext[self::DISABLE_JSON_SCHEMA_SERIALIZER_GROUPS] ?? true) ? $serializerContext : [];
139+
$schema = $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $jsonApiSerializerContext, $forceCollection);
128140

129141
if (($key = $schema->getRootDefinitionKey()) || ($key = $schema->getItemsDefinitionKey())) {
130142
$definitions = $schema->getDefinitions();
131143
$properties = $definitions[$key]['properties'] ?? [];
132144

145+
if (Error::class === $className && !isset($properties['errors'])) {
146+
$definitions[$key]['properties'] = [
147+
'errors' => [
148+
'type' => 'object',
149+
'properties' => $properties,
150+
],
151+
];
152+
153+
return $schema;
154+
}
155+
133156
// Prevent reapplying
134-
if (isset($properties['id'], $properties['type']) || isset($properties['data'])) {
157+
if (isset($properties['id'], $properties['type']) || isset($properties['data']) || isset($properties['errors'])) {
135158
return $schema;
136159
}
137160

src/JsonSchema/ResourceMetadataTrait.php

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ private function findOutputClass(string $className, string $type, Operation $ope
3636
return $forceSubschema ? ($inputOrOutput['class'] ?? $inputOrOutput->class ?? $operation->getClass()) : ($inputOrOutput['class'] ?? $inputOrOutput->class ?? null);
3737
}
3838

39-
private function findOperation(string $className, string $type, ?Operation $operation, ?array $serializerContext): Operation
39+
private function findOperation(string $className, string $type, ?Operation $operation, ?array $serializerContext, ?string $format = null): Operation
4040
{
4141
if (null === $operation) {
4242
if (null === $this->resourceMetadataFactory) {
@@ -54,7 +54,7 @@ private function findOperation(string $className, string $type, ?Operation $oper
5454
$operation = new HttpOperation();
5555
}
5656

57-
return $this->findOperationForType($resourceMetadataCollection, $type, $operation);
57+
return $this->findOperationForType($resourceMetadataCollection, $type, $operation, $format);
5858
}
5959

6060
// The best here is to use an Operation when calling `buildSchema`, we try to do a smart guess otherwise
@@ -65,13 +65,13 @@ private function findOperation(string $className, string $type, ?Operation $oper
6565
return $resourceMetadataCollection->getOperation($operation->getName());
6666
}
6767

68-
return $this->findOperationForType($resourceMetadataCollection, $type, $operation);
68+
return $this->findOperationForType($resourceMetadataCollection, $type, $operation, $format);
6969
}
7070

7171
return $operation;
7272
}
7373

74-
private function findOperationForType(ResourceMetadataCollection $resourceMetadataCollection, string $type, Operation $operation): Operation
74+
private function findOperationForType(ResourceMetadataCollection $resourceMetadataCollection, string $type, Operation $operation, ?string $format = null): Operation
7575
{
7676
// Find the operation and use the first one that matches criterias
7777
foreach ($resourceMetadataCollection as $resourceMetadata) {
@@ -85,6 +85,11 @@ private function findOperationForType(ResourceMetadataCollection $resourceMetada
8585
$operation = $op;
8686
break 2;
8787
}
88+
89+
if ($format && Schema::TYPE_OUTPUT === $type && \array_key_exists($format, $op->getOutputFormats() ?? [])) {
90+
$operation = $op;
91+
break 2;
92+
}
8893
}
8994
}
9095

src/JsonSchema/SchemaFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public function buildSchema(string $className, string $format = 'json', string $
6161
$inputOrOutputClass = $className;
6262
$serializerContext ??= [];
6363
} else {
64-
$operation = $this->findOperation($className, $type, $operation, $serializerContext);
64+
$operation ??= $this->findOperation($className, $type, $operation, $serializerContext, $format);
6565
$inputOrOutputClass = $this->findOutputClass($className, $type, $operation, $serializerContext);
6666
$serializerContext ??= $this->getSerializerContext($operation, $type);
6767
}

src/Metadata/ApiProperty.php

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ public function getProperty(): ?string
227227
return $this->property;
228228
}
229229

230-
public function withProperty(string $property): self
230+
public function withProperty(string $property): static
231231
{
232232
$self = clone $this;
233233
$self->property = $property;
@@ -240,7 +240,7 @@ public function getDescription(): ?string
240240
return $this->description;
241241
}
242242

243-
public function withDescription(string $description): self
243+
public function withDescription(string $description): static
244244
{
245245
$self = clone $this;
246246
$self->description = $description;
@@ -253,7 +253,7 @@ public function isReadable(): ?bool
253253
return $this->readable;
254254
}
255255

256-
public function withReadable(bool $readable): self
256+
public function withReadable(bool $readable): static
257257
{
258258
$self = clone $this;
259259
$self->readable = $readable;
@@ -266,7 +266,7 @@ public function isWritable(): ?bool
266266
return $this->writable;
267267
}
268268

269-
public function withWritable(bool $writable): self
269+
public function withWritable(bool $writable): static
270270
{
271271
$self = clone $this;
272272
$self->writable = $writable;
@@ -279,7 +279,7 @@ public function isReadableLink(): ?bool
279279
return $this->readableLink;
280280
}
281281

282-
public function withReadableLink(bool $readableLink): self
282+
public function withReadableLink(bool $readableLink): static
283283
{
284284
$self = clone $this;
285285
$self->readableLink = $readableLink;
@@ -292,7 +292,7 @@ public function isWritableLink(): ?bool
292292
return $this->writableLink;
293293
}
294294

295-
public function withWritableLink(bool $writableLink): self
295+
public function withWritableLink(bool $writableLink): static
296296
{
297297
$self = clone $this;
298298
$self->writableLink = $writableLink;
@@ -305,7 +305,7 @@ public function isRequired(): ?bool
305305
return $this->required;
306306
}
307307

308-
public function withRequired(bool $required): self
308+
public function withRequired(bool $required): static
309309
{
310310
$self = clone $this;
311311
$self->required = $required;
@@ -318,7 +318,7 @@ public function isIdentifier(): ?bool
318318
return $this->identifier;
319319
}
320320

321-
public function withIdentifier(bool $identifier): self
321+
public function withIdentifier(bool $identifier): static
322322
{
323323
$self = clone $this;
324324
$self->identifier = $identifier;
@@ -331,7 +331,7 @@ public function getDefault()
331331
return $this->default;
332332
}
333333

334-
public function withDefault($default): self
334+
public function withDefault($default): static
335335
{
336336
$self = clone $this;
337337
$self->default = $default;
@@ -344,7 +344,7 @@ public function getExample(): mixed
344344
return $this->example;
345345
}
346346

347-
public function withExample(mixed $example): self
347+
public function withExample(mixed $example): static
348348
{
349349
$self = clone $this;
350350
$self->example = $example;
@@ -357,7 +357,7 @@ public function getDeprecationReason(): ?string
357357
return $this->deprecationReason;
358358
}
359359

360-
public function withDeprecationReason($deprecationReason): self
360+
public function withDeprecationReason($deprecationReason): static
361361
{
362362
$self = clone $this;
363363
$self->deprecationReason = $deprecationReason;
@@ -370,7 +370,7 @@ public function isFetchable(): ?bool
370370
return $this->fetchable;
371371
}
372372

373-
public function withFetchable($fetchable): self
373+
public function withFetchable($fetchable): static
374374
{
375375
$self = clone $this;
376376
$self->fetchable = $fetchable;
@@ -383,7 +383,7 @@ public function getFetchEager(): ?bool
383383
return $this->fetchEager;
384384
}
385385

386-
public function withFetchEager($fetchEager): self
386+
public function withFetchEager($fetchEager): static
387387
{
388388
$self = clone $this;
389389
$self->fetchEager = $fetchEager;
@@ -396,7 +396,7 @@ public function getJsonldContext(): ?array
396396
return $this->jsonldContext;
397397
}
398398

399-
public function withJsonldContext($jsonldContext): self
399+
public function withJsonldContext($jsonldContext): static
400400
{
401401
$self = clone $this;
402402
$self->jsonldContext = $jsonldContext;
@@ -409,7 +409,7 @@ public function getOpenapiContext(): ?array
409409
return $this->openapiContext;
410410
}
411411

412-
public function withOpenapiContext($openapiContext): self
412+
public function withOpenapiContext($openapiContext): static
413413
{
414414
$self = clone $this;
415415
$self->openapiContext = $openapiContext;
@@ -422,7 +422,7 @@ public function getJsonSchemaContext(): ?array
422422
return $this->jsonSchemaContext;
423423
}
424424

425-
public function withJsonSchemaContext($jsonSchemaContext): self
425+
public function withJsonSchemaContext($jsonSchemaContext): static
426426
{
427427
$self = clone $this;
428428
$self->jsonSchemaContext = $jsonSchemaContext;
@@ -435,7 +435,7 @@ public function getPush(): ?bool
435435
return $this->push;
436436
}
437437

438-
public function withPush($push): self
438+
public function withPush($push): static
439439
{
440440
$self = clone $this;
441441
$self->push = $push;
@@ -448,7 +448,7 @@ public function getSecurity(): ?string
448448
return $this->security instanceof \Stringable ? (string) $this->security : $this->security;
449449
}
450450

451-
public function withSecurity($security): self
451+
public function withSecurity($security): static
452452
{
453453
$self = clone $this;
454454
$self->security = $security;
@@ -461,7 +461,7 @@ public function getSecurityPostDenormalize(): ?string
461461
return $this->securityPostDenormalize instanceof \Stringable ? (string) $this->securityPostDenormalize : $this->securityPostDenormalize;
462462
}
463463

464-
public function withSecurityPostDenormalize($securityPostDenormalize): self
464+
public function withSecurityPostDenormalize($securityPostDenormalize): static
465465
{
466466
$self = clone $this;
467467
$self->securityPostDenormalize = $securityPostDenormalize;
@@ -477,7 +477,7 @@ public function getTypes(): ?array
477477
/**
478478
* @param string[]|string $types
479479
*/
480-
public function withTypes(array|string $types = []): self
480+
public function withTypes(array|string $types = []): static
481481
{
482482
$self = clone $this;
483483
$self->types = (array) $types;
@@ -496,7 +496,7 @@ public function getBuiltinTypes(): ?array
496496
/**
497497
* @param Type[] $builtinTypes
498498
*/
499-
public function withBuiltinTypes(array $builtinTypes = []): self
499+
public function withBuiltinTypes(array $builtinTypes = []): static
500500
{
501501
$self = clone $this;
502502
$self->builtinTypes = $builtinTypes;
@@ -509,15 +509,15 @@ public function getSchema(): ?array
509509
return $this->schema;
510510
}
511511

512-
public function withSchema(array $schema = []): self
512+
public function withSchema(array $schema = []): static
513513
{
514514
$self = clone $this;
515515
$self->schema = $schema;
516516

517517
return $self;
518518
}
519519

520-
public function withInitializable(?bool $initializable): self
520+
public function withInitializable(?bool $initializable): static
521521
{
522522
$self = clone $this;
523523
$self->initializable = $initializable;
@@ -535,7 +535,7 @@ public function getExtraProperties(): ?array
535535
return $this->extraProperties;
536536
}
537537

538-
public function withExtraProperties(array $extraProperties = []): self
538+
public function withExtraProperties(array $extraProperties = []): static
539539
{
540540
$self = clone $this;
541541
$self->extraProperties = $extraProperties;
@@ -556,7 +556,7 @@ public function getIris()
556556
*
557557
* @param string|string[] $iris
558558
*/
559-
public function withIris(string|array $iris): self
559+
public function withIris(string|array $iris): static
560560
{
561561
$metadata = clone $this;
562562
$metadata->iris = (array) $iris;
@@ -572,7 +572,7 @@ public function getGenId()
572572
return $this->genId;
573573
}
574574

575-
public function withGenId(bool $genId): self
575+
public function withGenId(bool $genId): static
576576
{
577577
$metadata = clone $this;
578578
$metadata->genId = $genId;
@@ -590,7 +590,7 @@ public function getUriTemplate(): ?string
590590
return $this->uriTemplate;
591591
}
592592

593-
public function withUriTemplate(?string $uriTemplate): self
593+
public function withUriTemplate(?string $uriTemplate): static
594594
{
595595
$metadata = clone $this;
596596
$metadata->uriTemplate = $uriTemplate;

0 commit comments

Comments
 (0)