Description
Description
When using JsonSerializer to serialize to a stream, if more than one custom converter is in play we are seeing that the internal buffer is not able to be written to the stream until the entirety of the object has been serialized to the buffer. This can result in significant memory usage and high allocations in ArrayPool.Rent and Resize called from PooledByteBufferWriter. It seems to me like the serialization state/context gets trapped when a second custom converter is in play both sync and async -- and we lose the ability to write to the stream and clear the buffer until the entire object has been serialized. This can be extremely problematic when serializing large objects with custom converters. If we had a 5gb object in memory, we'd effectively need a similarly large buffer just to hold the serialized representation of the object.
I am aware of this issue here -- and understand that a workaround can be to return IAsyncEnumerable in an async context... but not much info provided for sync.
My question is: in a sync context, is there a recommended approach to buffer/stream outside of what is posted on the docs? The docs recommend creating a writer for the stream and then passing the writer to the serializer -- this is also not ideal and can cause extremely high CPU if flushing after serializing using a custom converter, especially if there's a large result with many properties requiring custom conversion. I'm currently playing around with wrapping the stream with a MS's BCL BufferedStream, but would really appreciate any recommendations from your side to remedy these perf issues.
Configuration
Here is a sample where you can observe the buffer being filled and not cleared/flushed until the entire payload is serialized to the buffer:
using System.Text.Json;
using System.Text.Json.Serialization;
const string fileName = "testFile.json";
const int numTestObjects = 1000;
const int stringLength = 64;
Random random = new Random();
var fileStream = File.Create(fileName);
JsonSerializer.Serialize(fileStream, CreateManyTestObjects(), new JsonSerializerOptions { Converters = { new ParentConverter(), new ChildConverter() }});
File.Delete(fileName);
string RandomString(int length)
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
return new string(Enumerable.Repeat(chars, length)
.Select(s => s[random.Next(s.Length)]).ToArray());
}
IEnumerable<Parent> CreateManyTestObjects()
{
for (var i = 0; i < numTestObjects; i++)
{
yield return new Parent(new Child(RandomString(stringLength)));
}
}
class Parent
{
public Child Child { get; }
public Parent(Child child)
{
Child = child;
}
}
class Child
{
public string Prop { get; }
public Child(string prop)
{
Prop = prop;
}
}
class ParentConverter : JsonConverter<Parent>
{
public override Parent Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
throw new NotImplementedException();
public override void Write(Utf8JsonWriter writer, Parent value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WritePropertyName(nameof(value.Child));
// this will call the converter
JsonSerializer.Serialize(writer, value.Child, options);
writer.WriteEndObject();
}
}
class ChildConverter : JsonConverter<Child>
{
public override Child Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
throw new NotImplementedException();
public override void Write(Utf8JsonWriter writer, Child value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WritePropertyName(nameof(value.Prop));
writer.WriteStringValue(value.Prop);
writer.WriteEndObject();
}
}