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.
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.
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.
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).
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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With