-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Description
Background & Motivation
The current design of the JsonSerializer class exposes all serialization functionality as static methods accepting configuration parametrically (and optionally). In hindsight, this design has contributed to a few ergonomic problems when using this API:
-
Forgetting to pass
JsonSerializerOptionsto the serialization methods:var options = new JsonSerializerOptions { Converters = { new JsonStringEnumConverter() }}; JsonSerializer.Serialize(MyEnum.Value);
This is probably the most common issue -- I've personally fallen for this too many times.
-
Creating a new
JsonSerializerOptionsinstance on each serialization:foreach (MyPoco value in values) { var options = new JsonSerializerOptions { Converters = { new JsonStringEnumConverter() }}; JsonSerializer.Serialize(value, options); // recalculates JSON contracts from scratch on each iteration }
Even though this anti-pattern has been explicitly documented as a potential performance bug, users keep falling for it. We've made attempts to mitigate the problem by implementing shared metadata caches but ultimately the underlying issue still affects users. See also Add an analyzer that warns on single-use JsonSerializerOptions instances #65396 for plans an adding an analyzer in this space.
-
Pervasive
RequiresUnreferenceCode/RequiresDynamicCodeannotations: all serialization methods acceptingJsonSerializerOptionshave been annotated as linker/AOT-unsafe even though legitimate scenaria exist where this is not the case:var options = new JsonSerializerOptions { TypeInfoResolver = MySourceGeneratedContext.Default }; JsonSerializer.Serialize(value, options); // warning: JSON serialization and deserialization might require types that cannot be statically analyzed.
-
Bifurcation of API surface between reflection and source generation: users need to call into distinct methods depending on whether they use sourcegen or reflection. The distinction between
JsonSerializerOptionsandJsonSerializerContexthas always been tenuous and has been rendered obsolete with the infrastructural changes introduced by Developers can customize the JSON serialization contracts of their types #63686. Arguably, the source of JSON contracts is a configuration detail that should be dealt with at the composition root and not concern any serialization methods.
API Proposal
This issue proposes we expose instance methods in JsonSerializer (or some different class):
namespace System.Text.Json;
public partial class JsonSerializerInstance // TODO come up with a better name
{
public JsonSerializerOptions Options { get; }
public JsonSerializer(JsonSerializerOptions options); // linker-safe constructor, throws if options.TypeInfoResolver == null;
// we might consider adding a linker-unsafe factory that populates TypeInfoResolver with the reflection resolver, like the existing serialization APIs do.
[RequiresUnreferencedCode]
public static JsonSerializer Default { get; } // serializer wrapping JsonSerializerOptions.Default
/* Serialization APIs */
public string Serialize<TValue>(TValue value);
public byte[] SerializeToUtf8Bytes<TValue>(TValue value);
public void Serialize<TValue>(Utf8JsonWriter utf8Json, TValue value);
public void Serialize<TValue>(Stream utf8Json, TValue value);
public void SerializeAsync<TValue>(Stream utf8Json, TValue value);
public JsonDocument SerializeToDocument<TValue>(TValue value);
public JsonElement SerializeToElement<TValue>(TValue value);
public JsonNode SerializeToNode<TValue>(TValue value);
public string Serialize(object? value, Type inputType);
public byte[] SerializeToUtf8Bytes(object? value, Type inputType);
public void Serialize(Utf8JsonWriter writer, object? value, Type inputType);
public void Serialize(Stream utf8Json, object? value, Type inputType);
public void SerializeAsync(Stream utf8Json, object? value, Type inputType);
public JsonDocument SerializeToDocument(object? value, Type inputType);
public JsonElement SerializeToElement(object? value, Type inputType);
public JsonNode SerializeToNode(object? value, Type inputType);
/* Deserialization APIs */
public TValue? Deserialize<TValue>(string json);
public TValue? Deserialize<TValue>(ReadOnlySpan<char> json);
public TValue? Deserialize<TValue>(ReadOnlySpan<byte> utf8Json);
public TValue? Deserialize<TValue>(ref Utf8JsonReader reader);
public TValue? Deserialize<TValue>(Stream utf8Json);
public ValueTask<TValue?> DeserializeAsync<TValue>(Stream utf8Json);
public IAsyncEnumerable<TValue?> DeserializeAsyncEnumerable<TValue>(Stream utf8Json);
public TValue? Deserialize<TValue>(JsonDocument document);
public TValue? Deserialize<TValue>(JsonElement element);
public TValue? Deserialize<TValue>(JsonNode node);
public object? Deserialize(ReadOnlySpan<byte> utf8Json, Type returnType);
public object? Deserialize(ReadOnlySpan<char> json, Type returnType);
public object? Deserialize(string json, Type returnType);
public object? Deserialize(JsonDocument document, Type returnType);
public object? Deserialize(JsonElement element, Type returnType);
public object? Deserialize(JsonNode node, Type returnType);
public object? Deserialize(ref Utf8JsonReader reader, Type returnType);
public object? Deserialize(Stream utf8Json, Type returnType);
public ValueTask<object?> DeserializeAsync(Stream utf8Json, Type returnType);
}
namespace System.Text.Json.Serialization;
public partial class JsonSerializerContext
{
public JsonSerializer Serializer { get; }
}Usage Examples
Using the reflection-based serializer
/* composition root */
var options = new JsonSerializerOptions
{
Converters = new JsonStringEnumConverter(),
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
};
var serializer = new JsonSerializer(options);
/* usage */
serializer.Serialize(value); Using the source generator
JsonSerializer serializer = MyContext.Default.Serializer;
serializer.Serialize(new MyPoco()); // Serializes using the source generator
[JsonSerializable(typeof(MyPoco))]
public partial class MyContext : JsonSerializerContext
{}Open Questions
- Should we reuse the existing
JsonSerializerclass or introduce a new type? - Should we have an obsoletion plan for static APIs accepting
JsonSerializerOptions?