Skip to content
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

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

Closed
jlhernandez opened this issue Jun 20, 2019 · 23 comments
Labels
Bug Help wanted Issues and PRs which are looking for volunteers to complete them. Serializer Status: Needs Review

Comments

@jlhernandez
Copy link

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());
    }
@mkrauser
Copy link
Contributor

I can confirm this exact issue

@monteiro
Copy link
Contributor

I have create an issue to cover both cases without using the SerializedName annotation. The solution is just a name converter that is passed to the ObjectNormalizer. The name converter converts extra attributes (that are prefixed with @) and node value ( using #) returned from the XmlEncoder.

Can this be useful for you?

@tacman
Copy link
Contributor

tacman commented Apr 9, 2020

I think this is related, but perhaps not. I'm trying to deserialize XML into a Property entity with a name and value, e.g.

<property name="length">4110</property>

serialization.yaml is configured so that the attribute and value are generated properly.

App\Entity\Property:
  attributes:
    name:
      serialized_name: '@name'
      groups: ['xml']
    value:
      serialized_name: '#'
      groups: ['xml']

And works as expected.

        $property = (new Property())->setName('length')->setValue(4110);
        $context = [
            'xml_root_node_name' => 'property',
            'groups' => ['xml']
        ];

        $xml = $serializer->serialize($property, 'xml', $context);   
       //         $xml is now '<property name="length">4110</property>' as expected

        $xml = '<property name="length">4110</property>';
    
       // but serializing to Property doesn't work.
        $data = $serializer->deserialize($xml, Property::class, 'xml');
        // neither name or value is set.
        dd($data);

Does the serialized_name property (or @SerializedName annotation) work in the deserialization process? Or should I be calling this differently?

@nicolas-grekas
Copy link
Member

Help wanted.

@nicolas-grekas nicolas-grekas added the Help wanted Issues and PRs which are looking for volunteers to complete them. label Jan 23, 2021
@carsonbot
Copy link

Hey, thanks for your report!
There has not been a lot of activity here for a while. Is this bug still relevant? Have you managed to find a workaround?

@tacman
Copy link
Contributor

tacman commented Jul 24, 2021

My workaround was to use a regex to rewrite the attributes as node elements, then rewrite my parsing accordingly. Of course, my solution is a classic definition of "hack" -- functional, but certainly not the "right" way.

@carsonbot carsonbot removed the Stalled label Jul 24, 2021
@monteiro
Copy link
Contributor

Did something related here: #35087

@carsonbot
Copy link

Hey, thanks for your report!
There has not been a lot of activity here for a while. Is this bug still relevant? Have you managed to find a workaround?

@carsonbot
Copy link

Could I get a reply or should I close this?

@tacman
Copy link
Contributor

tacman commented Feb 9, 2022

@nicolas-grekas Do you know if any tests have been added for this?

@carsonbot carsonbot removed the Stalled label Feb 9, 2022
@nicolas-grekas
Copy link
Member

I don't remember sorry. Please have a look if you can.

@tacman
Copy link
Contributor

tacman commented Feb 10, 2022

I can confirm that I still have the issue.

I've create a repo that shows example, trivial (2 entities, serialization.yaml, and a Command).

git clone git@github.com:tacman/xml-serializer-demo.git
cd xml-serializer-demo && composer install
bin/console app:bug

The serializations works, but not the deserialization:

$product = (new Product())
    ->setDescription("Wool sweater");
$product
    ->addProperty((new Property())->setName('color')->setValue('blue'))
    ->addProperty((new Property())->setName('size')->setValue('small'))
    ;

$context = [
    'xml_root_node_name' => 'product',
    'groups' => ['xml'],
    'xml_standalone' => false,
    'xml_format_output' => true,
];
$xml = $this->serializer->serialize($product, 'xml', $context);

// works!

        /** @var Product $product2 */
        $product2 = $this->serializer->deserialize($xml, Product::class, 'xml');

        dump($product2); // nothing is set :-(

@carsonbot
Copy link

Hey, thanks for your report!
There has not been a lot of activity here for a while. Is this bug still relevant? Have you managed to find a workaround?

@carsonbot
Copy link

Could I get a reply or should I close this?

@tacman
Copy link
Contributor

tacman commented Oct 29, 2022

I'll try again.

@carsonbot carsonbot removed the Stalled label Oct 29, 2022
@tacman
Copy link
Contributor

tacman commented Oct 29, 2022

I bumped the example repo to Symfony 6.1 and it still fails.

git clone git@github.com:tacman/xml-serializer-demo.git
cd xml-serializer-demo && composer install
bin/console app:bug

@carsonbot
Copy link

Hey, thanks for your report!
There has not been a lot of activity here for a while. Is this bug still relevant? Have you managed to find a workaround?

@carsonbot
Copy link

Could I get a reply or should I close this?

@tacman
Copy link
Contributor

tacman commented Jul 4, 2023

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

@carsonbot
Copy link

Hey, thanks for your report!
There has not been a lot of activity here for a while. Is this bug still relevant? Have you managed to find a workaround?

@carsonbot
Copy link

Could I get a reply or should I close this?

@tacman
Copy link
Contributor

tacman commented Jan 19, 2024

I think this PR will fix it: b70032c

but shouldn't it be on all Symfony releases, not just 5.4?

@carsonbot carsonbot removed the Stalled label Jan 19, 2024
@xabbuh
Copy link
Member

xabbuh commented Jan 19, 2024

@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.

@fabpot fabpot closed this as completed Apr 5, 2024
fabpot added a commit that referenced this issue Apr 5, 2024
…rld)

This PR was merged into the 5.4 branch.

Discussion
----------

[Serializer] Fix XML scalar to object denormalization

| Q             | A
| ------------- | ---
| Branch?       | 5.4
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Issues        | Fix #32114 #34579
| License       | MIT

Another approach to #34825

Commits
-------

6fe892f [Serializer] Fix XML scalar to object denormalization
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug Help wanted Issues and PRs which are looking for volunteers to complete them. Serializer Status: Needs Review
Projects
None yet
Development

No branches or pull requests

8 participants