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

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());
    }

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 2
  • Comments: 23 (14 by maintainers)

Commits related to this issue

Most upvoted comments

@tacman We regularly merge the 5.4 up into all other maintained branches. So if the linked PR is merged into the 5.4 branch, the patch will eventually land in all maintained Symfony versions.

I guess not many people use XML deserialization. It continues to be an issue.