Skip to content

Passing a Stream Utf8JsonWriter to a JsonSerializer.Serialize method results in Pending bytes being misreported #66102

Closed
@mattchidley

Description

@mattchidley

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();
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-System.Text.Jsonbugin-prThere is an active PR which will close this issue when it is mergedtenet-performancePerformance related issuetenet-reliabilityReliability/stability related issue (stress, load problems, etc.)

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions