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

Skip to content

[Serializer] XML deserialization with MetadataAwareNameConverter and optional xml attributes fails if attributes are not present #32114

Closed
@jlhernandez

Description

@jlhernandez

Symfony version(s) affected: ^4.3

Description
I am using Symfony Serializer to deserialize complex XML documents into custom objects. Some XML nodes have optional attributes that may be present or not.

For example, the "price" node can have a "currency" attribute or not:
<price currency="EUR">66</price>
or
<price>55</price>
are both valid.

I am using MetadataAwareNameConverter to map our class attributes to xml attributes.
In the first case, the Price object is correctly created and both attributes are filled in, but in the second case the Price object is created by both attributes are left NULL.

How to reproduce
Our Price class is this one:

use Symfony\Component\Serializer\Annotation\SerializedName;

class Price {

    /**
     * @SerializedName("@currency")
     * @var string
     */
    private $currency;

    /**
     * @SerializedName("#")
     * @var string
     */
    private $amount;


    public function getCurrency() {
        return $this->currency;
    }
    public function setCurrency(string $currency) {
        $this->currency = $currency;
    }

    public function getAmount() {
        return $this->amount;
    }
    public function setAmount(string $amount) {
        $this->amount = $amount;
    }
}

This test case shows the correct behaviour when "currency" attribute is present:

    function testWorks() {

        $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
        $metadataAwareNameConverter = new MetadataAwareNameConverter($classMetadataFactory);

        $encoders = [new XmlEncoder()];
        $normalizers = [new ObjectNormalizer($classMetadataFactory, $metadataAwareNameConverter)];  
        $serializer = new Serializer($normalizers, $encoders);
    
        $xml = '<price currency="EUR">55</price>';

        $object = $serializer->deserialize($xml, Price::class, 'xml');

        $this->assertEquals('EUR', $object->getCurrency());
        $this->assertEquals(55, $object->getAmount());
    }

but this test case fails (Amount is also NULL!):

    function testFails() {

        $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
        $metadataAwareNameConverter = new MetadataAwareNameConverter($classMetadataFactory);

        $encoders = [new XmlEncoder()];
        $normalizers = [new ObjectNormalizer($classMetadataFactory, $metadataAwareNameConverter)];  
        $serializer = new Serializer($normalizers, $encoders);
    
        $xml = '<price>55</price>';

        $object = $serializer->deserialize($xml, Price::class, 'xml');

        $this->assertNull($object->getCurrency());
        $this->assertEquals(55, $object->getAmount());
    }

Possible Solution

I have found that when denormalizing the attributes, the name converter receives an array with key '0' as property name. I have implemented a CustomMetadataAwareNameConverter that replaces the property name '0' by '#', and it works now..

This is the CustomMetadataAwareNameConverter class:

class CustomMetadataAwareNameConverter implements AdvancedNameConverterInterface {

    /**
     * The real MetadataAwareNameConverter
     * 
     * @var MetadataAwareNameConverter
     */
    private $converter;

    public function __construct(ClassMetadataFactoryInterface $metadataFactory, NameConverterInterface $fallbackNameConverter = null) {
        $this->converter = new MetadataAwareNameConverter($metadataFactory, $fallbackNameConverter);
    }

    public function normalize($propertyName, string $class = null, string $format = null, array $context = []) {
        return $this->converter->normalize($propertyName, $class, $format, $context);
    }

    public function denormalize($propertyName, string $class = null, string $format = null, array $context = []) {
        return $this->converter->denormalize($propertyName == '0' ? '#' : $propertyName, $class, $format, $context);
    }
}

and the test that previously failed works now:

    function testFixed() {

        $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
        $metadataAwareNameConverter = new CustomMetadataAwareNameConverter($classMetadataFactory);

        $encoders = [new XmlEncoder()];
        $normalizers = [new ObjectNormalizer($classMetadataFactory, $metadataAwareNameConverter)];  
        $serializer = new Serializer($normalizers, $encoders);
    
        $xml = '<price>55</price>';

        $object = $serializer->deserialize($xml, Price::class, 'xml');

        $this->assertNull($object->getCurrency());
        $this->assertEquals(55, $object->getAmount());
    }

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