Skip to content

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

@DamianEdwards

Description

@DamianEdwards

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'.

Associated WorkItem - 368772

Metadata

Metadata

Labels

📌 seQUESTeredIdentifies that an issue has been imported into Quest.in-prThis issue will be closed (fixed) by an active pull request.

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions