Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Serializer] XmlEncoder provides different types for XML attributes depending on if the XMLNode has children #51594

Closed
@themasch

Description

@themasch

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions