StringEpsilon / PhpSerializerNET

A .NET library for working with the PHP serialization format

Repository from Github https://github.comStringEpsilon/PhpSerializerNETRepository from Github https://github.comStringEpsilon/PhpSerializerNET

Deserialization .UseLists = Listoption.Never breaks parsing

sommmen opened this issue · comments

Hiya,

I'm trying to work with the following string:

a:1:{i:0;a:11:{s:14:"content_length";s:2:"96";s:13:"content_width";s:4:"14.5";s:14:"content_height";s:3:"1.5";s:14:"content_weight";s:5:"2.094";s:9:"belt_size";s:3:"146";s:5:"items";a:3:{i:108192;s:1:"1";i:108191;s:1:"1";i:108190;s:1:"1";}s:6:"length";s:3:"104";s:5:"width";s:2:"17";s:6:"height";s:1:"4";s:6:"weight";s:5:"2.362";s:9:"packaging";a:3:{s:2:"id";s:5:"84565";s:4:"cost";s:1:"0";s:6:"weight";s:5:"0.268";}}}

Which should parse fine:

Array
(
    [0] => Array
        (
            [content_length] => 96
            [content_width] => 14.5
            [content_height] => 1.5
            [content_weight] => 2.094
            [belt_size] => 146
            [items] => Array
                (
                    [108192] => 1
                    [108191] => 1
                    [108190] => 1
                )

            [length] => 104
            [width] => 17
            [height] => 4
            [weight] => 2.362
            [packaging] => Array
                (
                    [id] => 84565
                    [cost] => 0
                    [weight] => 0.268
                )
        )
)

https://www.unserialize.com/s/7bab51d5-6a0c-a748-bb0f-00004aac1852

Using the library - this works when UseLists is the default value, but when i set it to Listoption.Never, because 'items' is a dictionary and not a list (this is weird but out of my control..) it simply returns null;

            var parcelAllocations = PhpSerialization.Deserialize(parcelAllocationMetaValue, new PhpDeserializationOptions
            {
                // Needed because of a b.u.g in the library TODO 27092021 typelookup issue seems to be fixed, classnames are also added so we can refactor the parsing to be somewhat nicer.
                EnableTypeLookup = false,

                // Needed because items is dictionary of integers and will be parsed as a list instead of a dict.
                UseLists = ListOptions.Never
            }) as List<object>;

What version of the Library are you using? Unless I misunderstand the issue, I can't reproduce this.

I cobbled together this test on 0.7.0 and it seems to work just fine:

[TestMethod]
public void Bug8(){
	Dictionary<object, object> result = (Dictionary<object, object>)PhpSerialization.Deserialize(
		"a:1:{i:0;a:11:{s:14:\"content_length\";s:2:\"96\";s:13:\"content_width\";s:4:\"14.5\";s:14:\"content_height\";s:3:\"1.5\";s:14:\"content_weight\";s:5:\"2.094\";s:9:\"belt_size\";s:3:\"146\";s:5:\"items\";a:3:{i:108192;s:1:\"1\";i:108191;s:1:\"1\";i:108190;s:1:\"1\";}s:6:\"length\";s:3:\"104\";s:5:\"width\";s:2:\"17\";s:6:\"height\";s:1:\"4\";s:6:\"weight\";s:5:\"2.362\";s:9:\"packaging\";a:3:{s:2:\"id\";s:5:\"84565\";s:4:\"cost\";s:1:\"0\";s:6:\"weight\";s:5:\"0.268\";}}}",
		new PhpDeserializationOptions(){UseLists = ListOptions.Never}
	);
	Dictionary<object, object> firstEntry = (Dictionary<object, object>)result[(long)0];
	Assert.IsNotNull(firstEntry);
	Assert.IsNotNull(firstEntry["items"]);
	Assert.IsNotNull(firstEntry["content_width"]);
	Assert.IsNotNull(firstEntry["content_height"]);
	Assert.IsNotNull(firstEntry["content_weight"]);
	Assert.IsNotNull(firstEntry["belt_size"]);
	Assert.IsNotNull(firstEntry["items"]);
	Assert.IsInstanceOfType(firstEntry["items"], typeof(Dictionary<object, object>));
	var items = firstEntry["items"] as Dictionary<object, object>;
	Assert.AreEqual(1, items[(long)108192]);
	Assert.AreEqual(1, items[(long)108191]);
	Assert.AreEqual(1, items[(long)108190]);

	Assert.IsNotNull(firstEntry["length"]);
	Assert.IsNotNull(firstEntry["width"]);
	Assert.IsNotNull(firstEntry["height"]);
	Assert.IsNotNull(firstEntry["weight"]);
	Assert.IsNotNull(firstEntry["packaging"]);
}

Also seems to work using explicit to-type parsing. Though, what I cooked up might not work for your use case, depending on what your data looks like: https://gist.github.com/StringEpsilon/b6f627663bbe3d0affd83521dc8ab95c

Hiya - thanks for responding.

Well i'm still on 0.5.1 - sorry for not mentioning that.

I looked at the commits till v6 and only same class related fixes so i felt like that would not have anything to do with this.
I'm also seeeing a v7 package in nuget, but not on the release page:
https://github.com/StringEpsilon/PhpSerializerNET/releases

Anyhow ill update and see if i still see the issue. Will take some time because i'll have to test my other code with the new lib. It has enableTypeLookup set to false and its using dictionaries as result so i'm thinking there won't be any issues.

Your gist actually seems perfect - i feel like parsing to an object is more error prone since my incoming data strings may change, but i think with the 'dont throw exception on new property names' option i saw somewhere i'll be fine.

Working on a solution right now. Latest version seems to work as-is. still testing.

In the meantime, I'm running into an issue where the property 'packaging' may contain an array of ints OR strings.
This is ofcourse pretty weird. Any tips/suggestions on how to deal with this?

Ill post a full string if i encounter a good one. Working with a lot of data... ;)

Edit: I've also ran into a case where there is a 'supports' property which is sometimes present but not always. Is there a flag that says a property in my dto can be skipped? do you have any recommendations on how to approach this?

a:1:{i:0;a:12:{s:14:"content_length";s:5:"112.5";s:13:"content_width";s:2:"28";s:14:"content_height";s:1:"5";s:14:"content_weight";s:5:"7.222";s:9:"belt_size";s:5:"206.5";s:5:"items";a:1:{i:222897;s:1:"5";}s:6:"length";s:5:"120.5";s:5:"width";s:2:"33";s:6:"height";s:2:"10";s:6:"weight";s:5:"8.167";s:9:"packaging";a:3:{s:2:"id";s:6:"150514";s:4:"cost";s:1:"0";s:6:"weight";s:5:"0.945";}s:8:"supports";a:5:{s:8:"material";s:9:"honeycomb";s:6:"length";s:5:"112.5";s:5:"width";s:2:"28";s:6:"height";s:1:"3";s:6:"weight";s:5:"0.337";}}}
    public class ParcelAllocationPhpModel
    {
         ...
        public ParcelAllocationPackagingPhpModel packaging { get; set; }
        public ParcelAllocationSupportsPhpModel supports { get; set; } // Sometimes present, sometimes not...

Hm i feel like a 'PhpSerialization.TryDeserialize' method would be helpful in these type of situations. That way i can try different models. I will use a try-catch based extension method myself for now i think.

public static bool TryDeserializeFromList(string input, out List<ParcelAllocationPhpModel> parcelAllocationPhpModels)
        {
            try
            {
                parcelAllocationPhpModels = DeserializeFromList(input);
                return true;
            }
            catch (Exception)
            {
                parcelAllocationPhpModels = null;
                return false;
            }
        }

        public static List<ParcelAllocationPhpModel> DeserializeFromList(string input) =>
            PhpSerialization.Deserialize<List<ParcelAllocationPhpModel>>(input);

Anyhow - i'm thinking about closing this issue. I'm using the to-type parsing method now and that works fine.
In terms of debugging experience, i saw a formatexception and i found it hard to figure out which property had a wrong type, so posting what kind of property its trying to deserialize to would be helpful.

I still need a way to ignore properties if they're not present. I.e. set them to their default value.

Would you like me to create a new issue for that one or do you think its not necesary (for ex. because its not possible).

I was thinking in the line of using an [Optional] attribute or something similar.

Feel free to submit feature requests and general feedback. Closing this one for now. :)

As for

In the meantime, I'm running into an issue where the property 'packaging' may contain an array of ints OR strings.
This is ofcourse pretty weird. Any tips/suggestions on how to deal with this?

In theory, having the corresponding property be a string should work, if I recall my code right. In the deserialization step, there's some logic to make use of the IConvertible interface that both integers / longs and strings implement.