Skip to content

Commit b968ccd

Browse files
authored
feat(openapi): document error outputs using json-schemas (api-platform#6923)
1 parent 4a85f30 commit b968ccd

File tree

16 files changed

+624
-264
lines changed

16 files changed

+624
-264
lines changed

src/JsonApi/JsonSchema/SchemaFactory.php

Lines changed: 24 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,27 @@ 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+
$serializerContext ??= $this->getSerializerContext($operation ?? $this->findOperation($className, $type, $operation, $serializerContext, $format), $type);
137+
$jsonApiSerializerContext = !($serializerContext[self::DISABLE_JSON_SCHEMA_SERIALIZER_GROUPS] ?? true) ? $serializerContext : [];
138+
$schema = $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $jsonApiSerializerContext, $forceCollection);
128139

129140
if (($key = $schema->getRootDefinitionKey()) || ($key = $schema->getItemsDefinitionKey())) {
130141
$definitions = $schema->getDefinitions();
131142
$properties = $definitions[$key]['properties'] ?? [];
132143

144+
if (Error::class === $className && !isset($properties['errors'])) {
145+
$definitions[$key]['properties'] = [
146+
'errors' => [
147+
'type' => 'object',
148+
'properties' => $properties,
149+
],
150+
];
151+
152+
return $schema;
153+
}
154+
133155
// Prevent reapplying
134-
if (isset($properties['id'], $properties['type']) || isset($properties['data'])) {
156+
if (isset($properties['id'], $properties['type']) || isset($properties['data']) || isset($properties['errors'])) {
135157
return $schema;
136158
}
137159

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/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyMetadataFactory.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ public function __construct(
4646
*/
4747
public function create(string $resourceClass, string $property, array $options = []): ApiProperty
4848
{
49+
if (!is_a($resourceClass, Model::class, true)) {
50+
return $this->decorated?->create($resourceClass, $property, $options) ?? new ApiProperty();
51+
}
52+
4953
try {
5054
$refl = new \ReflectionClass($resourceClass);
5155
$model = $refl->newInstanceWithoutConstructor();

src/Metadata/ApiProperty.php

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ public function getProperty(): ?string
231231
return $this->property;
232232
}
233233

234-
public function withProperty(string $property): self
234+
public function withProperty(string $property): static
235235
{
236236
$self = clone $this;
237237
$self->property = $property;
@@ -244,7 +244,7 @@ public function getDescription(): ?string
244244
return $this->description;
245245
}
246246

247-
public function withDescription(string $description): self
247+
public function withDescription(string $description): static
248248
{
249249
$self = clone $this;
250250
$self->description = $description;
@@ -257,7 +257,7 @@ public function isReadable(): ?bool
257257
return $this->readable;
258258
}
259259

260-
public function withReadable(bool $readable): self
260+
public function withReadable(bool $readable): static
261261
{
262262
$self = clone $this;
263263
$self->readable = $readable;
@@ -270,7 +270,7 @@ public function isWritable(): ?bool
270270
return $this->writable;
271271
}
272272

273-
public function withWritable(bool $writable): self
273+
public function withWritable(bool $writable): static
274274
{
275275
$self = clone $this;
276276
$self->writable = $writable;
@@ -283,7 +283,7 @@ public function isReadableLink(): ?bool
283283
return $this->readableLink;
284284
}
285285

286-
public function withReadableLink(bool $readableLink): self
286+
public function withReadableLink(bool $readableLink): static
287287
{
288288
$self = clone $this;
289289
$self->readableLink = $readableLink;
@@ -296,7 +296,7 @@ public function isWritableLink(): ?bool
296296
return $this->writableLink;
297297
}
298298

299-
public function withWritableLink(bool $writableLink): self
299+
public function withWritableLink(bool $writableLink): static
300300
{
301301
$self = clone $this;
302302
$self->writableLink = $writableLink;
@@ -309,7 +309,7 @@ public function isRequired(): ?bool
309309
return $this->required;
310310
}
311311

312-
public function withRequired(bool $required): self
312+
public function withRequired(bool $required): static
313313
{
314314
$self = clone $this;
315315
$self->required = $required;
@@ -322,7 +322,7 @@ public function isIdentifier(): ?bool
322322
return $this->identifier;
323323
}
324324

325-
public function withIdentifier(bool $identifier): self
325+
public function withIdentifier(bool $identifier): static
326326
{
327327
$self = clone $this;
328328
$self->identifier = $identifier;
@@ -335,7 +335,7 @@ public function getDefault()
335335
return $this->default;
336336
}
337337

338-
public function withDefault($default): self
338+
public function withDefault($default): static
339339
{
340340
$self = clone $this;
341341
$self->default = $default;
@@ -348,7 +348,7 @@ public function getExample(): mixed
348348
return $this->example;
349349
}
350350

351-
public function withExample(mixed $example): self
351+
public function withExample(mixed $example): static
352352
{
353353
$self = clone $this;
354354
$self->example = $example;
@@ -361,7 +361,7 @@ public function getDeprecationReason(): ?string
361361
return $this->deprecationReason;
362362
}
363363

364-
public function withDeprecationReason($deprecationReason): self
364+
public function withDeprecationReason($deprecationReason): static
365365
{
366366
$self = clone $this;
367367
$self->deprecationReason = $deprecationReason;
@@ -374,7 +374,7 @@ public function isFetchable(): ?bool
374374
return $this->fetchable;
375375
}
376376

377-
public function withFetchable($fetchable): self
377+
public function withFetchable($fetchable): static
378378
{
379379
$self = clone $this;
380380
$self->fetchable = $fetchable;
@@ -387,7 +387,7 @@ public function getFetchEager(): ?bool
387387
return $this->fetchEager;
388388
}
389389

390-
public function withFetchEager($fetchEager): self
390+
public function withFetchEager($fetchEager): static
391391
{
392392
$self = clone $this;
393393
$self->fetchEager = $fetchEager;
@@ -400,7 +400,7 @@ public function getJsonldContext(): ?array
400400
return $this->jsonldContext;
401401
}
402402

403-
public function withJsonldContext($jsonldContext): self
403+
public function withJsonldContext($jsonldContext): static
404404
{
405405
$self = clone $this;
406406
$self->jsonldContext = $jsonldContext;
@@ -413,7 +413,7 @@ public function getOpenapiContext(): ?array
413413
return $this->openapiContext;
414414
}
415415

416-
public function withOpenapiContext($openapiContext): self
416+
public function withOpenapiContext($openapiContext): static
417417
{
418418
$self = clone $this;
419419
$self->openapiContext = $openapiContext;
@@ -426,7 +426,7 @@ public function getJsonSchemaContext(): ?array
426426
return $this->jsonSchemaContext;
427427
}
428428

429-
public function withJsonSchemaContext($jsonSchemaContext): self
429+
public function withJsonSchemaContext($jsonSchemaContext): static
430430
{
431431
$self = clone $this;
432432
$self->jsonSchemaContext = $jsonSchemaContext;
@@ -439,7 +439,7 @@ public function getPush(): ?bool
439439
return $this->push;
440440
}
441441

442-
public function withPush($push): self
442+
public function withPush($push): static
443443
{
444444
$self = clone $this;
445445
$self->push = $push;
@@ -452,7 +452,7 @@ public function getSecurity(): ?string
452452
return $this->security instanceof \Stringable ? (string) $this->security : $this->security;
453453
}
454454

455-
public function withSecurity($security): self
455+
public function withSecurity($security): static
456456
{
457457
$self = clone $this;
458458
$self->security = $security;
@@ -465,7 +465,7 @@ public function getSecurityPostDenormalize(): ?string
465465
return $this->securityPostDenormalize instanceof \Stringable ? (string) $this->securityPostDenormalize : $this->securityPostDenormalize;
466466
}
467467

468-
public function withSecurityPostDenormalize($securityPostDenormalize): self
468+
public function withSecurityPostDenormalize($securityPostDenormalize): static
469469
{
470470
$self = clone $this;
471471
$self->securityPostDenormalize = $securityPostDenormalize;
@@ -481,7 +481,7 @@ public function getTypes(): ?array
481481
/**
482482
* @param string[]|string $types
483483
*/
484-
public function withTypes(array|string $types = []): self
484+
public function withTypes(array|string $types = []): static
485485
{
486486
$self = clone $this;
487487
$self->types = (array) $types;
@@ -500,7 +500,7 @@ public function getBuiltinTypes(): ?array
500500
/**
501501
* @param Type[] $builtinTypes
502502
*/
503-
public function withBuiltinTypes(array $builtinTypes = []): self
503+
public function withBuiltinTypes(array $builtinTypes = []): static
504504
{
505505
$self = clone $this;
506506
$self->builtinTypes = $builtinTypes;
@@ -513,15 +513,15 @@ public function getSchema(): ?array
513513
return $this->schema;
514514
}
515515

516-
public function withSchema(array $schema = []): self
516+
public function withSchema(array $schema = []): static
517517
{
518518
$self = clone $this;
519519
$self->schema = $schema;
520520

521521
return $self;
522522
}
523523

524-
public function withInitializable(bool $initializable): self
524+
public function withInitializable(?bool $initializable): static
525525
{
526526
$self = clone $this;
527527
$self->initializable = $initializable;
@@ -539,7 +539,7 @@ public function getExtraProperties(): ?array
539539
return $this->extraProperties;
540540
}
541541

542-
public function withExtraProperties(array $extraProperties = []): self
542+
public function withExtraProperties(array $extraProperties = []): static
543543
{
544544
$self = clone $this;
545545
$self->extraProperties = $extraProperties;
@@ -560,7 +560,7 @@ public function getIris()
560560
*
561561
* @param string|string[] $iris
562562
*/
563-
public function withIris(string|array $iris): self
563+
public function withIris(string|array $iris): static
564564
{
565565
$metadata = clone $this;
566566
$metadata->iris = (array) $iris;
@@ -576,7 +576,7 @@ public function getGenId()
576576
return $this->genId;
577577
}
578578

579-
public function withGenId(bool $genId): self
579+
public function withGenId(bool $genId): static
580580
{
581581
$metadata = clone $this;
582582
$metadata->genId = $genId;
@@ -594,7 +594,7 @@ public function getUriTemplate(): ?string
594594
return $this->uriTemplate;
595595
}
596596

597-
public function withUriTemplate(?string $uriTemplate): self
597+
public function withUriTemplate(?string $uriTemplate): static
598598
{
599599
$metadata = clone $this;
600600
$metadata->uriTemplate = $uriTemplate;

0 commit comments

Comments
 (0)