0

I am trying to deserialize an object that contains a property of type Stack<T>:

public class Card
{
    public string Name { get; set; }
}

public class Game 
{
   public Stack<Card> Cards { get; } = new Stack<Card>();
}

var game = new Game();
game.Cards.Push(new Card() { Name = "Whatever" });
string ser = JsonConvert.SerializeObject(game);
Game unser = JsonConvert.DeserializeObject<Game>(ser);

When I try to deserialize an object previously serialized, the Cards-Property contains an empty Stack.

The missing setter is not the issue. When I change the Stack<> to List<> deserializing works:

public class Game 
{
    public List<Card> Cards { get; } = new List<Card>();
}
var game = new Game();
game.Cards.Add(new Card() { Name = "Whatever"));
string ser = JsonConvert.SerializeObject(game);
Game unser = JsonConvert.DeserializeObject<Game>(ser);

So the same works fine when using a List<>, but not a Stack<>

So my questions are:

  1. Why :)
  2. Can I force Json.NET to work with Stack<>?

As dbc already showed, this is not a duplicate. The issue is not a wrong direction of the entries, but the entries completely missing.

Ole Albers
  • 8,715
  • 10
  • 73
  • 166
  • 3
    Does this code even compile? You have two types in your property definition `string Stack Cards {get;}`. Is it a string or a Stack? – Heretic Monkey Mar 25 '18 at 16:05
  • Sorry. Typo. Corrected – Ole Albers Mar 25 '18 at 16:23
  • 1
    Related (but possibly not a perfect duplicate, need to check): [JsonConvert.Deserializer indexing issues](https://stackoverflow.com/q/39137123/3744182)... OK, I think `StackConverter` from [this answer](https://stackoverflow.com/a/39481981/3744182) should work for you since it uses the existing value if present. – dbc Mar 25 '18 at 16:28
  • Possible duplicate of [JsonConvert.Deserializer indexing issues](https://stackoverflow.com/questions/39137123/jsonconvert-deserializer-indexing-issues) – Heretic Monkey Mar 25 '18 at 16:32
  • @dbc Looks like a good answer for me. Still I am surprised, that in the linked question the issue wasn't that the deserializing completely failed, but it was returned in the wrong order. I am curious if the Json-behavior changed in the last two years. – Ole Albers Mar 25 '18 at 17:20
  • 1
    @OleAlbers - actually there's an additional problem. Will write something up soon. – dbc Mar 25 '18 at 17:20

1 Answers1

2

There is a known issue using Json.NET to deserialize Stack<T> which is explained in

The behavior reported is that Stack<T> is reversed on deserialization, and the suggested solution is to use a custom JsonConverter such as the one shown here.

Unfortunately it appears that solution does not work for a get-only stack property such as yours. Even if I specify a working converter for Stack<T> the get-only property comes back empty. See .Net fiddle #1 for a demo of the problem.

So, why does this happen? Because Stack<T> implements IEnumerable<T> but not ICollection<T>, the JsonArrayContract constructor interprets Stack<T> as a read-only collection that must be constructed via its parameterized constructor. Then, later, when attempting to deserialize your get-only, pre-allocated Stack<T> property, JsonSerializerInternalReader.CalculatePropertyDetails() decides the pre-existing value cannot be used (because it's a read-only collection) and since a newly allocated collection cannot be set back (because the property is not writable) the JSON property must be skipped. This, of course, doesn't account for the possibility that a JsonConverter if present might be able to populate the collection somehow, even though Json.NET cannot.

This feels like a bug to me; you might report an issue to Newtonsoft that a get-only Stack<T> property cannot be deserialized even with a custom JsonConverter.

As a workaround that preserves the immutability of your Stack<T> property, in addition to creating a custom JsonConverter for Stack<T>, you can create a parameterized constructor for your Game that takes a Stack<Card> cards argument, and mark it with JsonConstructor:

public class Game 
{
    public Game() { this.cards =  new Stack<Card>(); }

    [JsonConstructor]       
    Game(Stack<Card> cards)
    {
        this.cards = cards ??  new Stack<Card>();
    }

    readonly Stack<Card> cards;

    public Stack<Card> Cards { get { return cards; } }
}

Note that the constructor argument name must be the same as the JSON property name, modulo case.

(Alternatively, you could make the property be privately settable and mark it with [JsonProperty]; this destroys the guarantee of immutability however.)

Then to deserialize, do:

var settings = new JsonSerializerSettings
{
    Converters = { new StackConverter() },
};          
var unser = JsonConvert.DeserializeObject<Game>(ser, settings);     

Where StackConverter is taken verbatim from this answer.

Demo .Net fiddle #2 showing that the workaround does work.

dbc
  • 104,963
  • 20
  • 228
  • 340