Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(openapi): document error outputs using json-schemas #6923

Merged
merged 1 commit into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions src/JsonApi/JsonSchema/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\ResourceClassResolverInterface;
use ApiPlatform\State\ApiResource\Error;

/**
* Decorator factory which adds JSON:API properties to the JSON Schema document.
Expand All @@ -31,6 +32,14 @@
final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareInterface
{
use ResourceMetadataTrait;

/**
* As JSON:API recommends using [includes](https://jsonapi.org/format/#fetching-includes) instead of groups
* this flag allows to force using groups to generate the JSON:API JSONSchema. Defaults to true, use it in
* a serializer context.
*/
public const DISABLE_JSON_SCHEMA_SERIALIZER_GROUPS = 'disable_json_schema_serializer_groups';

private const LINKS_PROPS = [
'type' => 'object',
'properties' => [
Expand Down Expand Up @@ -124,14 +133,27 @@ public function buildSchema(string $className, string $format = 'jsonapi', strin
}
// We don't use the serializer context here as JSON:API doesn't leverage serializer groups for related resources.
// That is done by query parameter. @see https://jsonapi.org/format/#fetching-includes
$schema = $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, [], $forceCollection);
$serializerContext ??= $this->getSerializerContext($operation ?? $this->findOperation($className, $type, $operation, $serializerContext, $format), $type);
$jsonApiSerializerContext = !($serializerContext[self::DISABLE_JSON_SCHEMA_SERIALIZER_GROUPS] ?? true) ? $serializerContext : [];
$schema = $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $jsonApiSerializerContext, $forceCollection);

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

if (Error::class === $className && !isset($properties['errors'])) {
$definitions[$key]['properties'] = [
'errors' => [
'type' => 'object',
'properties' => $properties,
],
];

return $schema;
}

