Skip to content

feat(serializer): type info #7104

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

Merged
merged 3 commits into from
Apr 24, 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
67 changes: 0 additions & 67 deletions features/main/validation.feature
Original file line number Diff line number Diff line change
Expand Up @@ -87,74 +87,7 @@ Feature: Using validations groups
And the JSON node "detail" should be equal to "test: This value should not be null."
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"

@!mongodb
@createSchema
Scenario: Create a resource with collectDenormalizationErrors
When I add "Content-type" header equal to "application/ld+json"
And I send a "POST" request to "/dummy_collect_denormalization" with body:
"""
{
"foo": 3,
"bar": "baz",
"qux": true,
"uuid": "y",
"relatedDummy": 8,
"relatedDummies": 76
}
"""
Then the response status code should be 422
And the response should be in JSON
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
And the JSON should be a superset of:
"""
{
"@context": "/contexts/ConstraintViolation",
"@type": "ConstraintViolation",
"hydra:title": "An error occurred",
"hydra:description": "baz: This value should be of type string.\nqux: This value should be of type string.\nfoo: This value should be of type bool.\nbar: This value should be of type int.\nuuid: This value should be of type uuid.\nrelatedDummy: This value should be of type array|string.\nrelatedDummies: This value should be of type array.",
"violations": [
{
"propertyPath": "baz",
"message": "This value should be of type string.",
"code": "ba785a8c-82cb-4283-967c-3cf342181b40",
"hint": "Failed to create object because the class misses the \"baz\" property."
},
{
"propertyPath": "qux",
"message": "This value should be of type string.",
"code": "ba785a8c-82cb-4283-967c-3cf342181b40"
},
{
"propertyPath": "foo",
"message": "This value should be of type bool.",
"code": "ba785a8c-82cb-4283-967c-3cf342181b40"
},
{
"propertyPath": "bar",
"message": "This value should be of type int.",
"code": "ba785a8c-82cb-4283-967c-3cf342181b40"
},
{
"propertyPath": "uuid",
"message": "This value should be of type uuid.",
"code": "ba785a8c-82cb-4283-967c-3cf342181b40",
"hint": "Invalid UUID string: y"
},
{
"propertyPath": "relatedDummy",
"message": "This value should be of type array|string.",
"code": "ba785a8c-82cb-4283-967c-3cf342181b40",
"hint": "The type of the \"relatedDummy\" attribute must be \"array\" (nested document) or \"string\" (IRI), \"integer\" given."
},
{
"propertyPath": "relatedDummies",
"message": "This value should be of type array.",
"code": "ba785a8c-82cb-4283-967c-3cf342181b40"
}
]
}
"""

@!mongodb
Scenario: Get violations constraints
When I add "Accept" header equal to "application/json"
Expand Down
24 changes: 4 additions & 20 deletions src/Elasticsearch/Util/FieldDatatypeTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,11 @@
use ApiPlatform\Metadata\Exception\PropertyNotFoundException;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Metadata\ResourceClassResolverInterface;
use ApiPlatform\Metadata\Util\TypeHelper;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\PropertyInfo\Type as LegacyType;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\CollectionType;
use Symfony\Component\TypeInfo\Type\CompositeTypeInterface;
use Symfony\Component\TypeInfo\Type\ObjectType;
use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;

/**
* Field datatypes helpers.
Expand Down Expand Up @@ -108,13 +106,8 @@
/** @var class-string|null $className */
$className = null;

$typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass, &$className): bool {
return match (true) {
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass),
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass),
$type instanceof ObjectType => $this->resourceClassResolver->isResourceClass($className = $type->getClassName()),
default => false,
};
$typeIsResourceClass = function (Type $type) use (&$className): bool {
return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName());

Check warning on line 110 in src/Elasticsearch/Util/FieldDatatypeTrait.php

View check run for this annotation

Codecov / codecov/patch

src/Elasticsearch/Util/FieldDatatypeTrait.php#L109-L110

Added lines #L109 - L110 were not covered by tests
};

if ($type->isSatisfiedBy($typeIsResourceClass)) {
Expand All @@ -123,16 +116,7 @@
return null === $nestedPath ? $nestedPath : "$currentProperty.$nestedPath";
}

$collectionValueTypeIsResourceClass = function (Type $type) use (&$collectionValueTypeIsResourceClass, &$className): bool {
return match (true) {
$type instanceof CollectionType => $type->getCollectionValueType() instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getCollectionValueType()->getClassName()),
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($collectionValueTypeIsResourceClass),
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($collectionValueTypeIsResourceClass),
default => false,
};
};

if ($type->isSatisfiedBy($collectionValueTypeIsResourceClass)) {
if (TypeHelper::getCollectionValueType($type)?->isSatisfiedBy($typeIsResourceClass)) {

Check warning on line 119 in src/Elasticsearch/Util/FieldDatatypeTrait.php

View check run for this annotation

Codecov / codecov/patch

src/Elasticsearch/Util/FieldDatatypeTrait.php#L119

Added line #L119 was not covered by tests
$nestedPath = $this->getNestedFieldPath($className, implode('.', $properties));

return null === $nestedPath ? $currentProperty : "$currentProperty.$nestedPath";
Expand Down
8 changes: 2 additions & 6 deletions src/Hal/Serializer/ItemNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -193,12 +193,8 @@ private function getComponents(object $object, ?string $format, array $context):

// prevent declaring $attribute as attribute if it's already declared as relationship
$isRelationship = false;
$typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass, &$className): bool {
return match (true) {
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass),
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass),
default => $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName()),
};
$typeIsResourceClass = function (Type $type) use (&$className): bool {
return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName());
};

