Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JsonSerializer.SerializeAsync to PipeWriter or Stream throws when using source generated JsonSerializerContext and JsonSourceGenerationMode.Serialization #44413

Open
DamianEdwards opened this issue Jan 15, 2025 · 5 comments
Assignees
Labels
⌚ Not Triaged Not triaged

Comments

@DamianEdwards
Copy link
Member

When using the JSON source generator with [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Seralization)], the JsonSerializer.SerializeAsync|Serialize methods that accept Stream and PipeWriter throw InvalidOperationException. This is not a regression from .NET 8.0 (when using a Stream).

var pipe = new Pipe();
await JsonSerializer.SerializeAsync(pipe.Writer, new JsonMessage { message = "Hello, World!" }, SerializationJsonContext.Default.JsonMessage);

[JsonSourceGenerationOptions(JsonSerializerDefaults.Web, GenerationMode = JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(JsonMessage))]
partial class SerializationJsonContext : JsonSerializerContext
{

}

struct JsonMessage
{
    public required string message { get; set; }
}

Stack trace:

Unhandled exception. System.InvalidOperationException: TypeInfoResolver 'SerializationJsonContext' did not provide property metadata for type 'JsonMessage'.
   at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_NoMetadataForTypeProperties(IJsonTypeInfoResolver resolver, Type type)
   at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializerOptions options, WriteStack& state)
   at System.Text.Json.Serialization.Converters.JsonMetadataServicesConverter`1.OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializerOptions options, WriteStack& state)
   at System.Text.Json.Serialization.JsonConverter`1.TryWrite(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
   at System.Text.Json.Serialization.JsonConverter`1.WriteCore(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.SerializeAsync(PipeWriter pipeWriter, T rootValue, Int32 flushThreshold, CancellationToken cancellationToken, Object rootValueBoxed)
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.SerializeAsync(PipeWriter pipeWriter, T rootValue, Int32 flushThreshold, CancellationToken cancellationToken, Object rootValueBoxed)
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.SerializeAsync(PipeWriter pipeWriter, T rootValue, Int32 flushThreshold, CancellationToken cancellationToken, Object rootValueBoxed)
   at Program.<Main>$(String[] args) in D:\src\local\JsonSerializeToPipe\JsonSerializeToPipe\Program.cs:line 20
   at Program.<Main>(String[] args)

Full repro app that multi-targets net8.0 and net9.0 and shows serialization succeeding when using other overloads of Serialize or using a generated JsonSerializationContext with default generation mode:

using System.Text.Json.Serialization;
using System.Text.Json;
#if NET9_0_OR_GREATER
using System.IO.Pipelines;
#endif

var message = new JsonMessage { message = "Hello, World!" };

Console.WriteLine($"Serialize to string with {nameof(DefaultJsonContext)}: {JsonSerializer.Serialize(message, DefaultJsonContext.Default.JsonMessage)}");
Console.WriteLine($"Serialize to string with {nameof(SerializationJsonContext)}: {JsonSerializer.Serialize(message, SerializationJsonContext.Default.JsonMessage)}");

#if NET9_0_OR_GREATER
var pipe1 = new Pipe();
Console.Write($"Serialize to pipe with {nameof(DefaultJsonContext)}...");
await JsonSerializer.SerializeAsync(pipe1.Writer, new JsonMessage { message = "Hello, World!" }, DefaultJsonContext.Default.JsonMessage);
Console.WriteLine($"done!");

try
{
    Console.Write($"Serialize to pipe with {nameof(SerializationJsonContext)}...");
    var pipe2 = new Pipe();
    await JsonSerializer.SerializeAsync(pipe2.Writer, new JsonMessage { message = "Hello, World!" }, SerializationJsonContext.Default.JsonMessage);
    Console.WriteLine($"done!");
}
catch (Exception ex)
{
    Console.WriteLine($"error!: {ex.GetType().Name} -> {ex.Message}");
}
#endif

using var ms1 = new MemoryStream();
Console.Write($"Serialize to stream (async) with {nameof(DefaultJsonContext)}...");
await JsonSerializer.SerializeAsync(ms1, new JsonMessage { message = "Hello, World!" }, DefaultJsonContext.Default.JsonMessage);
Console.WriteLine($"done!");

try
{
    using var ms2 = new MemoryStream();
    Console.Write($"Serialize to stream (async) with {nameof(SerializationJsonContext)}...");
    await JsonSerializer.SerializeAsync(ms2, new JsonMessage { message = "Hello, World!" }, SerializationJsonContext.Default.JsonMessage);
    Console.Write($"done!");
}
catch (Exception ex)
{
    Console.WriteLine($"error!: {ex.GetType().Name} -> {ex.Message}");
}

using var ms3 = new MemoryStream();
Console.Write($"Serialize to stream (sync) with {nameof(DefaultJsonContext)}...");
JsonSerializer.Serialize(ms3, new JsonMessage { message = "Hello, World!" }, DefaultJsonContext.Default.JsonMessage);
Console.WriteLine($"done!");

try
{
    using var ms4 = new MemoryStream();
    Console.Write($"Serialize to stream (sync) with {nameof(SerializationJsonContext)}...");
    JsonSerializer.Serialize(ms4, new JsonMessage { message = "Hello, World!" }, SerializationJsonContext.Default.JsonMessage);
    Console.Write($"done!");
}
catch (Exception ex)
{
    Console.WriteLine($"error!: {ex.GetType().Name} -> {ex.Message}");
}

[JsonSourceGenerationOptions(JsonSerializerDefaults.Web, GenerationMode = JsonSourceGenerationMode.Default)]
[JsonSerializable(typeof(JsonMessage))]
partial class DefaultJsonContext : JsonSerializerContext
{

}

[JsonSourceGenerationOptions(JsonSerializerDefaults.Web, GenerationMode = JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(JsonMessage))]
partial class SerializationJsonContext : JsonSerializerContext
{

}

struct JsonMessage
{
    public required string message { get; set; }
}

Output:

Serialize to string with DefaultJsonContext: {"message":"Hello, World!"}
Serialize to string with SerializationJsonContext: {"message":"Hello, World!"}
Serialize to pipe with DefaultJsonContext...done!
Serialize to pipe with SerializationJsonContext...error!: InvalidOperationException -> TypeInfoResolver 'SerializationJsonContext' did not provide property metadata for type 'JsonMessage'.
Serialize to stream (async) with DefaultJsonContext...done!
Serialize to stream (async) with SerializationJsonContext...error!: InvalidOperationException -> TypeInfoResolver 'SerializationJsonContext' did not provide property metadata for type 'JsonMessage'.
Serialize to stream (sync) with DefaultJsonContext...done!
Serialize to stream (sync) with SerializationJsonContext...error!: InvalidOperationException -> TypeInfoResolver 'SerializationJsonContext' did not provide property metadata for type 'JsonMessage'.
Copy link
Contributor

Tagging subscribers to this area: @dotnet/area-system-text-json, @gregsdennis
See info in area-owners.md if you want to be subscribed.

@jeffhandley
Copy link
Member

Assigned to @eiriktsarpalis for triage.

@eiriktsarpalis
Copy link
Member

JsonSourceGenerationMode.Seralization is a mode that only emits fast-path serialization methods but not serialization metadata. Fast-path serialization is very restricted in what it can do, namely it only supports synchronous serialization but not async serialization or any mode of deserialization.

As such, I would put this under by design behavior.

@BrennanConroy
Copy link
Member

BrennanConroy commented Jan 16, 2025

Probably needs more documentation to that effect?

Edit: below comment was wrong, it's using default generation mode

Also, the fact that async serialization via:

using var ms1 = new MemoryStream();
Console.Write($"Serialize to stream (async) with {nameof(DefaultJsonContext)}...");
await JsonSerializer.SerializeAsync(ms1, new JsonMessage { message = "Hello, World!" }, DefaultJsonContext.Default.JsonMessage);
Console.WriteLine($"done!");

works, makes it feel very weird.

@eiriktsarpalis
Copy link
Member

Possibly. There's no mention of async in the relevant article: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/source-generation-modes

@eiriktsarpalis eiriktsarpalis transferred this issue from dotnet/runtime Jan 16, 2025
@dotnet-policy-service dotnet-policy-service bot added the ⌚ Not Triaged Not triaged label Jan 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
⌚ Not Triaged Not triaged
Projects
None yet
Development

No branches or pull requests

5 participants