Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Deserialization of IOrderedEnumerable<T> with JSON.NET

My team and I came accross a weird behavior with JSON.NET deserialization in C#.

We have a simple ViewModel with a IOrderedEnumerable<long> :

public class TestClass
{
    public IOrderedEnumerable<long> orderedDatas { get; set; }
    public string Name { get; set; }

    public TestClass(string name)
    {
        this.Name = name;
        this.orderedDatas = new List<long>().OrderBy(p => p);
    }
}

Then, we just want to POST/PUT this viewmodel in an API controller

[HttpPost]
public IHttpActionResult Post([FromBody]TestClass test)
{
    return Ok(test);
}

Calling this API with a json that look like that :

{
    Name: "tiit",
    "orderedDatas": [
        2,
        3,
        4
    ],
}

With this call, we saw that the constructor wasn't called (which can be explained by the fact that it's not a default constructor). But the strange thing is that if we change the type of the collection to IEnumerable or IList, the constructor is properly called.

If we change the constructor of TestClass to be a default one :

public class TestClass
{
    public IOrderedEnumerable<long> orderedDatas { get; set; }
    public string Name { get; set; }

    public TestClass()
    {
        this.Name = "default";
        this.orderedDatas = new List<long>().OrderBy(i => i);
    }
}

The object retrieve by the controller will not be null. And if we change the type of the collection to IEnumerable and we keep the constructor with a parameter (public TestClass(string name)), it'll work also.

Another strange thing is that the object test in the controller is "null". Not only the orderedDatas is null, but the entire object.

If we add an attribute [JsonObject] on the class, and an [JsonIgnore] on the property orderedData, it works.

For now, we changed the object to a simple List and it's working fine, but we were wondering why the JSON deserialization act different depending on the type of the collection.

If we use directly the JsonConvert.Deserialize :

var json = "{ Name: 'tiit', 'orderedDatas': [2,3,4,332232] }";
var result = JsonConvert.DeserializeObject<TestClass>(json);

we can saw the actual exception :

Cannot create and populate list type System.Linq.IOrderedEnumerable`1[System.Int64]. Path 'orderedDatas', line 1, position 33.

Any idea / help is appreciated.

Thanks !

** Edit : thanks for your answers. There is one thing that I keep finding weird (I put it in bold), if you have any idea to explain this behavior, please tell me **

like image 792
Cyril Dupuis Avatar asked Sep 07 '16 12:09

Cyril Dupuis


2 Answers

As kisu states in this answer, Json.NET fails to deserilize your TestClass because it has no built-in logic for mapping the IOrderedEnumerable<T> interface to a concrete class as is required to deserialize it. This is not surprising, because:

  1. IOrderedEnumerable<TElement> has no publicly available property indicating how it is sorted -- ascending; descending; using some complex keySelector delegate that refers to one or more captured variables. Thus this information would be lost during serialization - and serializing a delegate such as the keySelector delegate anyway isn't implemented even if the information were public.

  2. The concrete .Net class that implements this interface, OrderedEnumerable<TElement, TKey>, is internal. Normally it is returned by Enumerable.OrderBy() or Enumerable.ThenBy() not created directly in application code. See here for a sample implementation.

A minimal change to your TestClass to make it serializable by Json.NET would be to add params long [] orderedDatas to your its constructor:

public class TestClass
{
    public IOrderedEnumerable<long> orderedDatas { get; set; }
    public string Name { get; set; }

    public TestClass(string name, params long [] orderedDatas)
    {
        this.Name = name;
        this.orderedDatas = orderedDatas.OrderBy(i => i);
    }
}

This takes advantage of the fact that, when a type has exactly one public constructor, if that constructor is parameterized, Json.NET will invoke it to construct instances of the type, matching and deserializing JSON properties to constructor arguments by name (modulo case).

That being said, I don't recommend this design. From the reference source for OrderedEnumerable<TElement, TKey>.GetEnumerator() we can see that the underlying enumerable is re-sorted each time GetEnumerator() is called. Thus your implementation could be quite inefficient. And of course the ordering logic will be lost after round-tripping. To see what I mean, consider the following:

var test = new TestClass("tiit");
int factor = 1;
test.orderedDatas = new[] { 1L, 6L }.OrderBy(i => factor * i);
Console.WriteLine(JsonConvert.SerializeObject(test, Formatting.Indented));
factor = -1;
Console.WriteLine(JsonConvert.SerializeObject(test, Formatting.Indented));

The first call to Console.WriteLine() prints

{
  "orderedDatas": [
    1,
    6
  ],
  "Name": "tiit"
}

And the second prints

{
  "orderedDatas": [
    6,
    1
  ],
  "Name": "tiit"
}

As you can see, orderedDatas is re-sorted every time it is enumerated, according to the current value of the captured variable factor. All that Json.NET could do is to snapshot the current sequence when serializing, it has no way to serialize the dynamic logic of how the sequence constantly re-sorts itself.

Sample fiddle.

Of course, when you change the property to be IList<long> then no exception is thrown and the object will be deserialized. Json.NET has built-in logic to deserialize the interfaces IList<T> and IEnumerable<T> as List<T>. It has no built-in concrete type to use for IOrderedEnumerable<T> for the reasons explained.

Update

You ask, to paraphrase, why is the parameterized constructor not called when trying and failing to deserialize a nested IOrderedEnumerable<T> property, while the parameterless constructor is called?

Json.NET uses a different order of operations when deserializing objects with and without a parameterized constructor. That difference is explained in the answer to the question Usage of non-default constructor breaks order of deserialization in Json.net. Bearing that answer in mind, when exactly does Json.NET throw the exception from trying and failing to deserialize an instance of type IOrderedEnumerable<T>?

  1. When the type has a parameterized constructor, Json.NET will chew through the properties in the JSON file and deserialize values for each one before constructing the object, then pass the appropriate values into the constructor. Thus when the exception gets thrown the object is not constructed.

  2. When the type has a parameterless constructor, Json.NET will construct an instance and begin to chew through the JSON properties to deserialize them, throwing the exception partway through. Thus when the exception gets thrown the object is constructed but not fully deserialized.

Apparently, somewhere in your framework the exception from Json.NET is getting caught and swallowed. So in case #1 you get a null object and in case #2 you get a partially deserialized object.

like image 155
dbc Avatar answered Nov 10 '22 20:11

dbc


From what I can see in constructor of JsonArrayContract, interface IOrderedEnumerable<> is not correctly handled, most likely because it is not implemented by List<> and T[] and therefore needs some more special treatment and basically can be replaced by some other interface.

Also, putting size-less interfaces inside a deserialized class does not make sense, because by definition, they cannot be infinite and can be replaced by collection that has finite, known size.

Remember that using interfaces in deserialized objects involves some "guess work" from the deserializer to create a correct instance.

If you want some immutable collection with specified order, I would advise you to go with IReadOnlyList<>.

like image 23
kiziu Avatar answered Nov 10 '22 22:11

kiziu