Skip to content

Commit b968ccd

Browse files
authoredJan 27, 2025··
feat(openapi): document error outputs using json-schemas (#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

+24-2
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

+9-4
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

+4
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

+27-27
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;

‎src/Metadata/ApiResource.php

+30-27
Original file line numberDiff line numberDiff line change
@@ -1027,7 +1027,7 @@ public function getOperations(): ?Operations
10271027
return $this->operations;
10281028
}
10291029

1030-
public function withOperations(Operations $operations): self
1030+
public function withOperations(Operations $operations): static
10311031
{
10321032
$self = clone $this;
10331033
$self->operations = $operations;
@@ -1041,7 +1041,7 @@ public function getUriTemplate(): ?string
10411041
return $this->uriTemplate;
10421042
}
10431043

1044-
public function withUriTemplate(string $uriTemplate): self
1044+
public function withUriTemplate(string $uriTemplate): static
10451045
{
10461046
$self = clone $this;
10471047
$self->uriTemplate = $uriTemplate;
@@ -1057,7 +1057,7 @@ public function getTypes(): ?array
10571057
/**
10581058
* @param string[]|string $types
10591059
*/
1060-
public function withTypes(array|string $types): self
1060+
public function withTypes(array|string $types): static
10611061
{
10621062
$self = clone $this;
10631063
$self->types = (array) $types;
@@ -1073,7 +1073,7 @@ public function getFormats()
10731073
return $this->formats;
10741074
}
10751075

1076-
public function withFormats(mixed $formats): self
1076+
public function withFormats(mixed $formats): static
10771077
{
10781078
$self = clone $this;
10791079
$self->formats = $formats;
@@ -1092,7 +1092,7 @@ public function getInputFormats()
10921092
/**
10931093
* @param mixed|null $inputFormats
10941094
*/
1095-
public function withInputFormats($inputFormats): self
1095+
public function withInputFormats($inputFormats): static
10961096
{
10971097
$self = clone $this;
10981098
$self->inputFormats = $inputFormats;
@@ -1111,7 +1111,7 @@ public function getOutputFormats()
11111111
/**
11121112
* @param mixed|null $outputFormats
11131113
*/
1114-
public function withOutputFormats($outputFormats): self
1114+
public function withOutputFormats($outputFormats): static
11151115
{
11161116
$self = clone $this;
11171117
$self->outputFormats = $outputFormats;
@@ -1130,7 +1130,7 @@ public function getUriVariables()
11301130
/**
11311131
* @param array<string, Link>|array<string, array>|string[]|string|null $uriVariables
11321132
*/
1133-
public function withUriVariables($uriVariables): self
1133+
public function withUriVariables($uriVariables): static
11341134
{
11351135
$self = clone $this;
11361136
$self->uriVariables = $uriVariables;
@@ -1143,7 +1143,7 @@ public function getRoutePrefix(): ?string
11431143
return $this->routePrefix;
11441144
}
11451145

1146-
public function withRoutePrefix(string $routePrefix): self
1146+
public function withRoutePrefix(string $routePrefix): static
11471147
{
11481148
$self = clone $this;
11491149
$self->routePrefix = $routePrefix;
@@ -1156,7 +1156,7 @@ public function getDefaults(): ?array
11561156
return $this->defaults;
11571157
}
11581158

1159-
public function withDefaults(array $defaults): self
1159+
public function withDefaults(array $defaults): static
11601160
{
11611161
$self = clone $this;
11621162
$self->defaults = $defaults;
@@ -1169,7 +1169,7 @@ public function getRequirements(): ?array
11691169
return $this->requirements;
11701170
}
11711171

1172-
public function withRequirements(array $requirements): self
1172+
public function withRequirements(array $requirements): static
11731173
{
11741174
$self = clone $this;
11751175
$self->requirements = $requirements;
@@ -1182,7 +1182,7 @@ public function getOptions(): ?array
11821182
return $this->options;
11831183
}
11841184

1185-
public function withOptions(array $options): self
1185+
public function withOptions(array $options): static
11861186
{
11871187
$self = clone $this;
11881188
$self->options = $options;
@@ -1195,7 +1195,7 @@ public function getStateless(): ?bool
11951195
return $this->stateless;
11961196
}
11971197

1198-
public function withStateless(bool $stateless): self
1198+
public function withStateless(bool $stateless): static
11991199
{
12001200
$self = clone $this;
12011201
$self->stateless = $stateless;
@@ -1208,7 +1208,7 @@ public function getSunset(): ?string
12081208
return $this->sunset;
12091209
}
12101210

1211-
public function withSunset(string $sunset): self
1211+
public function withSunset(string $sunset): static
12121212
{
12131213
$self = clone $this;
12141214
$self->sunset = $sunset;
@@ -1221,7 +1221,7 @@ public function getAcceptPatch(): ?string
12211221
return $this->acceptPatch;
12221222
}
12231223

1224-
public function withAcceptPatch(string $acceptPatch): self
1224+
public function withAcceptPatch(string $acceptPatch): static
12251225
{
12261226
$self = clone $this;
12271227
$self->acceptPatch = $acceptPatch;
@@ -1234,7 +1234,10 @@ public function getStatus(): ?int
12341234
return $this->status;
12351235
}
12361236

1237-
public function withStatus($status): self
1237+
/**
1238+
* @param int $status
1239+
*/
1240+
public function withStatus($status): static
12381241
{
12391242
$self = clone $this;
12401243
$self->status = $status;
@@ -1247,7 +1250,7 @@ public function getHost(): ?string
12471250
return $this->host;
12481251
}
12491252

1250-
public function withHost(string $host): self
1253+
public function withHost(string $host): static
12511254
{
12521255
$self = clone $this;
12531256
$self->host = $host;
@@ -1260,7 +1263,7 @@ public function getSchemes(): ?array
12601263
return $this->schemes;
12611264
}
12621265

1263-
public function withSchemes(array $schemes): self
1266+
public function withSchemes(array $schemes): static
12641267
{
12651268
$self = clone $this;
12661269
$self->schemes = $schemes;
@@ -1273,7 +1276,7 @@ public function getCondition(): ?string
12731276
return $this->condition;
12741277
}
12751278

1276-
public function withCondition(string $condition): self
1279+
public function withCondition(string $condition): static
12771280
{
12781281
$self = clone $this;
12791282
$self->condition = $condition;
@@ -1286,7 +1289,7 @@ public function getController(): ?string
12861289
return $this->controller;
12871290
}
12881291

1289-
public function withController(string $controller): self
1292+
public function withController(string $controller): static
12901293
{
12911294
$self = clone $this;
12921295
$self->controller = $controller;
@@ -1299,7 +1302,7 @@ public function getHeaders(): ?array
12991302
return $this->headers;
13001303
}
13011304

1302-
public function withHeaders(array $headers): self
1305+
public function withHeaders(array $headers): static
13031306
{
13041307
$self = clone $this;
13051308
$self->headers = $headers;
@@ -1312,7 +1315,7 @@ public function getCacheHeaders(): ?array
13121315
return $this->cacheHeaders;
13131316
}
13141317

1315-
public function withCacheHeaders(array $cacheHeaders): self
1318+
public function withCacheHeaders(array $cacheHeaders): static
13161319
{
13171320
$self = clone $this;
13181321
$self->cacheHeaders = $cacheHeaders;
@@ -1328,7 +1331,7 @@ public function getHydraContext(): ?array
13281331
return $this->hydraContext;
13291332
}
13301333

1331-
public function withHydraContext(array $hydraContext): self
1334+
public function withHydraContext(array $hydraContext): static
13321335
{
13331336
$self = clone $this;
13341337
$self->hydraContext = $hydraContext;
@@ -1341,7 +1344,7 @@ public function getOpenapi(): bool|OpenApiOperation|null
13411344
return $this->openapi;
13421345
}
13431346

1344-
public function withOpenapi(bool|OpenApiOperation $openapi): self
1347+
public function withOpenapi(bool|OpenApiOperation $openapi): static
13451348
{
13461349
$self = clone $this;
13471350
$self->openapi = $openapi;
@@ -1354,7 +1357,7 @@ public function getPaginationViaCursor(): ?array
13541357
return $this->paginationViaCursor;
13551358
}
13561359

1357-
public function withPaginationViaCursor(array $paginationViaCursor): self
1360+
public function withPaginationViaCursor(array $paginationViaCursor): static
13581361
{
13591362
$self = clone $this;
13601363
$self->paginationViaCursor = $paginationViaCursor;
@@ -1367,7 +1370,7 @@ public function getExceptionToStatus(): ?array
13671370
return $this->exceptionToStatus;
13681371
}
13691372

1370-
public function withExceptionToStatus(array $exceptionToStatus): self
1373+
public function withExceptionToStatus(array $exceptionToStatus): static
13711374
{
13721375
$self = clone $this;
13731376
$self->exceptionToStatus = $exceptionToStatus;
@@ -1383,7 +1386,7 @@ public function getGraphQlOperations(): ?array
13831386
return $this->graphQlOperations;
13841387
}
13851388

1386-
public function withGraphQlOperations(array $graphQlOperations): self
1389+
public function withGraphQlOperations(array $graphQlOperations): static
13871390
{
13881391
$self = clone $this;
13891392
$self->graphQlOperations = $graphQlOperations;
@@ -1399,7 +1402,7 @@ public function getLinks(): ?array
13991402
/**
14001403
* @param Link[] $links
14011404
*/
1402-
public function withLinks(array $links): self
1405+
public function withLinks(array $links): static
14031406
{
14041407
$self = clone $this;
14051408
$self->links = $links;

‎src/Metadata/HttpOperation.php

+27-27
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ public function getMethod(): string
270270
return $this->method;
271271
}
272272

273-
public function withMethod(string $method): self
273+
public function withMethod(string $method): static
274274
{
275275
$self = clone $this;
276276
$self->method = $method;
@@ -299,7 +299,7 @@ public function getTypes(): ?array
299299
/**
300300
* @param string[]|string $types
301301
*/
302-
public function withTypes($types): self
302+
public function withTypes($types): static
303303
{
304304
$self = clone $this;
305305
$self->types = (array) $types;
@@ -312,7 +312,7 @@ public function getFormats()
312312
return $this->formats;
313313
}
314314

315-
public function withFormats($formats = null): self
315+
public function withFormats($formats = null): static
316316
{
317317
$self = clone $this;
318318
$self->formats = $formats;
@@ -325,7 +325,7 @@ public function getInputFormats()
325325
return $this->inputFormats;
326326
}
327327

328-
public function withInputFormats($inputFormats = null): self
328+
public function withInputFormats($inputFormats = null): static
329329
{
330330
$self = clone $this;
331331
$self->inputFormats = $inputFormats;
@@ -338,7 +338,7 @@ public function getOutputFormats()
338338
return $this->outputFormats;
339339
}
340340

341-
public function withOutputFormats($outputFormats = null): self
341+
public function withOutputFormats($outputFormats = null): static
342342
{
343343
$self = clone $this;
344344
$self->outputFormats = $outputFormats;
@@ -351,7 +351,7 @@ public function getUriVariables()
351351
return $this->uriVariables;
352352
}
353353

354-
public function withUriVariables($uriVariables): self
354+
public function withUriVariables($uriVariables): static
355355
{
356356
$self = clone $this;
357357
$self->uriVariables = $uriVariables;
@@ -364,7 +364,7 @@ public function getRoutePrefix(): ?string
364364
return $this->routePrefix;
365365
}
366366

367-
public function withRoutePrefix(string $routePrefix): self
367+
public function withRoutePrefix(string $routePrefix): static
368368
{
369369
$self = clone $this;
370370
$self->routePrefix = $routePrefix;
@@ -377,7 +377,7 @@ public function getRouteName(): ?string
377377
return $this->routeName;
378378
}
379379

380-
public function withRouteName(?string $routeName): self
380+
public function withRouteName(?string $routeName): static
381381
{
382382
$self = clone $this;
383383
$self->routeName = $routeName;
@@ -390,7 +390,7 @@ public function getDefaults(): ?array
390390
return $this->defaults;
391391
}
392392

393-
public function withDefaults(array $defaults): self
393+
public function withDefaults(array $defaults): static
394394
{
395395
$self = clone $this;
396396
$self->defaults = $defaults;
@@ -403,7 +403,7 @@ public function getRequirements(): ?array
403403
return $this->requirements;
404404
}
405405

406-
public function withRequirements(array $requirements): self
406+
public function withRequirements(array $requirements): static
407407
{
408408
$self = clone $this;
409409
$self->requirements = $requirements;
@@ -416,7 +416,7 @@ public function getOptions(): ?array
416416
return $this->options;
417417
}
418418

419-
public function withOptions(array $options): self
419+
public function withOptions(array $options): static
420420
{
421421
$self = clone $this;
422422
$self->options = $options;
@@ -429,7 +429,7 @@ public function getStateless(): ?bool
429429
return $this->stateless;
430430
}
431431

432-
public function withStateless($stateless): self
432+
public function withStateless($stateless): static
433433
{
434434
$self = clone $this;
435435
$self->stateless = $stateless;
@@ -442,7 +442,7 @@ public function getSunset(): ?string
442442
return $this->sunset;
443443
}
444444

445-
public function withSunset(string $sunset): self
445+
public function withSunset(string $sunset): static
446446
{
447447
$self = clone $this;
448448
$self->sunset = $sunset;
@@ -455,7 +455,7 @@ public function getAcceptPatch(): ?string
455455
return $this->acceptPatch;
456456
}
457457

458-
public function withAcceptPatch(string $acceptPatch): self
458+
public function withAcceptPatch(string $acceptPatch): static
459459
{
460460
$self = clone $this;
461461
$self->acceptPatch = $acceptPatch;
@@ -468,7 +468,7 @@ public function getStatus(): ?int
468468
return $this->status;
469469
}
470470

471-
public function withStatus(int $status): self
471+
public function withStatus(int $status): static
472472
{
473473
$self = clone $this;
474474
$self->status = $status;
@@ -481,7 +481,7 @@ public function getHost(): ?string
481481
return $this->host;
482482
}
483483

484-
public function withHost(string $host): self
484+
public function withHost(string $host): static
485485
{
486486
$self = clone $this;
487487
$self->host = $host;
@@ -494,7 +494,7 @@ public function getSchemes(): ?array
494494
return $this->schemes;
495495
}
496496

497-
public function withSchemes(array $schemes): self
497+
public function withSchemes(array $schemes): static
498498
{
499499
$self = clone $this;
500500
$self->schemes = $schemes;
@@ -507,7 +507,7 @@ public function getCondition(): ?string
507507
return $this->condition;
508508
}
509509

510-
public function withCondition(string $condition): self
510+
public function withCondition(string $condition): static
511511
{
512512
$self = clone $this;
513513
$self->condition = $condition;
@@ -520,7 +520,7 @@ public function getController(): ?string
520520
return $this->controller;
521521
}
522522

523-
public function withController(string $controller): self
523+
public function withController(string $controller): static
524524
{
525525
$self = clone $this;
526526
$self->controller = $controller;
@@ -533,7 +533,7 @@ public function getHeaders(): ?array
533533
return $this->headers;
534534
}
535535

536-
public function withHeaders(array $headers): self
536+
public function withHeaders(array $headers): static
537537
{
538538
$self = clone $this;
539539
$self->headers = $headers;
@@ -546,7 +546,7 @@ public function getCacheHeaders(): ?array
546546
return $this->cacheHeaders;
547547
}
548548

549-
public function withCacheHeaders(array $cacheHeaders): self
549+
public function withCacheHeaders(array $cacheHeaders): static
550550
{
551551
$self = clone $this;
552552
$self->cacheHeaders = $cacheHeaders;
@@ -559,7 +559,7 @@ public function getPaginationViaCursor(): ?array
559559
return $this->paginationViaCursor;
560560
}
561561

562-
public function withPaginationViaCursor(array $paginationViaCursor): self
562+
public function withPaginationViaCursor(array $paginationViaCursor): static
563563
{
564564
$self = clone $this;
565565
$self->paginationViaCursor = $paginationViaCursor;
@@ -572,7 +572,7 @@ public function getHydraContext(): ?array
572572
return $this->hydraContext;
573573
}
574574

575-
public function withHydraContext(array $hydraContext): self
575+
public function withHydraContext(array $hydraContext): static
576576
{
577577
$self = clone $this;
578578
$self->hydraContext = $hydraContext;
@@ -585,7 +585,7 @@ public function getOpenapi(): bool|OpenApiOperation|Webhook|null
585585
return $this->openapi;
586586
}
587587

588-
public function withOpenapi(bool|OpenApiOperation|Webhook $openapi): self
588+
public function withOpenapi(bool|OpenApiOperation|Webhook $openapi): static
589589
{
590590
$self = clone $this;
591591
$self->openapi = $openapi;
@@ -598,7 +598,7 @@ public function getExceptionToStatus(): ?array
598598
return $this->exceptionToStatus;
599599
}
600600

601-
public function withExceptionToStatus(array $exceptionToStatus): self
601+
public function withExceptionToStatus(array $exceptionToStatus): static
602602
{
603603
$self = clone $this;
604604
$self->exceptionToStatus = $exceptionToStatus;
@@ -614,7 +614,7 @@ public function getLinks(): ?array
614614
/**
615615
* @param WebLink[] $links
616616
*/
617-
public function withLinks(array $links): self
617+
public function withLinks(array $links): static
618618
{
619619
$self = clone $this;
620620
$self->links = $links;
@@ -630,7 +630,7 @@ public function getErrors(): ?array
630630
/**
631631
* @param class-string<ProblemExceptionInterface>[] $errors
632632
*/
633-
public function withErrors(array $errors): self
633+
public function withErrors(array $errors): static
634634
{
635635
$self = clone $this;
636636
$self->errors = $errors;

‎src/Metadata/Operation.php

+7-7
Original file line numberDiff line numberDiff line change
@@ -874,7 +874,7 @@ public function canRead(): ?bool
874874
return $this->read;
875875
}
876876

877-
public function withRead(bool $read = true): self
877+
public function withRead(bool $read = true): static
878878
{
879879
$self = clone $this;
880880
$self->read = $read;
@@ -887,7 +887,7 @@ public function canDeserialize(): ?bool
887887
return $this->deserialize;
888888
}
889889

890-
public function withDeserialize(bool $deserialize = true): self
890+
public function withDeserialize(bool $deserialize = true): static
891891
{
892892
$self = clone $this;
893893
$self->deserialize = $deserialize;
@@ -900,7 +900,7 @@ public function canValidate(): ?bool
900900
return $this->validate;
901901
}
902902

903-
public function withValidate(bool $validate = true): self
903+
public function withValidate(bool $validate = true): static
904904
{
905905
$self = clone $this;
906906
$self->validate = $validate;
@@ -913,7 +913,7 @@ public function canWrite(): ?bool
913913
return $this->write;
914914
}
915915

916-
public function withWrite(bool $write = true): self
916+
public function withWrite(bool $write = true): static
917917
{
918918
$self = clone $this;
919919
$self->write = $write;
@@ -926,7 +926,7 @@ public function canSerialize(): ?bool
926926
return $this->serialize;
927927
}
928928

929-
public function withSerialize(bool $serialize = true): self
929+
public function withSerialize(bool $serialize = true): static
930930
{
931931
$self = clone $this;
932932
$self->serialize = $serialize;
@@ -939,7 +939,7 @@ public function getPriority(): ?int
939939
return $this->priority;
940940
}
941941

942-
public function withPriority(int $priority = 0): self
942+
public function withPriority(int $priority = 0): static
943943
{
944944
$self = clone $this;
945945
$self->priority = $priority;
@@ -952,7 +952,7 @@ public function getName(): ?string
952952
return $this->name;
953953
}
954954

955-
public function withName(string $name = ''): self
955+
public function withName(string $name = ''): static
956956
{
957957
$self = clone $this;
958958
$self->name = $name;

‎src/Metadata/Parameter.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -248,15 +248,15 @@ public function withConstraints(mixed $constraints): static
248248
return $self;
249249
}
250250

251-
public function withSecurity(string|\Stringable|null $security): self
251+
public function withSecurity(string|\Stringable|null $security): static
252252
{
253253
$self = clone $this;
254254
$self->security = $security;
255255

256256
return $self;
257257
}
258258

259-
public function withSecurityMessage(?string $securityMessage): self
259+
public function withSecurityMessage(?string $securityMessage): static
260260
{
261261
$self = clone $this;
262262
$self->securityMessage = $securityMessage;

‎src/OpenApi/Factory/OpenApiFactory.php

+137-62
Large diffs are not rendered by default.

‎src/OpenApi/Tests/Factory/OpenApiFactoryTest.php

+169-77
Large diffs are not rendered by default.

‎src/OpenApi/Tests/Serializer/OpenApiNormalizerTest.php

+5
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@
4343
use ApiPlatform\OpenApi\Options;
4444
use ApiPlatform\OpenApi\Serializer\OpenApiNormalizer;
4545
use ApiPlatform\OpenApi\Tests\Fixtures\Dummy;
46+
use ApiPlatform\State\ApiResource\Error;
4647
use ApiPlatform\State\Pagination\PaginationOptions;
48+
use ApiPlatform\Validator\Exception\ValidationException;
4749
use PHPUnit\Framework\TestCase;
4850
use Prophecy\Argument;
4951
use Prophecy\PhpUnit\ProphecyTrait;
@@ -100,6 +102,7 @@ public function testNormalize(): void
100102
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
101103
$propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::any())->shouldBeCalled()->willReturn(new PropertyNameCollection(['id', 'name', 'description', 'dummyDate']));
102104
$propertyNameCollectionFactoryProphecy->create('Zorro', Argument::any())->shouldBeCalled()->willReturn(new PropertyNameCollection(['id']));
105+
$propertyNameCollectionFactoryProphecy->create(Error::class, Argument::any())->shouldBeCalled()->willReturn(new PropertyNameCollection([]));
103106

104107
$baseOperation = (new HttpOperation())->withTypes(['http://schema.example.com/Dummy'])
105108
->withInputFormats(self::OPERATION_FORMATS['input_formats'])->withOutputFormats(self::OPERATION_FORMATS['output_formats'])
@@ -141,6 +144,8 @@ public function testNormalize(): void
141144
$resourceCollectionMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);
142145
$resourceCollectionMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn($dummyMetadata);
143146
$resourceCollectionMetadataFactoryProphecy->create('Zorro')->shouldBeCalled()->willReturn($zorroMetadata);
147+
$resourceCollectionMetadataFactoryProphecy->create(Error::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(Error::class, []));
148+
$resourceCollectionMetadataFactoryProphecy->create(ValidationException::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(ValidationException::class, []));
144149

145150
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
146151
$propertyMetadataFactoryProphecy->create(Dummy::class, 'id', Argument::any())->shouldBeCalled()->willReturn(

‎src/State/ApiResource/Error.php

+61-10
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
use Symfony\Component\WebLink\Link;
2626

2727
#[ErrorResource(
28-
openapi: false,
2928
uriVariables: ['status'],
3029
uriTemplate: '/errors/{status}',
3130
operations: [
@@ -37,15 +36,17 @@
3736
normalizationContext: [
3837
'groups' => ['jsonproblem'],
3938
'skip_null_values' => true,
39+
'ignored_attributes' => ['trace', 'file', 'line', 'code', 'message', 'traceAsString', 'previous'],
4040
],
4141
),
4242
new Operation(
4343
name: '_api_errors_hydra',
4444
routeName: 'api_errors',
45-
outputFormats: ['jsonld' => ['application/problem+json']],
45+
outputFormats: ['jsonld' => ['application/problem+json', 'application/ld+json']],
4646
normalizationContext: [
4747
'groups' => ['jsonld'],
4848
'skip_null_values' => true,
49+
'ignored_attributes' => ['trace', 'file', 'line', 'code', 'message', 'traceAsString', 'previous'],
4950
],
5051
links: [new Link(rel: 'http://www.w3.org/ns/json-ld#error', href: 'http://www.w3.org/ns/hydra/error')],
5152
),
@@ -55,32 +56,46 @@
5556
hideHydraOperation: true,
5657
outputFormats: ['jsonapi' => ['application/vnd.api+json']],
5758
normalizationContext: [
59+
'disable_json_schema_serializer_groups' => false,
5860
'groups' => ['jsonapi'],
5961
'skip_null_values' => true,
62+
'ignored_attributes' => ['trace', 'file', 'line', 'code', 'message', 'traceAsString', 'previous'],
6063
],
6164
),
6265
new Operation(
6366
name: '_api_errors',
6467
routeName: 'api_errors',
6568
hideHydraOperation: true,
69+
openapi: false
6670
),
6771
],
6872
provider: 'api_platform.state.error_provider',
69-
graphQlOperations: []
73+
graphQlOperations: [],
74+
description: 'A representation of common errors.'
7075
)]
7176
#[ApiProperty(property: 'traceAsString', hydra: false)]
7277
#[ApiProperty(property: 'string', hydra: false)]
7378
class Error extends \Exception implements ProblemExceptionInterface, HttpExceptionInterface
7479
{
80+
private ?string $id = null;
81+
7582
public function __construct(
7683
private string $title,
7784
private string $detail,
78-
#[ApiProperty(identifier: true, writable: false, initializable: false)] private int $status,
85+
#[ApiProperty(
86+
description: 'The HTTP status code applicable to this problem.',
87+
identifier: true,
88+
writable: false,
89+
initializable: false,
90+
schema: ['type' => 'number', 'example' => 404, 'default' => 400]
91+
)] private int $status,
7992
?array $originalTrace = null,
8093
private ?string $instance = null,
8194
private string $type = 'about:blank',
8295
private array $headers = [],
8396
?\Throwable $previous = null,
97+
private ?array $meta = null,
98+
private ?array $source = null,
8499
) {
85100
parent::__construct($title, $status, $previous);
86101

@@ -98,7 +113,28 @@ public function __construct(
98113
#[Groups(['jsonapi'])]
99114
public function getId(): string
100115
{
101-
return (string) $this->status;
116+
return $this->id ?? ((string) $this->status);
117+
}
118+
119+
#[Groups(['jsonapi'])]
120+
#[ApiProperty(schema: ['type' => 'object'])]
121+
public function getMeta(): ?array
122+
{
123+
return $this->meta;
124+
}
125+
126+
#[Groups(['jsonapi'])]
127+
#[ApiProperty(schema: [
128+
'type' => 'object',
129+
'properties' => [
130+
'pointer' => ['type' => 'string'],
131+
'parameter' => ['type' => 'string'],
132+
'header' => ['type' => 'string'],
133+
],
134+
])]
135+
public function getSource(): ?array
136+
{
137+
return $this->source;
102138
}
103139

104140
#[SerializedName('trace')]
@@ -142,7 +178,7 @@ public function setHeaders(array $headers): void
142178
}
143179

144180
#[Groups(['jsonld', 'jsonproblem', 'jsonapi'])]
145-
#[ApiProperty(writable: false, initializable: false)]
181+
#[ApiProperty(writable: false, initializable: false, description: 'A URI reference that identifies the problem type')]
146182
public function getType(): string
147183
{
148184
return $this->type;
@@ -154,7 +190,7 @@ public function setType(string $type): void
154190
}
155191

156192
#[Groups(['jsonld', 'jsonproblem', 'jsonapi'])]
157-
#[ApiProperty(writable: false, initializable: false)]
193+
#[ApiProperty(writable: false, initializable: false, description: 'A short, human-readable summary of the problem.')]
158194
public function getTitle(): ?string
159195
{
160196
return $this->title;
@@ -177,7 +213,7 @@ public function setStatus(int $status): void
177213
}
178214

179215
#[Groups(['jsonld', 'jsonproblem', 'jsonapi'])]
180-
#[ApiProperty(writable: false, initializable: false)]
216+
#[ApiProperty(writable: false, initializable: false, description: 'A human-readable explanation specific to this occurrence of the problem.')]
181217
public function getDetail(): ?string
182218
{
183219
return $this->detail;
@@ -188,8 +224,8 @@ public function setDetail(?string $detail = null): void
188224
$this->detail = $detail;
189225
}
190226

191-
#[Groups(['jsonld', 'jsonproblem'])]
192-
#[ApiProperty(writable: false, initializable: false)]
227+
#[Groups(['jsonld', 'jsonproblem', 'jsonapi'])]
228+
#[ApiProperty(writable: false, initializable: false, description: 'A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced.')]
193229
public function getInstance(): ?string
194230
{
195231
return $this->instance;
@@ -199,4 +235,19 @@ public function setInstance(?string $instance = null): void
199235
{
200236
$this->instance = $instance;
201237
}
238+
239+
public function setId(?string $id = null): void
240+
{
241+
$this->id = $id;
242+
}
243+
244+
public function setMeta(?array $meta = null): void
245+
{
246+
$this->meta = $meta;
247+
}
248+
249+
public function setSource(?array $source = null): void
250+
{
251+
$this->source = $source;
252+
}
202253
}

‎src/Validator/Exception/ValidationException.php

+28-16
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface as SymfonyHttpExceptionInterface;
2424
use Symfony\Component\Serializer\Annotation\Groups;
2525
use Symfony\Component\Serializer\Annotation\SerializedName;
26+
use Symfony\Component\Validator\ConstraintViolationList;
2627
use Symfony\Component\Validator\ConstraintViolationListInterface;
2728
use Symfony\Component\WebLink\Link;
2829

@@ -34,32 +35,39 @@
3435
#[ErrorResource(
3536
uriTemplate: '/validation_errors/{id}',
3637
status: 422,
37-
openapi: false,
3838
uriVariables: ['id'],
3939
provider: 'api_platform.validator.state.error_provider',
4040
shortName: 'ConstraintViolation',
41+
description: 'Unprocessable entity',
4142
operations: [
4243
new ErrorOperation(
4344
name: '_api_validation_errors_problem',
4445
outputFormats: ['json' => ['application/problem+json']],
4546
normalizationContext: [
4647
'groups' => ['json'],
48+
'ignored_attributes' => ['trace', 'file', 'line', 'code', 'message', 'traceAsString', 'previous'],
4749
'skip_null_values' => true,
4850
]
4951
),
5052
new ErrorOperation(
5153
name: '_api_validation_errors_hydra',
52-
outputFormats: ['jsonld' => ['application/problem+json']],
54+
outputFormats: ['jsonld' => ['application/problem+json', 'application/ld+json']],
5355
links: [new Link(rel: 'http://www.w3.org/ns/json-ld#error', href: 'http://www.w3.org/ns/hydra/error')],
5456
normalizationContext: [
5557
'groups' => ['jsonld'],
58+
'ignored_attributes' => ['trace', 'file', 'line', 'code', 'message', 'traceAsString', 'previous'],
5659
'skip_null_values' => true,
5760
]
5861
),
5962
new ErrorOperation(
6063
name: '_api_validation_errors_jsonapi',
6164
outputFormats: ['jsonapi' => ['application/vnd.api+json']],
62-
normalizationContext: ['groups' => ['jsonapi'], 'skip_null_values' => true]
65+
normalizationContext: [
66+
'disable_json_schema_serializer_groups' => false,
67+
'groups' => ['jsonapi'],
68+
'skip_null_values' => true,
69+
'ignored_attributes' => ['trace', 'file', 'line', 'code', 'message', 'traceAsString', 'previous'],
70+
]
6371
),
6472
],
6573
graphQlOperations: []
@@ -70,21 +78,13 @@ class ValidationException extends RuntimeException implements ConstraintViolatio
7078
{
7179
private int $status = 422;
7280
protected ?string $errorTitle = null;
73-
private ConstraintViolationListInterface $constraintViolationList;
81+
private array|ConstraintViolationListInterface $constraintViolationList = [];
7482

75-
public function __construct(string|ConstraintViolationListInterface $message = '', string|int|null $code = null, int|\Throwable|null $previous = null, \Throwable|string|null $errorTitle = null)
83+
public function __construct(ConstraintViolationListInterface $message = new ConstraintViolationList(), string|int|null $code = null, int|\Throwable|null $previous = null, \Throwable|string|null $errorTitle = null)
7684
{
7785
$this->errorTitle = $errorTitle;
78-
79-
if ($message instanceof ConstraintViolationListInterface) {
80-
$this->constraintViolationList = $message;
81-
parent::__construct($this->__toString(), $code ?? 0, $previous);
82-
83-
return;
84-
}
85-
86-
trigger_deprecation('api_platform/core', '3.3', \sprintf('The "%s" exception will have a "%s" first argument in 4.x.', self::class, ConstraintViolationListInterface::class));
87-
parent::__construct($message ?: $this->__toString(), $code ?? 0, $previous);
86+
$this->constraintViolationList = $message;
87+
parent::__construct($this->__toString(), $code ?? 0, $previous);
8888
}
8989

9090
/**
@@ -166,7 +166,19 @@ public function getInstance(): ?string
166166

167167
#[SerializedName('violations')]
168168
#[Groups(['json', 'jsonld'])]
169-
#[ApiProperty(jsonldContext: ['@type' => 'ConstraintViolationList'])]
169+
#[ApiProperty(
170+
jsonldContext: ['@type' => 'ConstraintViolationList'],
171+
schema: [
172+
'type' => 'array',
173+
'items' => [
174+
'type' => 'object',
175+
'properties' => [
176+
'propertyPath' => ['type' => 'string', 'description' => 'The property path of the violation'],
177+
'message' => ['type' => 'string', 'description' => 'The message associated with the violation'],
178+
],
179+
],
180+
]
181+
)]
170182
public function getConstraintViolationList(): ConstraintViolationListInterface
171183
{
172184
return $this->constraintViolationList;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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;
15+
16+
use ApiPlatform\Metadata\ApiResource;
17+
18+
#[ApiResource]
19+
class Crud
20+
{
21+
public string $id;
22+
}

‎tests/Functional/OpenApiTest.php

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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\Functional;
15+
16+
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
17+
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Crud;
18+
use ApiPlatform\Tests\SetupClassResourcesTrait;
19+
20+
class OpenApiTest extends ApiTestCase
21+
{
22+
use SetupClassResourcesTrait;
23+
24+
/**
25+
* @return class-string[]
26+
*/
27+
public static function getResources(): array
28+
{
29+
return [Crud::class];
30+
}
31+
32+
public function testErrorsAreDocumented(): void
33+
{
34+
$container = static::getContainer();
35+
$response = self::createClient()->request('GET', '/docs', [
36+
'headers' => ['Accept' => 'application/vnd.openapi+json'],
37+
]);
38+
39+
$res = $response->toArray();
40+
$this->assertTrue(isset($res['paths']['/cruds/{id}']['patch']['responses']));
41+
$responses = $res['paths']['/cruds/{id}']['patch']['responses'];
42+
43+
foreach ($responses as $status => $response) {
44+
if ($status < 400) {
45+
continue;
46+
}
47+
48+
$this->assertArrayHasKey('application/problem+json', $response['content']);
49+
$this->assertArrayHasKey('application/ld+json', $response['content']);
50+
$this->assertArrayHasKey('application/vnd.api+json', $response['content']);
51+
52+
match ($status) {
53+
422 => $this->assertStringStartsWith('#/components/schemas/ConstraintViolation', $response['content']['application/problem+json']['schema']['$ref']),
54+
default => $this->assertStringStartsWith('#/components/schemas/Error', $response['content']['application/problem+json']['schema']['$ref']),
55+
};
56+
}
57+
58+
// problem detail https://datatracker.ietf.org/doc/html/rfc7807#section-3.1
59+
foreach (['title', 'detail', 'instance', 'type', 'status'] as $key) {
60+
$this->assertArrayHasKey($key, $res['components']['schemas']['Error.jsonld-jsonproblem']['properties']);
61+
}
62+
63+
foreach (['title', 'detail', 'instance', 'type', 'status', '@id', '@type', '@context'] as $key) {
64+
$this->assertArrayHasKey($key, $res['components']['schemas']['Error.jsonld-jsonld']['properties']);
65+
}
66+
foreach (['id', 'title', 'detail', 'instance', 'type', 'status', 'meta', 'source'] as $key) {
67+
$this->assertArrayHasKey($key, $res['components']['schemas']['Error.jsonapi-jsonapi']['properties']['errors']['properties']);
68+
}
69+
}
70+
}

‎tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php

+2-3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Question;
3535
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
3636
use ApiPlatform\Tests\SetupClassResourcesTrait;
37+
use PHPUnit\Framework\Attributes\DataProvider;
3738
use Symfony\Bundle\FrameworkBundle\Console\Application;
3839
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
3940
use Symfony\Component\Console\Tester\ApplicationTester;
@@ -378,9 +379,7 @@ public function testGenId(): void
378379
$this->assertArrayNotHasKey('@id', $json['definitions']['DisableIdGenerationItem.jsonld']['properties']);
379380
}
380381

381-
/**
382-
* @dataProvider arrayPropertyTypeSyntaxProvider
383-
*/
382+
#[DataProvider('arrayPropertyTypeSyntaxProvider')]
384383
public function testOpenApiSchemaGenerationForArrayProperty(string $propertyName, array $expectedProperties): void
385384
{
386385
$this->tester->run([

0 commit comments

Comments
 (0)
Please sign in to comment.