Skip to content
Draft
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
1 change: 1 addition & 0 deletions features/hal/table_inheritance.feature
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ Feature: Table inheritance
"href": "/dummy_table_inheritances/2"
}
},
"swaggerThanParent": true,
"id": 2,
"name": "Foobarbaz inheritance"
}
Expand Down
3 changes: 3 additions & 0 deletions features/main/table_inheritance.feature
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,9 @@ Feature: Table inheritance
"type": "string",
"pattern": "^single item$"
},
"bar": {
"type": ["string", "null"]
},
"fooz": {
"type": "string",
"pattern": "fooz"
Expand Down
140 changes: 120 additions & 20 deletions src/JsonSchema/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use ApiPlatform\Metadata\ResourceClassResolverInterface;
use ApiPlatform\Metadata\Util\TypeHelper;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\Serializer\Attribute\DiscriminatorMap;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\TypeInfo\Type\BuiltinType;
Expand Down Expand Up @@ -160,6 +161,8 @@ public function buildSchema(string $className, string $format = 'json', string $
}
}

$this->buildDiscriminatorSchema($schema, $definitions, $definitionName, $definition, $inputOrOutputClass, $format, $type, $version, $options, $serializerContext, $isJsonMergePatch);

return $schema;
}

Expand All @@ -169,16 +172,7 @@ public function buildSchema(string $className, string $format = 'json', string $
private function buildLegacyPropertySchema(Schema $schema, string $definitionName, string $normalizedPropertyName, ApiProperty $propertyMetadata, array $serializerContext, string $format, string $parentType): void
{
$version = $schema->getVersion();
if (Schema::VERSION_SWAGGER === $version || Schema::VERSION_OPENAPI === $version) {
$additionalPropertySchema = $propertyMetadata->getOpenapiContext();
} else {
$additionalPropertySchema = $propertyMetadata->getJsonSchemaContext();
}

$propertySchema = array_merge(
$propertyMetadata->getSchema() ?? [],
$additionalPropertySchema ?? []
);
$propertySchema = $this->getBasePropertySchema($propertyMetadata, $version);

// @see https://github.com/api-platform/core/issues/6299
if (Schema::UNKNOWN_TYPE === ($propertySchema['type'] ?? null) && isset($propertySchema['$ref'])) {
Expand Down Expand Up @@ -300,16 +294,7 @@ private function buildLegacyPropertySchema(Schema $schema, string $definitionNam
private function buildPropertySchema(Schema $schema, string $definitionName, string $normalizedPropertyName, ApiProperty $propertyMetadata, array $serializerContext, string $format, string $parentType): void
{
$version = $schema->getVersion();
if (Schema::VERSION_SWAGGER === $version || Schema::VERSION_OPENAPI === $version) {
$additionalPropertySchema = $propertyMetadata->getOpenapiContext();
} else {
$additionalPropertySchema = $propertyMetadata->getJsonSchemaContext();
}

$propertySchema = array_merge(
$propertyMetadata->getSchema() ?? [],
$additionalPropertySchema ?? []
);
$propertySchema = $this->getBasePropertySchema($propertyMetadata, $version);

$extraProperties = $propertyMetadata->getExtraProperties();
// see AttributePropertyMetadataFactory
Expand Down Expand Up @@ -566,4 +551,119 @@ private function getSchemaValue(array $schema, string $key): array|string|null

return $schema[$key] ?? $schema['allOf'][0][$key] ?? $schema['anyOf'][0][$key] ?? $schema['oneOf'][0][$key] ?? null;
}

private function getBasePropertySchema(ApiProperty $propertyMetadata, string $version): array
{
if (Schema::VERSION_SWAGGER === $version || Schema::VERSION_OPENAPI === $version) {
$additionalPropertySchema = $propertyMetadata->getOpenapiContext();
} else {
$additionalPropertySchema = $propertyMetadata->getJsonSchemaContext();
}

return array_merge(
$propertyMetadata->getSchema() ?? [],
$additionalPropertySchema ?? []
);
}

/**
* @return array<string, mixed>|null
*/
private function buildSubclassPropertySchema(Schema $schema, ApiProperty $propertyMetadata): ?array
{
$propertySchema = $this->getBasePropertySchema($propertyMetadata, $schema->getVersion());

return $propertySchema ?: null;
}

/**
* Builds polymorphic schema (oneOf + discriminator) when the class has a Symfony DiscriminatorMap attribute.
*/
private function buildDiscriminatorSchema(Schema $schema, \ArrayObject $definitions, string $definitionName, \ArrayObject $definition, string $inputOrOutputClass, string $format, string $type, string $version, array $options, array $serializerContext, bool $isJsonMergePatch): void
{
$reflectionClass = new \ReflectionClass($inputOrOutputClass);

$discriminatorMapAttributes = $reflectionClass->getAttributes(DiscriminatorMap::class);
if (!$discriminatorMapAttributes) {
return;
}

$discriminatorMap = $discriminatorMapAttributes[0]->newInstance();
$typeProperty = $discriminatorMap->typeProperty;
$mapping = $discriminatorMap->mapping;

if (!$mapping) {
return;
}

$uriPrefix = $this->getSchemaUriPrefix($version);
$oneOf = [];
$discriminatorMapping = [];

$parentPropertyNames = [];
foreach ($this->propertyNameCollectionFactory->create($inputOrOutputClass, $options) as $propertyName) {
$parentPropertyNames[$propertyName] = true;
}

foreach ($mapping as $typeValue => $subClassName) {
$subDefinitionName = $this->definitionNameFactory->create($subClassName, $format, $subClassName, null, $serializerContext + ['schema_type' => $type]);

if (isset($definitions[$subDefinitionName])) {
$oneOf[] = ['$ref' => $uriPrefix.$subDefinitionName];
$discriminatorMapping[$typeValue] = $uriPrefix.$subDefinitionName;
continue;
}

/** @var \ArrayObject<string, array<string, mixed>> $subclassProperties */
$subclassProperties = new \ArrayObject();

foreach ($this->propertyNameCollectionFactory->create($subClassName, $options) as $propertyName) {
if (isset($parentPropertyNames[$propertyName])) {
continue;
}

$propertyMetadata = $this->propertyMetadataFactory->create($subClassName, $propertyName, $options);

if (false === $propertyMetadata->isReadable() && false === $propertyMetadata->isWritable()) {
continue;
}

$normalizedPropertyName = $this->nameConverter ? $this->nameConverter->normalize($propertyName, $subClassName, $format, $serializerContext) : $propertyName;
if ($propertyMetadata->isRequired() && !$isJsonMergePatch) {
$definition['required'][] = $normalizedPropertyName;
}

if ($propertySchema = $this->buildSubclassPropertySchema($schema, $propertyMetadata)) {
$subclassProperties[$normalizedPropertyName] = $propertySchema;
}
}

$allOf = [
['$ref' => $uriPrefix.$definitionName],
];

if (\count($subclassProperties) > 0) {
$extra = ['type' => 'object', 'properties' => $subclassProperties->getArrayCopy()];
if (isset($definition['required']) && \count($definition['required']) > 0) {
$extra['required'] = $definition['required'];
}
$allOf[] = $extra;
}

$subDefinition = new \ArrayObject(['allOf' => new \ArrayObject($allOf)]);
$definitions[$subDefinitionName] = $subDefinition;

$oneOf[] = ['$ref' => $uriPrefix.$subDefinitionName];
$discriminatorMapping[$typeValue] = $uriPrefix.$subDefinitionName;
}

if (\count($oneOf) > 0) {
$definition['oneOf'] = $oneOf;
$normalizedTypeProperty = $this->nameConverter ? $this->nameConverter->normalize($typeProperty, $inputOrOutputClass, $format, $serializerContext) : $typeProperty;
$definition['discriminator'] = [
'propertyName' => $normalizedTypeProperty,
'mapping' => $discriminatorMapping,
];
}
}
}
8 changes: 4 additions & 4 deletions src/Serializer/AbstractItemNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -472,17 +472,17 @@ protected function extractAttributes(object $object, ?string $format = null, arr
*/
protected function getAllowedAttributes(string|object $classOrObject, array $context, bool $attributesAsString = false): array|bool
{
if (!$this->resourceClassResolver->isResourceClass($context['resource_class'])) {
$contextResourceClass = $context['resource_class'];
if (!$this->resourceClassResolver->isResourceClass($contextResourceClass)) {
return parent::getAllowedAttributes($classOrObject, $context, $attributesAsString);
}

$resourceClass = $this->resourceClassResolver->getResourceClass(null, $context['resource_class']); // fix for abstract classes and interfaces
$options = $this->getFactoryOptions($context);
$propertyNames = $this->propertyNameCollectionFactory->create($resourceClass, $options);
$propertyNames = $this->propertyNameCollectionFactory->create($contextResourceClass, $options);

$allowedAttributes = [];
foreach ($propertyNames as $propertyName) {
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName, $options);
$propertyMetadata = $this->propertyMetadataFactory->create($contextResourceClass, $propertyName, $options);

if (
$this->isAllowedAttribute($classOrObject, $propertyName, null, $context)
Expand Down
Loading
Loading