// Prevent reapplying
if (isset($properties['id'], $properties['type']) || isset($properties['data'])) {
if (isset($properties['id'], $properties['type']) || isset($properties['data']) || isset($properties['errors'])) {
return $schema;
}

Expand Down
13 changes: 9 additions & 4 deletions src/JsonSchema/ResourceMetadataTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ private function findOutputClass(string $className, string $type, Operation $ope
return $forceSubschema ? ($inputOrOutput['class'] ?? $inputOrOutput->class ?? $operation->getClass()) : ($inputOrOutput['class'] ?? $inputOrOutput->class ?? null);
}

private function findOperation(string $className, string $type, ?Operation $operation, ?array $serializerContext): Operation
private function findOperation(string $className, string $type, ?Operation $operation, ?array $serializerContext, ?string $format = null): Operation
{
if (null === $operation) {
if (null === $this->resourceMetadataFactory) {
Expand All @@ -54,7 +54,7 @@ private function findOperation(string $className, string $type, ?Operation $oper
$operation = new HttpOperation();
}

return $this->findOperationForType($resourceMetadataCollection, $type, $operation);
return $this->findOperationForType($resourceMetadataCollection, $type, $operation, $format);
}

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

return $this->findOperationForType($resourceMetadataCollection, $type, $operation);
return $this->findOperationForType($resourceMetadataCollection, $type, $operation, $format);
}

return $operation;
}

private function findOperationForType(ResourceMetadataCollection $resourceMetadataCollection, string $type, Operation $operation): Operation
private function findOperationForType(ResourceMetadataCollection $resourceMetadataCollection, string $type, Operation $operation, ?string $format = null): Operation
{
// Find the operation and use the first one that matches criterias
foreach ($resourceMetadataCollection as $resourceMetadata) {
Expand All @@ -85,6 +85,11 @@ private function findOperationForType(ResourceMetadataCollection $resourceMetada
$operation = $op;
break 2;
}

if ($format && Schema::TYPE_OUTPUT === $type && \array_key_exists($format, $op->getOutputFormats() ?? [])) {
$operation = $op;
break 2;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ public function __construct(
*/
public function create(string $resourceClass, string $property, array $options = []): ApiProperty
{
if (!is_a($resourceClass, Model::class, true)) {
return $this->decorated?->create($resourceClass, $property, $options) ?? new ApiProperty();
}

try {
$refl = new \ReflectionClass($resourceClass);
$model = $refl->newInstanceWithoutConstructor();
Expand Down
54 changes: 27 additions & 27 deletions src/Metadata/ApiProperty.php
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ public function getProperty(): ?string
return $this->property;
}

public function withProperty(string $property): self
public function withProperty(string $property): static
{
$self = clone $this;
$self->property = $property;
Expand All @@ -244,7 +244,7 @@ public function getDescription(): ?string
return $this->description;
}

public function withDescription(string $description): self
public function withDescription(string $description): static
{
$self = clone $this;
$self->description = $description;
Expand All @@ -257,7 +257,7 @@ public function isReadable(): ?bool
return $this->readable;
}

public function withReadable(bool $readable): self
public function withReadable(bool $readable): static
{
$self = clone $this;
$self->readable = $readable;
Expand All @@ -270,7 +270,7 @@ public function isWritable(): ?bool
return $this->writable;
}

public function withWritable(bool $writable): self
public function withWritable(bool $writable): static
{
$self = clone $this;
$self->writable = $writable;
Expand All @@ -283,7 +283,7 @@ public function isReadableLink(): ?bool
return $this->readableLink;
}

public function withReadableLink(bool $readableLink): self
public function withReadableLink(bool $readableLink): static
{
$self = clone $this;
$self->readableLink = $readableLink;
Expand All @@ -296,7 +296,7 @@ public function isWritableLink(): ?bool
return $this->writableLink;
}

public function withWritableLink(bool $writableLink): self
public function withWritableLink(bool $writableLink): static
{
$self = clone $this;
$self->writableLink = $writableLink;
Expand All @@ -309,7 +309,7 @@ public function isRequired(): ?bool
return $this->required;
}

public function withRequired(bool $required): self
public function withRequired(bool $required): static
{
$self = clone $this;
$self->required = $required;
Expand All @@ -322,7 +322,7 @@ public function isIdentifier(): ?bool
return $this->identifier;
}

public function withIdentifier(bool $identifier): self
public function withIdentifier(bool $identifier): static
{
$self = clone $this;
$self->identifier = $identifier;
Expand All @@ -335,7 +335,7 @@ public function getDefault()
return $this->default;
}

public function withDefault($default): self
public function withDefault($default): static
{
$self = clone $this;
$self->default = $default;
Expand All @@ -348,7 +348,7 @@ public function getExample(): mixed
return $this->example;
}

public function withExample(mixed $example): self
public function withExample(mixed $example): static
{
$self = clone $this;
$self->example = $example;
Expand All @@ -361,7 +361,7 @@ public function getDeprecationReason(): ?string
return $this->deprecationReason;
}

public function withDeprecationReason($deprecationReason): self
public function withDeprecationReason($deprecationReason): static
{
$self = clone $this;
$self->deprecationReason = $deprecationReason;
Expand All @@ -374,7 +374,7 @@ public function isFetchable(): ?bool
return $this->fetchable;
}

public function withFetchable($fetchable): self
public function withFetchable($fetchable): static
{
$self = clone $this;
$self->fetchable = $fetchable;
Expand All @@ -387,7 +387,7 @@ public function getFetchEager(): ?bool
return $this->fetchEager;
}

public function withFetchEager($fetchEager): self
public function withFetchEager($fetchEager): static
{
$self = clone $this;
$self->fetchEager = $fetchEager;
Expand All @@ -400,7 +400,7 @@ public function getJsonldContext(): ?array
return $this->jsonldContext;
}

public function withJsonldContext($jsonldContext): self
public function withJsonldContext($jsonldContext): static
{
$self = clone $this;
$self->jsonldContext = $jsonldContext;
Expand All @@ -413,7 +413,7 @@ public function getOpenapiContext(): ?array
return $this->openapiContext;
}

public function withOpenapiContext($openapiContext): self
public function withOpenapiContext($openapiContext): static
{
$self = clone $this;
$self->openapiContext = $openapiContext;
Expand All @@ -426,7 +426,7 @@ public function getJsonSchemaContext(): ?array
return $this->jsonSchemaContext;
}

public function withJsonSchemaContext($jsonSchemaContext): self
public function withJsonSchemaContext($jsonSchemaContext): static
{
$self = clone $this;
$self->jsonSchemaContext = $jsonSchemaContext;
Expand All @@ -439,7 +439,7 @@ public function getPush(): ?bool
return $this->push;
}

public function withPush($push): self
public function withPush($push): static
{
$self = clone $this;
$self->push = $push;
Expand All @@ -452,7 +452,7 @@ public function getSecurity(): ?string
return $this->security instanceof \Stringable ? (string) $this->security : $this->security;
}

public function withSecurity($security): self
public function withSecurity($security): static
{
$self = clone $this;
$self->security = $security;
Expand All @@ -465,7 +465,7 @@ public function getSecurityPostDenormalize(): ?string
return $this->securityPostDenormalize instanceof \Stringable ? (string) $this->securityPostDenormalize : $this->securityPostDenormalize;
}

public function withSecurityPostDenormalize($securityPostDenormalize): self
public function withSecurityPostDenormalize($securityPostDenormalize): static
{
$self = clone $this;
$self->securityPostDenormalize = $securityPostDenormalize;
Expand All @@ -481,7 +481,7 @@ public function getTypes(): ?array
/**
* @param string[]|string $types
*/
public function withTypes(array|string $types = []): self
public function withTypes(array|string $types = []): static
{
$self = clone $this;
$self->types = (array) $types;
Expand All @@ -500,7 +500,7 @@ public function getBuiltinTypes(): ?array
/**
* @param Type[] $builtinTypes
*/
public function withBuiltinTypes(array $builtinTypes = []): self
public function withBuiltinTypes(array $builtinTypes = []): static
{
$self = clone $this;
$self->builtinTypes = $builtinTypes;
Expand All @@ -513,15 +513,15 @@ public function getSchema(): ?array
return $this->schema;
}

public function withSchema(array $schema = []): self
public function withSchema(array $schema = []): static
{
$self = clone $this;
$self->schema = $schema;

return $self;
}

public function withInitializable(bool $initializable): self
public function withInitializable(?bool $initializable): static
{
$self = clone $this;
$self->initializable = $initializable;
Expand All @@ -539,7 +539,7 @@ public function getExtraProperties(): ?array
return $this->extraProperties;
}

public function withExtraProperties(array $extraProperties = []): self
public function withExtraProperties(array $extraProperties = []): static
{
$self = clone $this;
$self->extraProperties = $extraProperties;
Expand All @@ -560,7 +560,7 @@ public function getIris()
*
* @param string|string[] $iris
*/
public function withIris(string|array $iris): self
public function withIris(string|array $iris): static
{
$metadata = clone $this;
$metadata->iris = (array) $iris;
Expand All @@ -576,7 +576,7 @@ public function getGenId()
return $this->genId;
}

public function withGenId(bool $genId): self
public function withGenId(bool $genId): static
{
$metadata = clone $this;
$metadata->genId = $genId;
Expand All @@ -594,7 +594,7 @@ public function getUriTemplate(): ?string
return $this->uriTemplate;
}

public function withUriTemplate(?string $uriTemplate): self
public function withUriTemplate(?string $uriTemplate): static
{
$metadata = clone $this;
$metadata->uriTemplate = $uriTemplate;
Expand Down
Loading
Loading