Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I serialize a Stack<T> to JSON using System.Text.Json without reversing the stack?

If I have a Stack<T> for some T, and round-trip it to JSON using the new System.Text.Json.JsonSerializer, the order of the items in the stack will be reversed after deserialization. How can I serialize and deserialize a stack to JSON using this serializer without this happening?

Details as follows. I have a Stack<int> and push 3 values 1, 2, 3 onto it. I then serialize it to JSON using JsonSerializer, which results in

[3,2,1]

However, when I deserialize the JSON to a new stack, the integers in the stack are reversed, and a later assert that the stacks are sequentially equals fails:

var stack = new Stack<int>(new [] { 1, 2, 3 });

var json = JsonSerializer.Serialize(stack);

var stack2 = JsonSerializer.Deserialize<Stack<int>>(json);

var json2 = JsonSerializer.Serialize(stack2);

Console.WriteLine("Serialized {0}:", stack);
Console.WriteLine(json); // Prints [3,2,1]

Console.WriteLine("Round-tripped {0}:", stack);
Console.WriteLine(json2); // Prints [1,2,3]

Assert.IsTrue(stack.SequenceEqual(stack2)); // Fails
Assert.IsTrue(json == json2);               // Also fails

How can I prevent the serializer from reversing the stack during serialization?

Demo fiddle here.

like image 316
dbc Avatar asked Jan 05 '20 22:01

dbc


People also ask

Can JSON be serialized?

Json namespace provides functionality for serializing to and deserializing from JavaScript Object Notation (JSON). Serialization is the process of converting the state of an object, that is, the values of its properties, into a form that can be stored or transmitted.

What is the difference between System text JSON and Newtonsoft JSON?

System. Text. Json focuses primarily on performance, security, and standards compliance. It has some key differences in default behavior and doesn't aim to have feature parity with Newtonsoft.

What is the difference between serialize and deserialize JSON?

JSON is a format that encodes objects in a string. Serialization means to convert an object into that string, and deserialization is its inverse operation (convert string -> object).


1 Answers

This seems to be a bug in the serializer. In .NET Core 3.1 there is some code in CreateDerivedEnumerableInstance(ref ReadStack state, JsonPropertyInfo collectionPropertyInfo, IList sourceList) to create a stack from a deserialized list:

else if (instance is Stack<TDeclaredProperty> instanceOfStack)
{
    foreach (TDeclaredProperty item in sourceList)
    {
        instanceOfStack.Push(item);
    }

    return instanceOfStack;
}

However, it pushes them on in the wrong order. Thus a custom JsonConverter<Stack<T>> will be required to correctly deserialize a Stack<T>. In addition, a JsonConverterFactory can be used to manufacture an appropriate converter for every stack type Stack<T>:

public class StackConverterFactory : JsonConverterFactory
{
    public override bool CanConvert(Type typeToConvert)
    {
        return GetStackItemType(typeToConvert) != null;
    }

    public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
    {
        var itemType = GetStackItemType(typeToConvert);
        var converterType = typeof(StackConverter<,>).MakeGenericType(typeToConvert, itemType);
        return (JsonConverter)Activator.CreateInstance(converterType);
    }

    static Type GetStackItemType(Type type)
    {
        while (type != null)
        {
            if (type.IsGenericType)
            {
                var genType = type.GetGenericTypeDefinition();
                if (genType == typeof(Stack<>))
                    return type.GetGenericArguments()[0];
            }
            type = type.BaseType;
        }
        return null;
    }
}

public class StackConverter<TItem> : StackConverter<Stack<TItem>, TItem>
{
}

public class StackConverter<TStack, TItem> : JsonConverter<TStack> where TStack : Stack<TItem>, new()
{
    public override TStack Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var list = JsonSerializer.Deserialize<List<TItem>>(ref reader, options);
        if (list == null)
            return null;
        var stack = typeToConvert == typeof(Stack<TItem>) ? (TStack)new Stack<TItem>(list.Count) : new TStack();
        for (int i = list.Count - 1; i >= 0; i--)
            stack.Push(list[i]);
        return stack;
    }

    public override void Write(Utf8JsonWriter writer, TStack value, JsonSerializerOptions options)
    {
        writer.WriteStartArray();
        foreach (var item in value)
            JsonSerializer.Serialize(writer, item, options);
        writer.WriteEndArray();
    }
}

Then use it in JsonSerializerOptions as follows:

var stack = new Stack<int>(new [] { 1, 2, 3 });

var options = new JsonSerializerOptions
{
    Converters = { new StackConverterFactory() },
};

var json = JsonSerializer.Serialize(stack, options);

var stack2 = JsonSerializer.Deserialize<Stack<int>>(json, options);

var json2 = JsonSerializer.Serialize(stack2, options);

Assert.IsTrue(stack.SequenceEqual(stack2)); // Passes
Assert.IsTrue(json == json2);  // Passes

The converter could also be applied directly to some data model using JsonConverterAttribute

public class Model
{
    [JsonConverter(typeof(StackConverter<int>))]
    public Stack<int> Stack { get; set; }
}

Demo fiddle here.

Update: Looks round-tripping of Stack<T> will not be built into JsonSerializer. See (De)serializing stacks with JsonSerializer should round-trip #41887 (Closed):

We shouldn't do this. There's no standard on which side to reverse the items (serialization or deserialization) in order to roundtrip, so it is a non-starter as a breaking change candidate. The current behavior is compatible with Newtonsoft.Json behavior.

There's a work item to provide a sample converter showing how to roundtrip in the JSON docs which I think should suffice as a resolution for this issue: dotnet/docs#16690. Here's what this converter could look like - dotnet/docs#16225 (comment).

like image 155
dbc Avatar answered Sep 28 '22 08:09

dbc