Description
Symfony version(s) affected
6.3.4
Description
When deserializing XML into an object, sometimes the serializer "guesses" a field containing only digits to be an integer.
This behaviour can be controlled via the TYPE_CAST_ATTRIBUTES
/xml_type_cast_attributes
option.
But, this only applies if the node in question has child nodes. If its just a lone XML element without child nodes, some kind of "quick path" is taken that just copies the attribute values to an arry (as strings):
// XmlEncoder.php line 159ff
foreach ($rootNode->attributes as $attrKey => $attr) {
$data['@'.$attrKey] = $attr->nodeValue;
}
This causes different encoding results for very similar XML strings (see reproducer).
For me, setting xml_type_cast_attributes
to false
works around the issue, as that produces the (for us) correct behaviour in all
cases.
How to reproduce
Installed the followin libraries via composer:
"symfony/serializer": "^6.3",
"symfony/property-info": "^6.3",
"symfony/property-access": "^6.3",
"phpdocumentor/type-resolver": "^1.7",
"phpdocumentor/reflection-docblock": "^5.3"
<?php
declare(strict_types=1);
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
use Symfony\Component\Serializer\Mapping\Loader\LoaderChain;
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
use Symfony\Component\Serializer\Normalizer\PropertyNormalizer;
use Symfony\Component\Serializer\Serializer;
require_once 'vendor/autoload.php';
class TestClass
{
public function __construct(
#[SerializedName('@aString')]
public readonly string $aString,
#[SerializedName('@anInt')]
public readonly int $anInt
) {
}
}
$metadataFactory = new ClassMetadataFactory(new LoaderChain([new AnnotationLoader(null)]));
$nameConverter = new MetadataAwareNameConverter($metadataFactory);
$propertyTypeExtractor = new PropertyInfoExtractor([], [new ReflectionExtractor()]);
$classDiscriminator = new ClassDiscriminatorFromClassMetadata($metadataFactory);
$serializer = new Serializer(
[
new PropertyNormalizer($metadataFactory, $nameConverter, $propertyTypeExtractor, $classDiscriminator)
],
[
new XmlEncoder()
]
);
// this works
var_dump($serializer->deserialize('<outer aString="12345" anInt="12345"></outer>', TestClass::class, 'xml'));
// this also works
var_dump($serializer->deserialize('<outer aString="12345" anInt="12345"> </outer>', TestClass::class, 'xml', ['xml_type_cast_attributes' => false]));
// NotNormalizableValueException: The type of the "aString" attribute for class "TestClass" must be one of "string" ("int" given).
var_dump($serializer->deserialize('<outer aString="12345" anInt="12345"> </outer>', TestClass::class, 'xml'));
Possible Solution
Handling attribute type "guessing" identical, independent from $rootNode->hasChildNodes()
should result in a more consistent result. But I may be missing part of the picture here.
Additional Context
No response