foreach ($types as $type) {
Expand Down
17 changes: 4 additions & 13 deletions src/Hydra/Serializer/DocumentationNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -414,12 +414,8 @@ private function getRange(ApiProperty $propertyMetadata): array|string|null
/** @var class-string|null $className */
$className = null;

$typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass, &$className): bool {
return match (true) {
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass),
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass),
default => $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName()),
};
$typeIsResourceClass = function (Type $type) use (&$className): bool {
return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName());
};

if ($nativeType->isSatisfiedBy($typeIsResourceClass) && $className) {
Expand Down Expand Up @@ -515,13 +511,8 @@ private function isSingleRelation(ApiProperty $propertyMetadata): bool
return false;
}

$typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass): bool {
return match (true) {
$type instanceof CollectionType => false,
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass),
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass),
default => $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($type->getClassName()),
};
$typeIsResourceClass = function (Type $type) use (&$className): bool {
return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName());
};

return $nativeType->isSatisfiedBy($typeIsResourceClass);
Expand Down
22 changes: 4 additions & 18 deletions src/JsonApi/JsonSchema/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,12 @@
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\ResourceClassResolverInterface;
use ApiPlatform\Metadata\Util\TypeHelper;
use ApiPlatform\State\ApiResource\Error;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\CollectionType;
use Symfony\Component\TypeInfo\Type\CompositeTypeInterface;
use Symfony\Component\TypeInfo\Type\ObjectType;
use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;

/**
* Decorator factory which adds JSON:API properties to the JSON Schema document.
Expand Down Expand Up @@ -331,25 +330,12 @@ private function getRelationship(string $resourceClass, string $property, ?array
/** @var class-string|null $className */
$className = null;

$typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass, &$className): bool {
return match (true) {
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass),
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass),
default => $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName()),
};
};

$collectionValueIsResourceClass = function (Type $type) use (&$typeIsResourceClass): bool {
return match (true) {
$type instanceof CollectionType => $type->getCollectionValueType()->isSatisfiedBy($typeIsResourceClass),
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass),
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass),
default => false,
};
$typeIsResourceClass = function (Type $type) use (&$className): bool {
return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName());
};

foreach ($type instanceof CompositeTypeInterface ? $type->getTypes() : [$type] as $t) {
if ($t->isSatisfiedBy($collectionValueIsResourceClass)) {
if (TypeHelper::getCollectionValueType($t)?->isSatisfiedBy($typeIsResourceClass)) {
$isMany = true;
} elseif ($t->isSatisfiedBy($typeIsResourceClass)) {
$isOne = true;
Expand Down
23 changes: 4 additions & 19 deletions src/JsonApi/Serializer/ItemNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use ApiPlatform\Metadata\ResourceClassResolverInterface;
use ApiPlatform\Metadata\UrlGeneratorInterface;
use ApiPlatform\Metadata\Util\ClassInfoTrait;
use ApiPlatform\Metadata\Util\TypeHelper;
use ApiPlatform\Serializer\AbstractItemNormalizer;
use ApiPlatform\Serializer\CacheKeyTrait;
use ApiPlatform\Serializer\ContextTrait;
Expand All @@ -38,10 +39,8 @@
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\CollectionType;
use Symfony\Component\TypeInfo\Type\CompositeTypeInterface;
use Symfony\Component\TypeInfo\Type\ObjectType;
use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;

/**
* Converts between objects and array.
Expand Down Expand Up @@ -376,28 +375,14 @@ private function getComponents(object $object, ?string $format, array $context):
/** @var class-string|null $className */
$className = null;

$typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass, &$className): bool {
return match (true) {
$type instanceof ObjectType => $this->resourceClassResolver->isResourceClass($className = $type->getClassName()),
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass),
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass),
default => false,
};
};

$collectionValueIsResourceClass = function (Type $type) use ($typeIsResourceClass, &$collectionValueIsResourceClass): bool {
return match (true) {
$type instanceof CollectionType => $type->getCollectionValueType()->isSatisfiedBy($typeIsResourceClass),
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($collectionValueIsResourceClass),
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($collectionValueIsResourceClass),
default => false,
};
$typeIsResourceClass = function (Type $type) use (&$className): bool {
return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName());
};

foreach ($type instanceof CompositeTypeInterface ? $type->getTypes() : [$type] as $t) {
$isOne = $isMany = false;

if ($t->isSatisfiedBy($collectionValueIsResourceClass)) {
if (TypeHelper::getCollectionValueType($t)?->isSatisfiedBy($typeIsResourceClass)) {
$isMany = true;
} elseif ($t->isSatisfiedBy($typeIsResourceClass)) {
$isOne = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\BuiltinType;
use Symfony\Component\TypeInfo\Type\CollectionType;
use Symfony\Component\TypeInfo\Type\CompositeTypeInterface;
use Symfony\Component\TypeInfo\Type\IntersectionType;
use Symfony\Component\TypeInfo\Type\ObjectType;
use Symfony\Component\TypeInfo\Type\UnionType;
Expand Down Expand Up @@ -107,13 +106,8 @@ private function getTypeSchema(ApiProperty $propertyMetadata, array $propertySch
{
$type = $propertyMetadata->getNativeType();

$typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass): bool {
return match (true) {
$type instanceof CollectionType => $type->getCollectionValueType()->isSatisfiedBy($typeIsResourceClass),
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass),
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass),
default => $type instanceof ObjectType && $this->isResourceClass($type->getClassName()),
};
$typeIsResourceClass = function (Type $type) use (&$className): bool {
return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName());
};

if (!\array_key_exists('default', $propertySchema) && !empty($default = $propertyMetadata->getDefault()) && !$type?->isSatisfiedBy($typeIsResourceClass)) {
Expand Down
Loading
Loading