2

I'm trying to deserialize an XML-file to an object containing an array of other objects using Symfony Serializer, but can't get it to work with an array of objects under a specific key.

Here's a simplified version of the XML:

<?xml version="1.0"?>
<ROOT>
  <RETURN>
    <TYPE>test</TYPE>
  </RETURN>
  <COMPONENT>
    <item>
      <ID>1</ID>
    </item>
    <item>
      <ID>2</ID>
    </item>
  </COMPONENT>
</ROOT>

I have a class which I'd like to deserialize this to. It looks like this:

class MyObject
{
    private Return $return;
    /** @var array<Component> */
    private array $component = [];
    // getters and setters here
}

The class "Return" has a "type"-property, the class "Component" has an "id", both with getters and setters.

This is my (current) serializer:

$serializer    = new Serializer(
    [
        new ArrayDenormalizer(),
        new ObjectNormalizer(propertyTypeExtractor: new ReflectionExtractor()),
    ],
    [new XmlEncoder()]
);

Deserializing my XML will now produce the following result:

^ App\ValueObject\MyObject^ {
  -return: App\ValueObject\Return^ {
    -type: "test"
  }
  -component: array:1 [
    "item" => array:2 [
      0 => array:1 [
        "ID" => "1"
      ],
      1 => array:1 [
        "ID" => "2"
    (...)

while the expected output is this:

^ App\ValueObject\MyObject^ {
  -return: App\ValueObject\Return^ {
    -type: "test"
  }
  -component: array:2 [
    0 => App\ValueObject\Component^ {
      -id: "1"
    },
    1 => App\ValueObject\Component^ {
      -id: "2"
    }
    (...)

So deserializing the Return-object works as expected, mapping the XML to my array of Component-objects does not.

(Also I noticed that if my component only has one item, the output is not "-component: array:1 [ "item" => array:1 [ 0 => array:1 [ "ID" => "1" ] ]" but "-component: array:1 [ "item" => array:1 [ "ID" => "1" ] ]", so there's one level missing.)

Based on several tutorials and answers here I have tried using this as my extractors:

$phpDocExtractor = new PhpDocExtractor();
$typeExtractor   = new PropertyInfoExtractor(
    typeExtractors: [new ConstructorExtractor([$phpDocExtractor]), $phpDocExtractor, new ReflectionExtractor()]
);

which will produce this:

 -component: array:1 [
    0 => App\ValueObject\Component^ {
      -id: ""

so there's only one Component-object (instead of an array of 2), also it is empty (id is missing). The same thing happens, if I just use the PhpDocExptractor or add a GetSetMethodNormalizer with a PhpDocExtractor or ReflectionExtractor.

I also played around with the @var-comment for $component, using "array<array<Component>>", but that does not change anything.

Next stop: Based on this post I converted my $component to ArrayCollection and added addComponent and removeComponent to the Component-class, but nothing changes, the addComponent-function is only called once with an array of arrays which are not deserialized to my Component-class.

It seems that deserializing objects into an array of objects is possible somehow (as the enhanced serializer shows), but somehow I'm missing the right combination of extractors (I guess). Or is it a naming problem in the XML? Do I need to have <components> as parent and <component> as child keys instead of <component> and <item>? (Which unfortunately I could not change as the XML is coming from a remote system - but can I tell the Serializer about my structure to make it understand the keys better?)

1
  • I am having the same issue - were you ever able to resolve this? Commented Jan 30, 2024 at 12:43

1 Answer 1

3

I was battling with the same issue. The thing that was missing for me was the SerializedPath annotation/attribute for the XML deserialization. Here is a full working example similar to yours:

use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\Serializer\Attribute\SerializedPath;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;

require_once 'vendor/autoload.php';

class Job
{
    public string $title;
}

class Person
{
    public string $name;

    /** @var list<Job> */
    #[SerializedPath('[jobs][job]')]
    public array $jobs;
}

$data = <<<EOF
<person>
    <name>foo</name>
    <jobs>
        <job>
            <title>Lorem</title>
        </job>
        <job>
            <title>Ipsum</title>
        </job>
        <job>
            <title>Dolor</title>
        </job>
    </jobs>
</person>
EOF;

$serializer = new Serializer(
    normalizers: [
        new ArrayDenormalizer(),
        new ObjectNormalizer(
            classMetadataFactory: new ClassMetadataFactory(new AttributeLoader()),
            propertyTypeExtractor: new PhpDocExtractor(),
        ),
    ],
    encoders: [
        new XmlEncoder()
    ]
);

/** @var Person $person */
$person = $serializer->deserialize($data, Person::class, XmlEncoder::FORMAT);
{
    "require": {
        "symfony/serializer": "^7.0",
        "symfony/property-access": "^7.0",
        "symfony/property-info": "^7.0",
        "phpdocumentor/reflection-docblock": "^5.3"
    }
}

So if you add

classMetadataFactory: new ClassMetadataFactory(new AttributeLoader())

to your ObjectNormalizer and define

class MyObject
{
    private Return $return;
    /** @var array<Component> */
    #[SerializedPath('[COMPONENT][item]')]
    private array $component = [];
    // getters and setters here
}

in your case, I think it should work.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for sharing! I actually ended up with deserializing the arrays manually which did not resolve the actual "problem" but at least got it to work. I dug out the old code today and tried your solution but the problem remained, so I guess there's still something weird in my setup. As the remote system has changed, I don't have to deal with the problem any more but I keep "SerializedPath" in mind, which was new to me, if I ever need to process an XML like this again.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.