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

Compile-time source generation for System.Text.Json #45448

Closed
10 of 23 tasks
layomia opened this issue Dec 2, 2020 · 19 comments
Closed
10 of 23 tasks

Compile-time source generation for System.Text.Json #45448

layomia opened this issue Dec 2, 2020 · 19 comments
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-System.Text.Json blocking Marks issues that we want to fast track in order to unblock other important work source-generator Indicates an issue with a source generator feature Team:Libraries tenet-performance Performance related issue
Milestone

Comments

@layomia
Copy link
Contributor

layomia commented Dec 2, 2020

TODO (.NET 6):

Backlog (.NET 7/future) (click to view)
---

The “JSON Serializer recommendations for 5.0” document goes over benefits of generating additional source specific to serializable .NET types at compile time. These include faster startup performance, reduced memory usage, improved runtime throughput, and smaller size-on-disk of applications that use System.Text.Json. This document will discuss design considerations and technical details for a C# source generator that helps provide these benefits.

Scenarios

Goals

  • Reduce start-up time for apps that use System.Text.Json
  • Reduce private bytes usage for apps that use System.Text.Json
  • Improve serialization throughput in apps that use System.Text.Json
  • Reduce size of applications that use System.Text.Json (post ILLinker trimming)
  • Eradicate ILLinker warnings caused by apps using System.Text.Json

API Usage

Developers using System.Text.Json directly

New pattern (includes size benefits where we trim out unused converters/reflection code)

"Code was generated for all serializable types and I know what type I'm processing"
[assembly: JsonSerializable(typeof(MyType)]

byte[] json = JsonSerializer.SerializeToUtf8Bytes(myInstance, JsonContext.Default.MyType);
myInstance = JsonSerialize.Deserialize(json, JsonContext.Default.MyType);
"Code was generated for all serializable types but I don't know what type I'm processing"
[assembly: JsonSerializable(typeof(MyType)]

byte[] json = JsonSerializer.SerializeToUtf8Bytes(someObj, JsonContext.Default);
someObj = JsonSerialize.Deserialize(json, type, JsonContext.Default);

Existing pattern (does not provide size benefits)

"Code was generated for all serializable types and I know what type I'm processing"
[assembly: JsonSerializable(typeof(MyType)]

JsonSerializerOptions existingOptions = new(JsonSerializerDefaults.Web);
existingOptions.SetContext<JsonContext>(); // Both context and options must be unbound at this point.

byte[] json = JsonSerializer.SerializeToUtf8Bytes(myInstance, existingOptions);
myInstance = JsonSerialize.Deserialize<MyType>(json, existingOptions);
"Code was generated for all serializable types but I don't know what type I'm processing"
[assembly: JsonSerializable(typeof(MyType)]

JsonSerializerOptions existingOptions = new(JsonSerializerDefaults.Web);
existingOptions.SetContext<JsonContext>();

byte[] json = JsonSerializer.SerializeToUtf8Bytes(someObj, existingOptions);
someObj = JsonSerialize.Deserialize(json, type, existingOptions);

Developers using System.Text.Json indirectly

ASP.NET Core

Blazor

The pre-generated metadata can be forwarded to the serializer directly via a new overload on the System.Http.Net.Json.HttpClient.GetFromJsonAsync method.

[assembly: JsonSerializable(typeof(WeatherForecast[]))]

@code {
    private WeatherForecast[] forecasts;

    private static JsonSerializerOptions Options = new(JsonSerializerDefaults.Web);
    private static JsonContext Context = new JsonContext(Options);

    protected override async Task OnInitializedAsync()
    {
        forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json", Context);
    }
}
MVC, WebAPIs, SignalR, Houdini

These services can retrieve user-generated context with already-existing APIs to configure serialization options. These do not come with the size benefits.

[assembly: JsonSerializable(IEnumerable<WeatherForecast>)]

services.AddControllers().AddJsonOptions(options => options.SetContext<JsonContext>());

In the future, these services can expose APIs to directly take JsonSerializerContext instances. It is difficult today since the underlying calls to the different serializer overloads are based on runtime checks. The code is not separated from the root configuration of options, so the ILLinker cannot understand that it can trim out existing linker-unfriendly overloads of the serializer. A couple strategies to evntually achieve linker friendliness are:

  • Refactor the code that provides serialization support with STJ such that unused overloads can be trimmed out by ILLinker.
  • Expose a new linker feature switch at the ASP.NET Core level that users can opt into to trim unused call-sites to linker-unfriendly calls to the serializer.

New APIs can then be added to accept context instances.

Other libraries performing serialization on behalf of others

Similar to the ASP.NET Core scenarios above, developers can provide new APIs that accept options or context instances to forward to the serializer on behalf of the user. If neither of this is viable, then the library author has a couple of options

  • Use existing overloads of the serializer as they do to day. This incurs the cost of warm-up reflection and linker-unfriendliness.
  • Hand-write or generate code to perform serialization for user types with the low level Utf8JsonReader and Utf8JsonWriter.
What about a global static to cache unbound JsonSerializerContexts?

A global mechansim to cache JsonSerializeContexts may help with the viral nature of this design by allowing developers doing serialization on behalf of others to retreive the metadata from a static rather than needing new APIs to accept it. This mechanism is likely to be unreliable because it is difficult to establish a precedent for retrieving metadata in the event of collisions. For example, if mutliple contexts contain metadata for the same type, whose should win? First-one-wins? Last one? This question does matter because metadata for the same type may differ across assemblies (e.g. non-public members, specifically private). These are the same issues that make it difficult to expose the default options and make them mutable.

Nonetheless, the proposed design is forward compatible with solving for this scenario if it is deemed important in the future.

API Proposal

New Metadata APIs for source generation

namespace System.Text.Json
{
    // Provides options to be used with JsonSerializer (class exists already).
    public sealed partial class JsonSerializerOptions
    {
        // Binds the options instance to a JsonSerializerOptions instance.
        // Throws InvalidOperationException if the options or context are already bound.
        // Allows existing usage of JsonSerializerOptions to benefit from improved start-up time.
        public void SetContext<TContext>() where TContext : JsonSerializerContext, new() { }
    }
}

namespace System.Text.Json.Serialization
{
    // Provides an abstraction for JsonSerializer to receive pre-generated metadata about a type.
    public abstract partial class JsonSerializerContext
    {
        // The options instance associated with the context.
        public JsonSerializerOptions? Options { get { throw null; } }
        // Used to create a context instance with provided options, or default options if none are specified.
        protected JsonSerializerContext(JsonSerializerOptions? options) { }
        // Provides pre-generated metadata about a type.
        public virtual JsonTypeInfo? GetTypeInfo(Type type) { throw null; }
        // ASP.NET asked for this
        public virtual JsonTypeInfo<T>? GetTypeInfo<T>() { throw null; }
    }
}

namespace System.Text.Json.Serialization.Metadata
{
    // Provides metadata about a type.
    public partial class JsonTypeInfo
    {
        internal JsonTypeInfo() { }
        // The converter for the type. Referenced by generated code.
        public JsonConverter Converter { get { throw null; } }
    }
    
    // Provides metadata about a type.
    public abstract partial class JsonTypeInfo<T> : JsonTypeInfo
    {
        internal JsonTypeInfo() { }
    }
    
    // Provides metadata about a property or field.
    public abstract class JsonPropertyInfo
    {
        internal JsonPropertyInfo() { }
    }

    // Instructs the System.Text.Json source generator to generate serialization metadata for a specified type at compile time.
    [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
    public sealed class JsonSerializableAttribute : Attribute
    {
        // The name of the property for the generated type info for the type. Useful to resolve a name collision with another type in the closure.
        public string TypeInfoPropertyName { get; set; }

        // Initializes a new instance of JsonSerializableAttribute with the specified type.
        public JsonSerializableAttribute(Type type) { }
    }
}

namespace System.Text.Json.Serialization.Metadata.Internal
{
    [EditorBrowsable(EditorBrowsableState.Never)]
    // Provides utility methods to create metadata instances. Only expected to be called from generated code.
    public static class MetadataServices
    {
        // Creates a JsonTypeInfo<T> instance for a type that has a simple JsonConverter<T> implementation, e.g `int` and `string`.
        public static JsonTypeInfo<T> CreateValueInfo<T>(
            JsonSerializerOptions options,
            JsonConverter converter) { throw null; }

        // Creates a JsonTypeInfo<T> instance for an object type that should be serialized as a JSON object, i.e. it has serializable properties and fields.            
        public static JsonTypeInfo<T> CreateObjectInfo<T>() { throw null; }
        
        // Initializes a JsonTypeInfo instance for an object type. Objects can reference to themselves, so this two-step pattern is used to avoid a stack-overflow.
        public static void InitializeObject<T>(
            JsonTypeInfo<T> info,
            JsonSerializerOptions options,
            Func<T> createObjectFunc, // Assumes parameterless ctor; there's upcoming support for parameterized ctors.
            Func<JsonSerializerContext, JsonPropertyInfo[]> propInitFunc,
            JsonNumberHandling numberHandling) { }
            
        // Creates a JsonPropertyInfo instance for a property.
        public static JsonPropertyInfo CreatePropertyInfo<TProperty>(
            JsonSerializerOptions options,
            bool isProperty,
            Type declaringType,
            JsonTypeInfo<TProperty> propertyTypeInfo,
            JsonConverter converter,
            System.Func<object, TProperty> getter,
            System.Action<object, TProperty> setter,
            JsonIgnoreCondition ignoreCondition,
            JsonNumberHandling numberHandling,
            string clrPropertyName,
            JsonEncodedText jsonPropertyName) { throw null; }

        // Creates a JsonTypeInfo<T> instance for an array type.
        public static JsonTypeInfo<TElement[]> CreateArrayInfo<TElement>(
            JsonSerializerOptions options,
            JsonTypeInfo<TElement> elementInfo,
            JsonNumberHandling numberHandling) { throw null; }
            
        // Creates a JsonTypeInfo<T> instance for a type that derives from List<TElement>.
        public static JsonTypeInfo<TCollection> CreateListInfo<TCollection, TElement>(
            JsonSerializerOptions options,
            Func<TCollection> createObjectFunc,
            JsonTypeInfo<TElement> elementInfo,
            JsonNumberHandling numberHandling)
            where TCollection : List<TElement> { throw null; }
            
        // Creates a JsonTypeInfo<T> instance for a type that derives from Dictionary<TKey, TValue>
        public static JsonTypeInfo<TCollection> CreateDictionaryInfo<TCollection, TKey, TValue>(
            JsonSerializerOptions options,
            Func<TCollection> createObjectFunc,
            JsonTypeInfo<TKey> keyInfo,
            JsonTypeInfo<TValue> valueInfo,
            JsonNumberHandling numberHandling)
            where TCollection : Dictionary<TKey, TValue> where TKey : notnull { throw null; }
    }

    [EditorBrowsable(EditorBrowsableState.Never)]
    // Provides access to the built-in converters needed by the generated code.
    public static class ConverterProvider
    {
        public static JsonConverter<bool> Boolean { get; }
        public static JsonConverter<byte[]> ByteArray { get; }
        public static JsonConverter<byte> Byte { get; }
        public static JsonConverter<char> Char { get; }
        public static JsonConverter<DateTime> DateTime { get; }
        public static JsonConverter<DateTimeOffset> DateTimeOffset { get; }
        public static JsonConverter<Decimal> Decimal { get; }
        public static JsonConverter<double> Double { get; }
        public static JsonConverter<Guid> Guid { get; }
        public static JsonConverter<short> Int16 { get; }
        public static JsonConverter<int> Int32 { get; }
        public static JsonConverter<long> Int64 { get; }
        public static JsonConverter<object> Object { get; }
        public static JsonConverter<float> Single { get; }
        public static JsonConverter<sbyte> SByte { get; }
        public static JsonConverter<string> String { get; }
        public static JsonConverter<ushort> UInt16 { get; }
        public static JsonConverter<uint> UInt32 { get; }
        public static JsonConverter<ulong> UInt64 { get; }
        public static JsonConverter<Uri> Uri { get; }
        public static JsonConverter<Version> Version { get; }

        public static JsonConverter<T> GetEnumConverter<T>(JsonSerializerOptions options) where T : struct, Enum { throw null; }

        public static JsonConverter<T?> GetNullableConverter<T>(JsonConverter<T> underlyingTypeconverter) where T : struct { throw null; }
    }
}

New JsonSerializer method overloads

namespace System.Text.Json
{
    public static class JsonSerializer
    {
        public static object? Deserialize(ReadOnlySpan<byte> utf8Json, Type returnType, JsonSerializerContext context) { throw null; }
        public static object? Deserialize(ReadOnlySpan<char> json, Type returnType, JsonSerializerContext context) { throw null; }
        public static object? Deserialize(string json, Type returnType, JsonSerializerContext context) { throw null; }
        public static object? Deserialize(ref Utf8JsonReader reader, Type returnType, JsonSerializerContext context) { throw null; }
        public static ValueTask<object?> DeserializeAsync(Stream utf8Json, Type returnType, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
        public static ValueTask<TValue?> DeserializeAsync<TValue>(Stream utf8Json, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
        public static TValue? Deserialize<TValue>(ReadOnlySpan<byte> utf8Json, JsonTypeInfo<TValue> jsonTypeInfo) { throw null; }
        public static TValue? Deserialize<TValue>(string json, JsonTypeInfo<TValue> jsonTypeInfo) { throw null; }
        public static TValue? Deserialize<TValue>(ReadOnlySpan<char> json, JsonTypeInfo<TValue> jsonTypeInfo) { throw null; }
        public static TValue? Deserialize<TValue>(ref Utf8JsonReader reader, JsonTypeInfo<TValue> jsonTypeInfo) { throw null; }
        public static string Serialize(object? value, Type inputType, JsonSerializerContext context) { throw null; }
        public static void Serialize(Utf8JsonWriter writer, object? value, Type inputType, JsonSerializerContext context) { }
        public static Task SerializeAsync(Stream utf8Json, object? value, Type inputType, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
        public static Task SerializeAsync<TValue>(Stream utf8Json, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
        public static byte[] SerializeToUtf8Bytes(object? value, Type inputType, JsonSerializerContext context) { throw null; }
        public static byte[] SerializeToUtf8Bytes<TValue>(TValue value, JsonTypeInfo<TValue> jsonTypeInfo) { throw null; }
        public static void Serialize<TValue>(Utf8JsonWriter writer, TValue value, JsonTypeInfo<TValue> jsonTypeInfo) { }
        public static string Serialize<TValue>(TValue value, JsonTypeInfo<TValue> jsonTypeInfo) { throw null; }
    }
}

New System.Net.Http.Json method overloads

namespace System.Net.Http.Json
{
    public static partial class HttpClientJsonExtensions
    {
        public static Task<object?> GetFromJsonAsync(this HttpClient client, string? requestUri, Type type, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
        public static Task<object?> GetFromJsonAsync(this HttpClient client, System.Uri? requestUri, Type type, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
        public static Task<TValue?> GetFromJsonAsync<TValue>(this HttpClient client, string? requestUri, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
        public static Task<TValue?> GetFromJsonAsync<TValue>(this HttpClient client, System.Uri? requestUri, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
        public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(this HttpClient client, string? requestUri, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
        public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(this HttpClient client, System.Uri? requestUri, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
        public static Task<HttpResponseMessage> PutAsJsonAsync<TValue>(this HttpClient client, string? requestUri, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
        public static Task<HttpResponseMessage> PutAsJsonAsync<TValue>(this HttpClient client, System.Uri? requestUri, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
    }
    public static partial class HttpContentJsonExtensions
    {
        public static Task<object?> ReadFromJsonAsync(this HttpContent content, Type type, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
        public static Task<T?> ReadFromJsonAsync<T>(this HttpContent content, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
    }
    public sealed partial class JsonContent : HttpContent
    {
        public static JsonContent Create(object? inputValue, Type inputType, JsonSerializerContext context, MediaTypeHeaderValue mediaType = null) { throw null; }
        public static JsonContent Create<T>(T inputValue, JsonTypeInfo<T> jsonValueInfo, MediaTypeHeaderValue mediaType = null) { throw null; }
    }
}

What's next?

  • Fast path for simple POCOs and JsonSerializerOptions usages
  • Support for more collections
  • Support for parameterized ctors
  • Extended polymorphic support
Design doc (Click to view)

Overview

A source generator is a .NET Standard 2.0 assembly that is loaded by the compiler. Source generators allow developers to generate C# source files that can be added to an assembly during the course of compilation. The System.Text.Json source generator (System.Text.Json.SourceGeneration.dll) generates serialization metadata for JSON-serializable types in a project. The metadata generated for a type contains structured information in a format that can be optimally utilized by the serializer to serialize and deserialize instances of that type to and from JSON representations. Serializable types are indicated to the source generator via a new [JsonSerializable] attribute. The generator then generates metadata for each type in the object graphs of each indicated type.

In previous versions of System.Text.Json, serialization metadata was computed at runtime, during the first serialization or deserialization routine of every type in any object graph passed to the serializer. At a high level, this metadata includes delegates to constructors, properter setters and getters, along with user options indicated at both runtime and design (e.g. whether to ignore a property value when serializing and it is null). After this metadata is generated, the serializer performs the actual serialization and deserialization. The generation phase is based on reflection, and is computationally expensive both in terms of throughput and allocations. We can refer to this phase as the serializer's "warm-up" phase. With this design, we are concerned not only with the cost of the intial work done within the serializer, but with the cost of all the work when an application is first started because it uses System.Text.Json. We can refer to this work collectively as the start time of the application.

The fundamental approach of the design this document goes over is to shift this runtime metadata generation phase to compile-time, substantially reducing the cost of the first serialization or deserialization procedures. This metadata is generated to the compiling assembly, where it can be initialized and passed directly to JsonSerializer so that it doesn't have to generate it at runtime. This helps reduce the costs of the first serialization or deserialization of each type.

The serializer supports a wide range of scenarios and has multiple layers of abstraction and indirection to navigate through when serializing and deserializing. In order to improve throughput for a specific scenario, one may consider generating specific and optimized serialization and deserialization logic. For now, there is no change to the actual serialization and deserialization logic for each type. The existing code-paths of the serializer are still used. This is to support complicated serializer usages like complex object graphs, and also complex serialization options that can be indicated at runtime (using JsonSerializerOptions) as well as design-time (using serialization attributes like [JsonIgnore]). Attempting the generate code that adapts to each scenario can lead to large, complex, and unreliable code. A lightweight mode to generate low-level serialization logic for simple scenarios like POCOs and TechEmpower benchmarks is not yet implemented in this design, but is in scope for .NET 6.0. In the future, we can add optional knobs to improve throughput for other predetermined scenarios.

This project introduces new patterns for using JsonSerializer, with the aim of improving performance in consuming applications. Let us see an example of what compile-time generated metadata looks like and how to interact with it in a project. Given an object:

public class MyClass
{
    public int MyInt { get; set; }
    public string[] MyStrings { get; set; }
}

With the existing JsonSerializer functionality, this POCO may be serialized and deserialized as follows:

MyClass obj = new()
{
    MyInt = 1,
    MyStrings = new string[] { "Hello" },
};

byte[] serialized = JsonSerializer.SerializeToUtf8Bytes(obj);
obj = JsonSerializer.Deserialize<MyClass>(serialized);

Console.WriteLine(obj.MyInt); // 1
Console.WriteLine(obj.MyString); // “Hello”

With source generation, the serializable type may be indicated to the generator via JsonSerializableAttribute:

[assembly: JsonSerializable(typeof(MyClass))]
The generator will then generate structured type metadata to the compilation assembly (click to view).

JsonSerializableAttribute.g.cs

using System;

namespace System.Text.Json.SourceGeneration
{
    /// <summary>
    /// Instructs the System.Text.Json source generator to generate serialization metadata for a specified type at compile time.
    /// </summary>
    [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
    public sealed class JsonSerializableAttribute : Attribute
    {
        /// <summary>
        /// Indicates whether the specified type might be the runtime type of an object instance which was declared as
        /// a different type (polymorphic serialization).
        /// </summary>
        public bool CanBeDynamic { get; set; }

        /// <summary>
        /// Initializes a new instance of <see cref="JsonSerializableAttribute"/> with the specified type.
        /// </summary>
        /// <param name="type">The Type of the property.</param>
        public JsonSerializableAttribute(Type type) { }
    }
}

JsonContext.g.cs

using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;

namespace Startup.JsonSourceGeneration
{
    internal partial class JsonContext : JsonSerializerContext
    {
        private static JsonContext s_default;
        public static JsonContext Default => s_default ??= new JsonContext();

        private JsonContext()
        {
        }

        public JsonContext(JsonSerializerOptions options) : base(options)
        {
        }
        
        private static JsonConverter GetRuntimeProvidedCustomConverter(System.Type type, JsonSerializerOptions options)
        {
            System.Collections.Generic.IList<JsonConverter> converters = options.Converters;

            for (int i = 0; i < converters.Count; i++)
            {
                JsonConverter converter = converters[i];

                if (converter.CanConvert(type))
                {
                    if (converter is JsonConverterFactory factory)
                    {
                        converter = factory.CreateConverter(type, options);
                        if (converter == null || converter is JsonConverterFactory)
                        {
                            throw new System.InvalidOperationException($"The converter '{factory.GetType()}' cannot return null or a JsonConverterFactory instance.");
                        }
                    }

                    return converter;
                }
            }

            return null;
        }

        public JsonPropertyInfo<TProperty> CreateProperty<TProperty>(
                string clrPropertyName,
                System.Reflection.MemberTypes memberType,
                System.Type declaringType,
                JsonTypeInfo<TProperty> classInfo,
                JsonConverter converter,
                System.Func<object, TProperty> getter,
                System.Action<object, TProperty> setter,
                string jsonPropertyName,
                byte[] nameAsUtf8Bytes,
                byte[] escapedNameSection,
                JsonIgnoreCondition? ignoreCondition,
                JsonNumberHandling? numberHandling)
        {
            JsonSerializerOptions options = GetOptions();
            JsonPropertyInfo<TProperty> jsonPropertyInfo = JsonPropertyInfo<TProperty>.Create();
            jsonPropertyInfo.Options = options;

            if (nameAsUtf8Bytes != null && options.PropertyNamingPolicy == null)
            {
                jsonPropertyInfo.NameAsString = jsonPropertyName ?? clrPropertyName;
                jsonPropertyInfo.NameAsUtf8Bytes = nameAsUtf8Bytes;
                jsonPropertyInfo.EscapedNameSection = escapedNameSection;
            }
            else
            {
                jsonPropertyInfo.NameAsString = jsonPropertyName
                    ?? options.PropertyNamingPolicy?.ConvertName(clrPropertyName)
                    ?? (options.PropertyNamingPolicy == null
                            ? null
                            : throw new System.InvalidOperationException("TODO: PropertyNamingPolicy cannot return null."));
                // NameAsUtf8Bytes and EscapedNameSection will be set in CompleteInitialization() below.
            }

            if (ignoreCondition != JsonIgnoreCondition.Always)
            {
                jsonPropertyInfo.Get = getter;
                jsonPropertyInfo.Set = setter;
                jsonPropertyInfo.ConverterBase = converter ?? throw new System.NotSupportedException("TODO: need custom converter here?");
                jsonPropertyInfo.RuntimeClassInfo = classInfo;
                jsonPropertyInfo.DeclaredPropertyType = typeof(TProperty);
                jsonPropertyInfo.DeclaringType = declaringType;
                jsonPropertyInfo.IgnoreCondition = ignoreCondition;
                jsonPropertyInfo.NumberHandling = numberHandling;
                jsonPropertyInfo.MemberType = memberType;
            }
            jsonPropertyInfo.CompleteInitialization();
            return jsonPropertyInfo;
        }
    }
}

JsonContext.GetJsonClassInfo.g.cs

using Startup;
using System;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Converters;
using System.Text.Json.Serialization.Metadata;

namespace Startup.JsonSourceGeneration
{
    internal partial class JsonContext : JsonSerializerContext
    {
        public override JsonClassInfo GetJsonClassInfo(System.Type type)
        {
            if (type == typeof(Startup.MyClass))
            {
                return this.MyClass;
            }

            return null!;
        }
    }
}

MyClass.g.cs

using Startup;
using System;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Converters;
using System.Text.Json.Serialization.Metadata;

namespace Startup.JsonSourceGeneration
{
    internal partial class JsonContext : JsonSerializerContext
    {
        private JsonTypeInfo<Startup.MyClass> _MyClass;
        public JsonTypeInfo<Startup.MyClass> MyClass
        {
            get
            {
                if (_MyClass == null)
                {
                    JsonSerializerOptions options = GetOptions();

                    JsonConverter customConverter;
                    if (options.Converters.Count > 0 && (customConverter = GetRuntimeProvidedCustomConverter(typeof(Startup.MyClass), options)) != null)
                    {
                        _MyClass = new JsonValueInfo<Startup.MyClass>(customConverter, options);
                        _MyClass.NumberHandling = null;
                    }
                    else
                    {
                        JsonObjectInfo<Startup.MyClass> objectInfo = new(createObjectFunc: static () => new Startup.MyClass(), this.GetOptions());
                        objectInfo.NumberHandling = null;
                        _MyClass = objectInfo;
    
                        objectInfo.AddProperty(CreateProperty<System.Int32>(
                            clrPropertyName: "MyInt",
                            memberType: System.Reflection.MemberTypes.Property,
                            declaringType: typeof(Startup.MyClass),
                            classInfo: this.Int32,
                            converter: this.Int32.ConverterBase,
                            getter: static (obj) => { return ((Startup.MyClass)obj).MyInt; },
                            setter: static (obj, value) => { ((Startup.MyClass)obj).MyInt = value; },
                            jsonPropertyName: null,
                            nameAsUtf8Bytes: new byte[] {77,121,73,110,116},
                            escapedNameSection: new byte[] {34,77,121,73,110,116,34,58},
                            ignoreCondition: null,
                            numberHandling: null));
                    
                        objectInfo.AddProperty(CreateProperty<System.String[]>(
                            clrPropertyName: "MyStrings",
                            memberType: System.Reflection.MemberTypes.Property,
                            declaringType: typeof(Startup.MyClass),
                            classInfo: this.StringArray,
                            converter: this.StringArray.ConverterBase,
                            getter: static (obj) => { return ((Startup.MyClass)obj).MyStrings; },
                            setter: static (obj, value) => { ((Startup.MyClass)obj).MyStrings = value; },
                            jsonPropertyName: null,
                            nameAsUtf8Bytes: new byte[] {77,121,83,116,114,105,110,103,115},
                            escapedNameSection: new byte[] {34,77,121,83,116,114,105,110,103,115,34,58},
                            ignoreCondition: null,
                            numberHandling: null));
                    
                        objectInfo.CompleteInitialization();
                    }
                }

                return _MyClass;
            }
        }
    }
}

Int32.g.cs

using System;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Converters;
using System.Text.Json.Serialization.Metadata;

namespace Startup.JsonSourceGeneration
{
    internal partial class JsonContext : JsonSerializerContext
    {
        private JsonTypeInfo<System.Int32> _Int32;
        public JsonTypeInfo<System.Int32> Int32
        {
            get
            {
                if (_Int32 == null)
                {
                    JsonSerializerOptions options = GetOptions();

                    JsonConverter customConverter;
                    if (options.Converters.Count > 0 && (customConverter = GetRuntimeProvidedCustomConverter(typeof(System.Int32), options)) != null)
                    {
                        _Int32 = new JsonValueInfo<System.Int32>(customConverter, options);
                        _Int32.NumberHandling = null;
                    }
                    else
                    {
                        _Int32 = new JsonValueInfo<System.Int32>(new Int32Converter(), options);
                        _Int32.NumberHandling = null;
                    }
                }

                return _Int32;
            }
        }
    }
}

StringArray.g.cs

using System;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Converters;
using System.Text.Json.Serialization.Metadata;

namespace Startup.JsonSourceGeneration
{
    internal partial class JsonContext : JsonSerializerContext
    {
        private JsonTypeInfo<System.String[]> _StringArray;
        public JsonTypeInfo<System.String[]> StringArray
        {
            get
            {
                if (_StringArray == null)
                {
                    JsonSerializerOptions options = GetOptions();

                    JsonConverter customConverter;
                    if (options.Converters.Count > 0 && (customConverter = GetRuntimeProvidedCustomConverter(typeof(System.String[]), options)) != null)
                    {
                        _StringArray = new JsonValueInfo<System.String[]>(customConverter, options);
                        _StringArray.NumberHandling = null;
                    }
                    else
                    {
                        _StringArray = KnownCollectionTypeInfos<System.String>.GetArray(this.String, this);
                        _StringArray.NumberHandling = null;
                    }
                }

                return _StringArray;
            }
        }
    }
}

String.g.cs

using System;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Converters;
using System.Text.Json.Serialization.Metadata;

namespace Startup.JsonSourceGeneration
{
    internal partial class JsonContext : JsonSerializerContext
    {
        private JsonTypeInfo<System.String> _String;
        public JsonTypeInfo<System.String> String
        {
            get
            {
                if (_String == null)
                {
                    JsonSerializerOptions options = GetOptions();

                    JsonConverter customConverter;
                    if (options.Converters.Count > 0 && (customConverter = GetRuntimeProvidedCustomConverter(typeof(System.String), options)) != null)
                    {
                        _String = new JsonValueInfo<System.String>(customConverter, options);
                        _String.NumberHandling = null;
                    }
                    else
                    {
                        _String = new JsonValueInfo<System.String>(new StringConverter(), options);
                        _String.NumberHandling = null;
                    }
                }

                return _String;
            }
        }
    }
}

The generated type metadata can then be passed to new (de)serialization overloads as follows:

MyClass obj = new()
{
    MyInt = 1,
    MyString = "Hello",
};

byte[] serialized = JsonSerializer.SerializeToUtf8Bytes(obj, JsonContext.Default.MyClass);
obj = JsonSerializer.Deserialize<MyClass>(serialized, JsonContext.Default.MyClass);

Console.WriteLine(obj.MyInt); // 1
Console.WriteLine(obj.MyString); // “Hello”

Using the source generator

The source generator can be consumed in any .NET C# project, including console application, class libraries, and Blazor applications. If the application's TFM is net6.0 or upwards (inbox scenarios), then the generator will be part of the SDK(TODO: is this technically correct). For out-of-box scenarios such as .NET framework applications and .NET Standard-compatible libaries, the generator can be consumed via a System.Text.Json NuGet package reference. See "Inbox Source Generators" for details on how we can ship inbox source generators.

Design

Type discovery

Type discovery refers to how the source generator is made aware of types that will be passed to JsonSerializer. An explicit model where users manually indicate each type using a new assembly-level attribute ([assembly: JsonSerializable(Type)]) is employed. This model is safe and ensures that we do not skip any types, or include unwanted types. In the future we can introduce an implicit model where the generator scan source files for Ts and Type instances passed to the various JsonSerializer methods, but it is expected that the explicit model remains relevant to cover cases where serializable types cannot easily be detected by inspecting source code.

Here are some sample usages of the attribute for type discovery:

[assembly: JsonSerializable(typeof(MyClass))]
[assembly: JsonSerializable(typeof(object[]))]
[assembly: JsonSerializable(typeof(string), CanBeDynamic = true)]
[assembly: JsonSerializable(typeof(int), CanBeDynamic = true)]

_ = JsonSerializer.Serialize(new MyClass(), JsonContext.Default.MyClass);
_ = JsonSerializer.Serialize(new object[] { "Hello", 1 }, JsonContext.Default.ObjectArray);

The attribute instances instruct the source generator to generate serialization metadata for the MyClass, object[], string, and int types. The CanBeDynamic property tells the generator to structure its output such that the metadata for the string, int can be retrieved by an internal dictionary-based look up with the types as keys, since those values will be serialized polymorphically. The MyClass and object[] types will not be serialized polymorphically, so there is no need to specify CanBeDynamic.

Based on the serializer usages in the example, we could expect that a source generator can inspect this code and automatically figure out what type needs to be serialized. This expectation is valid, and such functionality can be provided in the future. However, the explicit model will still be needed to handle scenarios such as the following:

[assembly: JsonSerializable(typeof(MyType))]

byte[] payload = GetJsonPayload();
Type type = GetSomeType(); // the returned type could be `MyType`

_ = JsonSerializer.Deserialize(payload, type, JsonContext.Default); // Passing the entire context instance, so the serializer will call `JsonSerializerContext.GetJsonClassInfo` to get the metadata for the `type`.

Generated metadata

There are three major classes of types, corresponding to three major generated-metadata representations: primitives, objects (types that map to JSON object representations), and collections.

TODO: deep dive into different types of generated metadata.

How generating metadata helps meet goals

Faster startup & reduced private memory

Moving the generation of type metadata from runtime to compile time means that there is less work for the serializer to do on start-up, which leads to a reduction in the amount of time it takes to perform the first serialization or deserialization of each type.

The serializer uses Reflection.Emit where possible to generate fast member accessors to constructors, properties, and fields. Generating these IL methods takes a non-trivial time, but also consumes private memory. With source generators, we are able to generate delegates that statically invoke these accessors. These delegates are the used by the serializer alongside other metadata. This eliminates time and allocation cost due to Reflection emit.

All serialization and deserialization of JSON data is ultimately peformed within System.Text.Json converters. Today the serializer statically initializes several built-in converter instances to provide default functionality. User applications pay the cost of these allocations, even when only a few of these converters are needed given the input object graphs. With source generation, we can initialize only the converters that are needed by the types indicated to the generator.

Given a very simple POCO like the one used for the TechEmpower benchmark, we can observe startup improvements when serializing and deserializing instances of the type:

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

Old

Private bytes (KB): 2786.0

Elapsed time (ms): 41.0

New

Private bytes (KB): 2532.0

Elapsed time (ms): 30.0

Writer

Private bytes (KB): 337.0

Elapsed time (ms): 8.25

Deserialize

Old

Private bytes (KB): 1450

Elapsed time (ms): 30.25

New

Private bytes (KB): 916.0

Elapsed time (ms): 13.0

Reader*

Private bytes (KB): 457.0

Elapsed time (ms): 5.0

* not fair as only one prop with no complex lookup

Given the following object graph designed to similate a microservice that returns the weather forecase for the next 5 days, we can also notice startup improvements:

public class WeatherForecast
{
    public DateTime Date { get; set; }

    public int TemperatureC { get; set; }

    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

    public string Summary { get; set; }
}
Serialize

Old

Private bytes (KB): 3209.0

Elapsed time (ms): 48.25

New

Private bytes (KB): 2693.0

Elapsed time (ms): 36.0

Writer

Private bytes (KB): 815

Elapsed time (ms): 15.5

Deserialize

Old

Private bytes (KB): 1698.0

Elapsed time (ms): 36.5

New

Private bytes (KB): 1093

Elapsed time (ms): 19.5

It is natural to wonder if we could yield more performance here given the simple scenarios and the performance of the low level reader and writer. It is indeed possible to do so in limited scenarios like the ones described above. It is in scope for 6.0 to provide a mode that is closer in performance to the reader and writer. It is also planned to provide more knobs in the future to provide better throughput for more complex scenarios. See the "Throughput" section below for more notes on this.

Also see this gist to see how the startup performance scales as we add more types, properties, and serialization attributes. TODO: get updated numbers.

Reduced app size

By generating metadata at compile-time instead of at runtime, two major things are done to reduce the size of the consuming application. First, we can detect which custom or built-in System.Text.Json converter types are needed in the application at runtime, and reference them statically in the generated metadata. This allows the ILLinker to trim out JSON converter types which will not be needed in the application at runtime. Similarly, reflecting over input types at compile-time eradicates the need to do so at compile-time. This eradicates the need for lots of System.Reflection APIs at runtime, so the ILLinker can trim code internal code in System.Text.Json which interacts with those APIs. Unused source code further in the dependency graph is also trimmed out.

Size reductions are based on how JsonSerializerOptions instances are created. If you use the existing patterns, then all the converters and reflection-based support will be rooted in the application for backwards compat:

var options = new JsonSerializerOptions(); // or
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);

If you use a new pattern, then we can shed these types when unused:

var options = JsonSerializerOptions.CreateForSizeOpts(); // or
var options = JsonSerializerOptions.CreateForSizeOpts(JsonSerializerDefaults.Web); // same overload, optional defaults

When copying options, the new instance inherits the pattern of the old instance:

var newOptions = new JsonSerializerOptions(oldOptions); 

TODO: NotSupportedException is thrown if options is created for size, and metadata for a type hasn't been provided.

TODO: do we need to expose a JsonSerializerOptions.IsOptimizedForSize so that users can know whether they can use an options instance with existing overloads.

Given the weather forecast scenario from above, we can observe size reductions in both a Console app and the default Blazor app when processing JSON data. In a Blazor app, we get **~ 121 KB** of compressed dll size savings. In a console app we get about 400 KB in size reductions, post cross-gen and linker trimming. TODO: get updated numbers for console app.

TODO: if using POCO with 5 props as example, how many of those would we need in app before we start making the app bigger?

ILLinker friendliness

By avoiding runtime reflection, we avoid the primary coding pattern that is unfriendly to ILLinker analysis. Given that this code is trimmed out, applications that use the System.Text.Json go from having several ILLinker analysis warnings when trimming to having absolutely none. This means that applciations that use the System.Text.Json source generator can be safely trimmed, provided there are no other warnings due to the user app itself, or other parts of the BCL.

Throughput

Why not generate serialization code directly, using the Utf8JsonReader and Utf8JsonWriter?

The metadata-based design does not currently provide throughput improvements, but is forward-compatible with doing so in the future. Improving throughput means generating specific serialization and deserialization logic for each type in the object graph. Various serializer features would need to be baked into the generated code including:

  • Setting and getting properties.
  • Calling the constructor, possibly with values from JSON.
  • Null handling (e.g. if a null property should be serialized).
  • Property lookup on deserialization (match JSON property name to Type property) potenially case-insensitive.
  • Property naming policies (such as camel-casing).
  • Escaping and unescaping property names and values (potentially using a custom escaper).
  • Object reference handling (to preserve object references).
  • Extension data (preserving umatched JSON during deserialization so it can be written during serialization).
  • Async support (supporting a mode that doesn't drain a Stream upfront).
  • Use of custom converters (where logic\code is not known so it can't be pushed to code-gen).
  • Composition of objects (List<Dictionary<string, Poco>> or SalesOrder.Customer.Address.City).
  • Reading quoted numbers
  • New features over time (extended polymorphic support, IAsyncEnumerable...)

This can lead to a large, complex, and unserviceable amount of code to generate. The large amount of code that can potentially be generated also conflicts with the goal of app size reductions. With this in mind, we decided to start with an approach that works well for all usages of the serializer.

In the future, we plan to build on the design and provide knobs to employ different source generation modes that also improve throughput for different scenarios. For instance, an application can indicate to the generator ahead of time that it will not use a naming policy or custom converters at runtime. With this information, we could generate a reasonable amount of very fast serialization and deserialization logic using the writer and reader directly. Another mode could be to generate optimal logic for a happy-path scenario, but also generate metadata as a fallback in case runtime-specified options need more complex logic.

A mode to provide throughput improvements for simple scenarios like the TechEmpower JSON benchmark is in scope for .NET 6.0, and this design will be updated to include it.

API Proposal

(click to view)

System.Text.Json.SourceGeneration.dll

The following API will be generated into the compiling assembly, to mark serializable types.

namespace `System.Text.Json`.SourceGeneration
{
    /// <summary>
    /// Instructs the `System.Text.Json` source generator to generate serialization metadata for a specified type at compile time.
    /// </summary>
    [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
    public sealed class JsonSerializableAttribute : Attribute
    {
        /// <summary>
        /// Indicates whether the specified type might be the runtime type of an object instance which was declared as
        /// a different type (polymorphic serialization).
        /// </summary>
        public bool CanBeDynamic { get; set; }
        /// <summary>
        /// Initializes a new instance of <see cref=""JsonSerializableAttribute""/> with the specified type.
        /// </summary>
        /// <param name=""type"">The Type of the property.</param>
        public JsonSerializableAttribute(Type type) { }
    }
}

System.Text.Json.dll

namespace `System.Text.Json`
{
    public static partial class JsonSerializer
    {
        public static object? Deserialize(string json, Type type, JsonSerializerContext jsonSerializerContext) { throw null; }
        public static object? Deserialize(ref Utf8JsonReader reader, Type returnType, JsonSerializerContext jsonSerializerContext) { throw null; }
        public static ValueTask<object?> DeserializeAsync(Stream utf8Json, Type returnType, JsonSerializerContext jsonSerializerContext, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
        public static ValueTask<TValue?> DeserializeAsync<TValue>(Stream utf8Json, JsonSerializerContext jsonSerializerContext, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
        public static ValueTask<TValue?> DeserializeAsync<TValue>(Stream utf8Json, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
        public static TValue? Deserialize<TValue>(System.ReadOnlySpan<byte> utf8Json, JsonTypeInfo<TValue> jsonTypeInfo) { throw null; }
        public static TValue? Deserialize<TValue>(string json, JsonSerializerContext jsonSerializerContext) { throw null; }
        public static TValue? Deserialize<TValue>(string json, JsonTypeInfo<TValue> jsonTypeInfo) { throw null; }
        public static TValue? Deserialize<TValue>(ref Utf8JsonReader reader, JsonTypeInfo<TValue> jsonTypeInfo) { throw null; }
        public static string Serialize(object? value, Type inputType, JsonSerializerContext jsonSerializerContext) { throw null; }
        public static byte[] SerializeToUtf8Bytes<TValue>(TValue value, JsonTypeInfo<TValue> jsonTypeInfo) { throw null; }
        public static string Serialize<TValue>(TValue value, JsonSerializerContext jsonSerializerContext) { throw null; }
        public static string Serialize<TValue>(TValue value, JsonTypeInfo<TValue> jsonTypeInfo) { throw null; }
    }

    public sealed partial class JsonSerializerOptions
    {
        public static JsonSerializerOptions CreateForSizeOpts(JsonSerializerDefaults defaults = default) { throw null; }
    }
}

namespace `System.Text.Json`.Serialization
{
    public partial class JsonSerializerContext : System.IDisposable
    {
        public JsonSerializerContext() { }
        public JsonSerializerContext(JsonSerializerOptions options) { }
        public void Dispose() { }
        protected virtual void Dispose(bool disposing) { }
        public virtual JsonClassInfo? GetJsonClassInfo(Type type) { throw null; }
        public JsonSerializerOptions GetOptions() { throw null; }
    }
}

namespace `System.Text.Json`.Serialization.Converters
{
    public sealed class BooleanConverter : JsonConverter<bool>
    {
        public BooleanConverter() { }
        public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw null; }
        public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) {  }
    }
    public sealed class ByteArrayConverter : JsonConverter<byte[]>
    {
        public ByteArrayConverter() { }
        public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw null; }
        public override void Write(Utf8JsonWriter writer, byte[] value, JsonSerializerOptions options) { }
    }
    public sealed class ByteConverter : JsonConverter<byte>
    {
        public ByteConverter() { }
        public override byte Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw null; }
        public override void Write(Utf8JsonWriter writer, byte value, JsonSerializerOptions options) { }
    }
    public sealed class CharConverter : JsonConverter<char>
    {
        public CharConverter() { }
        public override char Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw null; }
        public override void Write(Utf8JsonWriter writer, char value, JsonSerializerOptions options) { }
    }
    public sealed class DateTimeConverter : JsonConverter<System.DateTime>
    {
        public DateTimeConverter() { }
        public override System.DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw null; }
        public override void Write(Utf8JsonWriter writer, System.DateTime value, JsonSerializerOptions options) { }
    }
    public sealed class DateTimeOffsetConverter : JsonConverter<System.DateTimeOffset>
    {
        public DateTimeOffsetConverter() { }
        public override System.DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw null; }
        public override void Write(Utf8JsonWriter writer, System.DateTimeOffset value, JsonSerializerOptions options) { }
    }
    public sealed class DecimalConverter : JsonConverter<decimal>
    {
        public DecimalConverter() { }
        public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw null; }
        public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options) { }
    }
    public sealed class DoubleConverter : JsonConverter<double>
    {
        public DoubleConverter() { }
        public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw null; }
        public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options) { }
    }
    public sealed class EnumConverter<T> : JsonConverter<T> where T : struct, Enum
    {
        public EnumConverter(JsonSerializerOptions serializerOptions) { }
        public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw null; }
        public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { }
    }
    public sealed class GuidConverter : JsonConverter<System.Guid>
    {
        public GuidConverter() { }
        public override System.Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw null; }
        public override void Write(Utf8JsonWriter writer, System.Guid value, JsonSerializerOptions options) { }
    }
    public sealed class Int16Converter : JsonConverter<short>
    {
        public Int16Converter() { }
        public override short Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw null; }
        public override void Write(Utf8JsonWriter writer, short value, JsonSerializerOptions options) { }
    }
    public sealed class Int32Converter : JsonConverter<int>
    {
        public Int32Converter() { }
        public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw null; }
        public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options) { }
    }
    public sealed class Int64Converter : JsonConverter<long>
    {
        public Int64Converter() { }
        public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw null; }
        public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOptions options) { }
    }
    public sealed class NullableConverter<T> : JsonConverter<T?> where T : struct
    {
        public NullableConverter(JsonConverter<T> converter) { }
        public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw null; }
        public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) { }
    }
    public sealed class ObjectConverter : JsonConverter<object>
    {
        public ObjectConverter() { }
        public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw null; }
        public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) { }
    }
    public sealed class SingleConverter : JsonConverter<float>
    {
        public SingleConverter() { }
        public override float Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw null; }
        public override void Write(Utf8JsonWriter writer, float value, JsonSerializerOptions options) { }
    }
    [System.CLSCompliant(false)]
    public sealed class SByteConverter : JsonConverter<sbyte>
    {
        public SByteConverter() { }
        public override sbyte Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw null; }
        public override void Write(Utf8JsonWriter writer, sbyte value, JsonSerializerOptions options) { }
    }
    public sealed class StringConverter : JsonConverter<string>
    {
        public StringConverter() { }
        public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw null; }
        public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) { }
    }
    [System.CLSCompliant(false)]
    public sealed class UInt16Converter : JsonConverter<ushort>
    {
        public UInt16Converter() { }
        public override ushort Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw null; }
        public override void Write(Utf8JsonWriter writer, ushort value, JsonSerializerOptions options) { }
    }
    [System.CLSCompliant(false)]
    public sealed class UInt32Converter : JsonConverter<uint>
    {
        public UInt32Converter() { }
        public override uint Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw null; }
        public override void Write(Utf8JsonWriter writer, uint value, JsonSerializerOptions options) { }
    }
    [System.CLSCompliant(false)]
    public sealed class UInt64Converter : JsonConverter<ulong>
    {
        public UInt64Converter() { }
        public override ulong Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw null; }
        public override void Write(Utf8JsonWriter writer, ulong value, JsonSerializerOptions options) { }
    }
    public sealed class UriConverter : JsonConverter<Uri>
    {
        public UriConverter() { }
        public override Uri Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw null; }
        public override void Write(Utf8JsonWriter writer, Uri value, JsonSerializerOptions options) { }
    }
    public sealed class VersionConverter : JsonConverter<Version>
    {
        public VersionConverter() { }
        public override Version Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw null; }
        public override void Write(Utf8JsonWriter writer, Version value, JsonSerializerOptions options) { }
    }
}

namespace System.Text.Json.Serialization.Metadata
{
    public partial class JsonClassInfo
    {
        internal JsonClassInfo() { }
        public JsonNumberHandling? NumberHandling { get { throw null; } set { } }
        public JsonConverter ConverterBase { get { throw null; } }
        public JsonClassInfo.ConstructorDelegate? CreateObject { get { throw null; } set { } }
        public JsonSerializerOptions Options { get { throw null; } }
        public Type Type { get { throw null; } }
        public delegate object? ConstructorDelegate();
    }
    public sealed partial class JsonObjectInfo<T> : JsonTypeInfo<T>
    {
        public JsonObjectInfo(JsonClassInfo.ConstructorDelegate? createObjectFunc, JsonSerializerOptions options) { }
        public void AddProperty(JsonPropertyInfo jsonPropertyInfo) { throw null; }
        public void CompleteInitialization() { }
    }
    public abstract partial class JsonPropertyInfo
    {
        internal JsonPropertyInfo() { }
        public byte[] EscapedNameSection { get { throw null; } set { } }
        public byte[] NameAsUtf8Bytes { get { throw null; } set { } }
        public abstract JsonConverter ConverterBase { get; set; }
        public Type DeclaredPropertyType { get { throw null; } set { } }
        public Type DeclaringType { get { throw null; } set { } }
        public string NameAsString { get { throw null; } set { } }
        public bool ShouldDeserialize { get { throw null; } }
        public bool ShouldSerialize { get { throw null; } }
        public JsonIgnoreCondition? IgnoreCondition { get { throw null; } set { } }
        public JsonNumberHandling? NumberHandling { get { throw null; } set { } }
        public System.Reflection.MemberTypes MemberType { get { throw null; } set { } }
        public JsonSerializerOptions Options { get { throw null; } set { } }
        public JsonClassInfo RuntimeClassInfo { get { throw null; } set { } }
    }
    public sealed partial class JsonPropertyInfo<T> : JsonPropertyInfo
    {
        public JsonConverter<T> Converter { get { throw null; } }
        public override JsonConverter ConverterBase { get { throw null; } set { } }
        public Func<object, T>? Get { get { throw null; } set { } }
        public Action<object, T>? Set { get { throw null; } set { } }
        public void CompleteInitialization() { }
        public static JsonPropertyInfo<T> Create() { throw null; }
    }
    public abstract partial class JsonTypeInfo<T> : JsonClassInfo
    {
        internal JsonTypeInfo() { }
        public void RegisterToOptions() { }
    }
    public sealed partial class JsonValueInfo<T> : JsonTypeInfo<T>
    {
        public JsonValueInfo(JsonConverter converter, JsonSerializerOptions options) { }
    }
    public sealed partial class JsonCollectionTypeInfo<T> : JsonTypeInfo<T>
    {
        public JsonCollectionTypeInfo(JsonClassInfo.ConstructorDelegate createObjectFunc, JsonConverter<T> converter, JsonClassInfo elementClassInfo, JsonSerializerOptions options) { }
        public JsonCollectionTypeInfo(JsonClassInfo.ConstructorDelegate createObjectFunc, JsonConverter<T> converter, JsonClassInfo elementClassInfo, JsonSerializerOptions options) { }
    }
    public static partial class KnownCollectionTypeInfos<T>
    {
        public static JsonCollectionTypeInfo<T[]> GetArray(JsonClassInfo elementClassInfo, JsonSerializerContext context) { throw null; }
        public static JsonCollectionTypeInfo<IEnumerable<T>> GetIEnumerable(JsonClassInfo elementClassInfo, JsonSerializerContext context) { throw null; }
        public static JsonCollectionTypeInfo<IList<T>> GetIList(JsonClassInfo elementClassInfo, JsonSerializerContext context) { throw null; }
        public static JsonCollectionTypeInfo<List<T>> GetList(JsonClassInfo elementClassInfo, JsonSerializerContext context) { throw null; }
    }
    public static partial class KnownDictionaryTypeInfos<TKey, TValue> where TKey : notnull
    {
        public static JsonCollectionTypeInfo<Dictionary<TKey, TValue>> GetDictionary(JsonClassInfo keyClassInfo, JsonClassInfo valueClassInfo, JsonSerializerContext context) { throw null; }
    }
}

System.Net.Http.Json.dll

namespace System.Net.Http.Json
{
    public static partial class HttpClientJsonExtensions
    {
        public static Task<object?> GetFromJsonAsync(this HttpClient client, string? requestUri, Type type, JsonSerializerOptions? options, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
        public static Task<object?> GetFromJsonAsync(this HttpClient client, string? requestUri, Type type, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
        public static Task<object?> GetFromJsonAsync(this HttpClient client, Uri? requestUri, Type type, JsonSerializerOptions? options, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
        public static Task<object?> GetFromJsonAsync(this HttpClient client, Uri? requestUri, Type type, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
        public static Task<TValue?> GetFromJsonAsync<TValue>(this HttpClient client, string? requestUri, JsonSerializerContext? context, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
        public static Task<TValue?> GetFromJsonAsync<TValue>(this HttpClient client, string? requestUri, JsonSerializerOptions? options, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
        public static Task<TValue?> GetFromJsonAsync<TValue>(this HttpClient client, string? requestUri, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
        public static Task<TValue?> GetFromJsonAsync<TValue>(this HttpClient client, Uri? requestUri, JsonSerializerOptions? options, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
        public static Task<TValue?> GetFromJsonAsync<TValue>(this HttpClient client, Uri? requestUri, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
        public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(this HttpClient client, string? requestUri, TValue value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
        public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(this HttpClient client, string? requestUri, TValue value, CancellationToken cancellationToken) { throw null; }
        public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(this HttpClient client, Uri? requestUri, TValue value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
        public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(this HttpClient client, Uri? requestUri, TValue value, CancellationToken cancellationToken) { throw null; }
        public static Task<HttpResponseMessage> PutAsJsonAsync<TValue>(this HttpClient client, string? requestUri, TValue value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
        public static Task<HttpResponseMessage> PutAsJsonAsync<TValue>(this HttpClient client, string? requestUri, TValue value, CancellationToken cancellationToken) { throw null; }
        public static Task<HttpResponseMessage> PutAsJsonAsync<TValue>(this HttpClient client, Uri? requestUri, TValue value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
        public static Task<HttpResponseMessage> PutAsJsonAsync<TValue>(this HttpClient client, Uri? requestUri, TValue value, CancellationToken cancellationToken) { throw null; }
    }

    public static partial class HttpContentJsonExtensions
    {
        public static Task<object?> ReadFromJsonAsync(this HttpContent content, Type type, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }

        public static Task<object?> ReadFromJsonAsync(this HttpContent content, JsonClassInfo jsonTypeInfo, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
        public static Task<object?> ReadFromJsonAsync(this HttpContent content, JsonSerializerContext, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }


        public static Task<T?> ReadFromJsonAsync<T>(this HttpContent content, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default(CancellationToken)) { throw null; }
    }
}

Implementation considerations

Compatibility with existing JsonSerializer functionality

All functionality that exists in JsonSerializer today will continue to do so after this new feature is implemented, assuming the JsonSerializerOptions are not created for size optimizations. This includes rooting all converters and reflection-based logic for a runtime warm up of the serializer if the existing methods are used. If the JsonSerializerOptions.CreateForSizeOpts method is used to create the options, then a NotSupportedException will be thrown if serialization is attempted.

What is the model for implementing and maintaining new features moving forward

For the first release, every feature that the serializer supports will be supported when the source generator is used. This involves implementing logic in System.Text.Json.SourceGeneration.dll to recognize static options like serializationa attributes. Each new feature needs to be implemented in the serializer (System.Text.Json.dll), but now the source generator must also learn how to check whether it is used at design time, so that the appropriate metadata can be generated.

Interaction between options instances and context classes

With the current design, a single JsonSerializerOptions instance can be used in multiple JsonSerializerContext classes. Multiple context instances could populate the options instance with metadata like converters and customization options. This presents a challenge, as APIs like JsonSerializerOptions.GetConverter(Type) must now depend on first-one-wins semantics among the context classes to fetch a converter from. This can be worked around with validation to enforce a 1:1 mapping from options instance to context instance. An alternative option might be to make the JsonSerializerContext type derive from JsonSerializerOptions. This enforces a 1:1 pairing, and might also be a more natural representation of the relationship given that options instances cache references to type metadata, as opposed to being just a lightweight POCO for holding runtime-specified serializer configuration.

TODO: Discuss context class deriving from JsonSerializerOptions

What does the metadata pattern mean for applications, APIs, services that perform JsonSerialization on behalf of others?

Services that perform JSON processing on behalf of others, such as Blazor client APIs, ASP.NET MVC, and APIs in the System.Net.Http.Json library must provide new APIs to accept JsonSerializerContext instances that were generated in the user's assembly.

Versioning

TODO. How to ensure we don't invoke stale/buggy metadata implementations?

Integer property on generated JsonContext class(es) which we can check at runtime.

@layomia layomia added this to the 6.0.0 milestone Dec 2, 2020
@layomia layomia self-assigned this Dec 2, 2020
@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added the untriaged New issue has not been triaged by the area owner label Dec 2, 2020
@layomia layomia removed the untriaged New issue has not been triaged by the area owner label Dec 2, 2020
@layomia layomia changed the title Work items for JSON source generation JSON serialization source generator Dec 2, 2020
@layomia layomia changed the title JSON serialization source generator Compile-time source generation for System.Text.Json Mar 31, 2021
@layomia layomia added api-needs-work API needs work before it is approved, it is NOT ready for implementation tenet-performance Performance related issue labels Mar 31, 2021
@terrajobst terrajobst added api-ready-for-review API is ready for review, it is NOT ready for implementation blocking Marks issues that we want to fast track in order to unblock other important work and removed api-needs-work API needs work before it is approved, it is NOT ready for implementation labels Mar 31, 2021
@terrajobst terrajobst added api-needs-work API needs work before it is approved, it is NOT ready for implementation and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Apr 6, 2021
@terrajobst
Copy link
Member

terrajobst commented Apr 6, 2021

Video

  • General notes

    • The current proposal is that building application that use linking by default (Blazor, iOS, Android)
    • This means that by default, starting, in .NET, those application models will warn on usage of JSON serialization/deserialization because those will be marked as linker unfriendly. Can we improve this, by, for example, having more of a progressive mode?
    • Since this proposal is generating the serializer on a per assembly boundary, and each assembly's context contains the serialization of the type closure, it means the closure of assemblies is a single versioning bubble. In other words, if a type in a dependency adds new property, the serializer won't know about until it's being re-generated. That's different from the reflection approach.
  • JsonContext

    • Consider dropping From and instead expose the constructor taking options directly
    • It seems unfortunate that for users to take advantage of the source generated serializer all call sites need to change to pass in the context/type info. This might make it harder for frameworks like ASP.NET Core that serialize user provided types without having access to the generated JsonContext
    • The SerializeXxx funcs are currently hardcoded. We need to make sure that they work for options that the context is tied to. We need to make sure that the generated code is correct; for example, it needs to honor the casing policy. It's OK to have a slower path for non-standard configuration, but we can't ignore the supplied configuration.
    • Can we also get a generic GetTypeInfo<T>()

@layomia layomia added api-ready-for-review API is ready for review, it is NOT ready for implementation and removed api-needs-work API needs work before it is approved, it is NOT ready for implementation labels Apr 8, 2021
@bartonjs
Copy link
Member

bartonjs commented Apr 8, 2021

Video

  • Consider changing JsonSerializerOptions.SetContext to JsonSerializerOptions.AddContext
    • Even if it would throw on a second call now, it allows either a stack or list-based implementation in the future, to combine contexts across assemblies.
  • Consider adding JsonSerializerContext.CreateTypeInfoViaReflection to enable dynamic scenarios in the context-preferred world.
partial class JsonSerializerContext
{
    public static JsonTypeInfo<T> CreateTypeInfoViaReflection<T>(JsonSerializerOptions options);
    public static JsonTypeInfo CreateTypeInfoViaReflection(Type type, JsonSerializerOptions options);
}
  • JsonSerializerContext.GetTypeInfo and JsonSerializerContext.GetTypeInfo should both be abstract
  • Remove the default ctor on JsonSerializerContext and hide the null options state
partial class JsonSerializerContext
{
    // This can use lazy initialization, or other techniques, to make AddContext work without creating an options.
    public JsonSerializerOptions Options { get; }

    protected JsonSerializerContext(JsonSerializerOptions? options = null) { }
}
  • Make JsonTypeInfo.Converter a non-public property, if possible
  • It feels like JsonTypeInfo should be abstract if JsonTypeInfo is. But it's not important since both types have no public ctors.
  • JsonSerializableAttribute.TypeInfoPropertyName should be declared nullable
  • Move JsonSerializableAttribute to System.Text.Json.Serialization
  • Collapse System.Text.Json.Serialization.Metadata.Internal into System.Text.Json.Serialization.Metadata
  • Rename MetadataServices to JsonMetadataServices
  • We discussed if it would make sense to replace the JsonNumberHandling parameter to InitializeObject be something to represent all possible type-specific attributes, but decided that could be deferred until we had a second thing
  • We discussed if the JsonNumberHandling parameter to InitializeObject should be nullable, and decided it didn't need to because the generator can already distinguish between using an attribute-specified value or options.DefaultNumberHandling
  • Rename CreatePropertyInfo's clrPropertyName to propertyName, declaredPropertyName, runtimePropertyName, or memberName.
  • Move all the members on ConverterProvider to JsonMetadataServices, adding a "Converter" suffix to each property.
  • Are we missing scenarios/overloads for the System.Net.Http.Json members that don't currently accept a JsonSerializerOptions?
  • We're missing IList/IDictionary versions of JsonMetadataServices.Create* members.

@bartonjs bartonjs added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Apr 8, 2021
@jkotas
Copy link
Member

jkotas commented Apr 11, 2021

// ASP.NET asked for this
virtual JsonTypeInfo? GetTypeInfo() { throw null; }

Can we add better justification for this than just "ASP.NET asked for this"? What are the examples where this overload is expected to be used, does it really need to be generic virtual method?

Generic virtual methods are well known source of bad combinatorics expansion in AOT scenarios. We should be using them only when they are really needed.

@davidfowl
Copy link
Member

Just for my education, when are they typically really needed vs nice to have?

@jkotas
Copy link
Member

jkotas commented Apr 11, 2021

When there are no alternatives or when the alternatives are much worse (e.g. require everybody write a ton of boiler plate code for common scenario).

In this particular case, I would think that the non-generic JsonTypeInfo? GetTypeInfo(Type type) should be able to handle everything that matters pretty well.

@layomia
Copy link
Contributor Author

layomia commented Apr 12, 2021

@davidfowl okay to leave this method out for now till I get some usage samples/scenarios from you? Target would be preview 5/6.

@layomia
Copy link
Contributor Author

layomia commented Jul 28, 2021

Adding a proposal to update the APIs to configure and use JSON source generation. This covers functionality added since the last review. Most of this functionality is prototyped/checked into .NET 6 and would only need minor clean-up, depending on how the review goes. cc @steveharter @eiriktsarpalis @eerhardt @stephentoub @ericstj


APIs for source-generated code to use

namespace System.Text.Json.Serialization.Metadata
{
    // Provides serialization info about a type
    public partial class JsonTypeInfo
    {
        internal JsonTypeInfo() { }
    }

    // Provides serialization info about a type
    public abstract partial class JsonTypeInfo<T> : JsonTypeInfo
    {
        internal JsonTypeInfo() { }
        public Action<Utf8JsonWriter, T>? Serialize { get { throw null; } }
    }

    // Provide serialization info about a property or field of a POCO
    public abstract partial class JsonPropertyInfo
    {
        internal JsonPropertyInfo() { }
    }

+   The list of parameters in `CreatePropertyInfo<T>` below is too long, so we move the data to a struct. This should help with forward-compat since we can add members representing new serializer features.
+   public readonly struct JsonPropertyInfoValues<T>
+   {
+       public bool IsProperty { get; init; }
+       public bool IsPublic { get; init; }
+       public bool IsVirtual { get; init; }
+       public Type DeclaringType { get; init; }
+       public JsonTypeInfo PropertyTypeInfo { get; init; }
+       public JsonConverter<T>? Converter { get; init; }
+       public Func<object, T>? Getter { get; init; }
+       public Action<object, T>? Setter { get; init; }
+       public JsonIgnoreCondition? IgnoreCondition { get; init; }
+       public bool IsExtensionDataProperty { get; init; }
+       public JsonNumberHandling? NumberHandling { get; init; }
+       // The property's statically-declared name.
+       public string PropertyName { get; init; }
+       // The name to use when processing the property, specified by [JsonPropertyName(string)].
+       public string? JsonPropertyName { get; init; }
+   }

+   // For the same reason as `JsonPropertyInfoValues<T>` above, move object info to a struct.
+   // Configures information about an object with members that is deserialized using a parameterless ctor.
+   public readonly struct JsonObjectInfoValues<T>
+   {
+       // A method to create an instance of the type, using a parameterless ctor
+       public Func<T>? CreateObjectFunc { get; init; }

+       // A method to create an instance of the type, using a parameterized ctor
+       public Func<object[], T>? CreateObjectFunc { get; init; }

+       // Provides information about the type's properties and fields.
+       public Func<JsonSerializerContext, JsonPropertyInfo[]>? PropInitFunc { get; init; }

+       // Provides information about the type's ctor params.
+       public Func<JsonParameterInfo[]>? CtorParamInitFunc { get; init; }

+       // The number-handling setting for the type's properties and fields.
+       public JsonNumberHandling NumberHandling { get; init; }

+       // An optimized method to serialize instances of the type, given pre-defined serialization settings.
+       public Action<Utf8JsonWriter, T>? SerializeFunc { get; init; }
+   }

+   // Configures information about a collection
+   public readonly struct JsonCollectionInfo<T>
+   {
+       // A method to create an instance of the collection  
+       public Func<T>? CreateObjectFunc { get; init; }

+       // Serialization metadata about the collection key type, if a dictionary.
+       public JsonTypeInfo? KeyInfo { get; init; }

+       // Serialization metadata about the collection element/value type.
+       public JsonTypeInfo ElementInfo { get; init; }

+       // The number-handling setting for the collection's elements.
+       public JsonNumberHandling NumberHandling { get; init; }

+       // An optimized method to serialize instances of the collection, given pre-defined serialization settings.
+       public Action<Utf8JsonWriter, T>? SerializeFunc { get; init; }
+   }

+   // Provides serialization info about a constructor parameter
+   public readonly struct JsonParameterInfo
+   {
+       public object? DefaultValue { readonly get { throw null; } init { } }
+       public bool HasDefaultValue { readonly get { throw null; } init { } }
+       public string Name { readonly get { throw null; } init { } }
+       public System.Type ParameterType { readonly get { throw null; } init { } }
+       public int Position { readonly get { throw null; } init { } }
+   }

    // Object type and property info creators
    public static partial class JsonMetadataServices
    {
        // Creator for an object type
-       public static JsonTypeInfo<T> CreateObjectInfo<T>(JsonSerializerOptions options, Func<T>? createObjectFunc, Func<JsonSerializerContext, JsonPropertyInfo[]>? propInitFunc, JsonNumberHandling numberHandling, Action<Utf8JsonWriter, T>? serializeFunc) where T : notnull { throw null; }
+       public static JsonTypeInfo<T> CreateObjectInfo<T>(JsonSerializerOptions options, JsonObjectInfoValues<T> objectInfo) where T : notnull { throw null; }
        
        // Creator for an object property
-       public static JsonPropertyInfo CreatePropertyInfo<T>(JsonSerializerOptions options, bool isProperty, bool isPublic, bool isVirtual, Type declaringType, JsonTypeInfo propertyTypeInfo, JsonConverter<T>? converter, Func<object, T>? getter, Action<object, T>? setter, JsonIgnoreCondition? ignoreCondition, bool hasJsonInclude, JsonNumberHandling? numberHandling, string propertyName, string? jsonPropertyName) { throw null; }
+       public static JsonPropertyInfo CreatePropertyInfo<T>(JsonSerializerOptions options, JsonPropertyInfoValues<T> propertyInfo) { throw null; }
    }

    // Collection type info creators
    public static partial class JsonMetadataServices
    {
-       public static JsonTypeInfo<TElement[]> CreateArrayInfo<TElement>(JsonSerializerOptions options, JsonTypeInfo elementInfo, JsonNumberHandling numberHandling, Action<Utf8JsonWriter, TElement[]>? serializeFunc) { throw null; }
+       public static JsonTypeInfo<TElement[]> CreateArrayInfo<TElement>(JsonSerializerOptions options, JsonCollectionInfo<TElement[]> info) { throw null; }

-       public static JsonTypeInfo<TCollection> CreateDictionaryInfo<TCollection, TKey, TValue>(JsonSerializerOptions options, Func<TCollection> createObjectFunc, JsonTypeInfo keyInfo, JsonTypeInfo valueInfo, JsonNumberHandling numberHandling, Action<Utf8JsonWriter, TCollection>? serializeFunc) where TCollection : Dictionary<TKey, TValue> where TKey : notnull { throw null; }
+       public static JsonTypeInfo<TCollection> CreateDictionaryInfo<TCollection, TKey, TValue>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : Dictionary<TKey, TValue> where TKey : notnull { throw null; }

-       public static JsonTypeInfo<TCollection> CreateListInfo<TCollection, TElement>(JsonSerializerOptions options, Func<TCollection>? createObjectFunc, JsonTypeInfo elementInfo, JsonNumberHandling numberHandling, Action<Utf8JsonWriter, TCollection>? serializeFunc) where TCollection : List<TElement> { throw null; }
+       public static JsonTypeInfo<TCollection> CreateListInfo<TCollection, TElement>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : List<TElement> { throw null; }

+       public static JsonTypeInfo<TCollection> CreateConcurrentQueueInfo<TCollection, TElement>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : ConcurrentQueue<TElement> { throw null; }
+       public static JsonTypeInfo<TCollection> CreateConcurrentStackInfo<TCollection, TElement>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : ConcurrentStack<TElement> { throw null; }
+       public static JsonTypeInfo<TCollection> CreateICollectionInfo<TCollection, TElement>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : ICollection<TElement> { throw null; }
+       public static JsonTypeInfo<TCollection> CreateIDictionaryInfo<TCollection>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : IDictionary { throw null; }
+       public static JsonTypeInfo<TCollection> CreateIDictionaryInfo<TCollection, TKey, TValue>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : IDictionary<TKey, TValue> where TKey : notnull { throw null; }
+       public static JsonTypeInfo<TCollection> CreateIEnumerableInfo<TCollection>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : IEnumerable { throw null; }
+       public static JsonTypeInfo<TCollection> CreateIEnumerableInfo<TCollection, TElement>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : IEnumerable<TElement> { throw null; }
+       public static JsonTypeInfo<TCollection> CreateIListInfo<TCollection>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : IList { throw null; }
+       public static JsonTypeInfo<TCollection> CreateIListInfo<TCollection, TElement>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : IList<TElement> { throw null; }
+       public static JsonTypeInfo<TCollection> CreateImmutableDictionaryInfo<TCollection, TKey, TValue>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info, Func<IEnumerable<KeyValuePair<TKey, TValue>>, TCollection> createRangeFunc) where TCollection : IReadOnlyDictionary<TKey, TValue> where TKey : notnull { throw null; }
+       public static JsonTypeInfo<TCollection> CreateImmutableEnumerableInfo<TCollection, TElement>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info, Func<IEnumerable<TElement>, TCollection> createRangeFunc) where TCollection : IEnumerable<TElement> { throw null; }
+       public static JsonTypeInfo<TCollection> CreateIReadOnlyDictionaryInfo<TCollection, TKey, TValue>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : IReadOnlyDictionary<TKey, TValue> where TKey : notnull { throw null; }
+       public static JsonTypeInfo<TCollection> CreateISetInfo<TCollection, TElement>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : ISet<TElement> { throw null; }
+       public static JsonTypeInfo<TCollection> CreateQueueInfo<TCollection, TElement>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : Queue<TElement> { throw null; }
+       public static JsonTypeInfo<TCollection> CreateStackInfo<TCollection, TElement>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : Stack<TElement> { throw null; }
+       public static JsonTypeInfo<TCollection> CreateStackOrQueueInfo<TCollection>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info, Action<TCollection, object?> addFunc) where TCollection : IEnumerable { throw null; }
    }
}

APIs to configure source generator

namespace System.Text.Json.Serialization
{
    public enum JsonKnownNamingPolicy
    {
        Unspecified = 0,
        CamelCase = 1,
    }

    public abstract partial class JsonSerializerContext
    {
-       protected JsonSerializerContext(JsonSerializerOptions? instanceOptions, JsonSerializerOptions? defaultOptions) { }
+       protected JsonSerializerContext(JsonSerializerOptions? options) { }
        public JsonSerializerOptions Options { get { throw null; } }
        public abstract JsonTypeInfo? GetTypeInfo(Type type);
+       // The set of options that are compatible with generated serialization and (future) deserialization logic for types in the context.
+       protected abstract JsonSerializerOptions? DesignTimeOptions { get; }
    }

    [AttributeUsage(AttributeTargets.Class, AllowMultiple=false)]
    public partial sealed class JsonSourceGenerationOptionsAttribute : JsonAttribute
    {
        public JsonSourceGenerationOptionsAttribute() { }
        public JsonIgnoreCondition DefaultIgnoreCondition { get { throw null; } set { } }
        public bool IgnoreReadOnlyFields { get { throw null; } set { } }
        public bool IgnoreReadOnlyProperties { get { throw null; } set { } }
-       // Whether the generated source code should ignore converters added at runtime.
-       public bool IgnoreRuntimeCustomConverters { get { throw null; } set { } }
        public bool IncludeFields { get { throw null; } set { } }
        public JsonKnownNamingPolicy PropertyNamingPolicy { get { throw null; } set { } }
        public bool WriteIndented { get { throw null; } set { } }
        public JsonSourceGenerationMode GenerationMode { get { throw null; } set { } }
    }

    [Flags]
    public enum JsonSourceGenerationMode
    {
        Default = 0,
        Metadata = 1,
        Serialization = 2,
        // Future
        // Deserialization = 4
    }
}

@layomia layomia added api-ready-for-review API is ready for review, it is NOT ready for implementation blocking Marks issues that we want to fast track in order to unblock other important work and removed api-approved API was approved in API review, it can be implemented labels Jul 28, 2021
@terrajobst
Copy link
Member

terrajobst commented Aug 3, 2021

Video

  • JsonTypeInfo<T>
    • The Serialize property can be null if we can't support the fast-path serialization, but that seems subtle. We may want to use a different name or throw an exception with more details.
  • JsonObjectInfoValues<T>
    • We should replace the Func suffix with Handler, Callback, or Factory. Some names, such as Getter or Setter are fine too.
    • We should expand name such as Ctor, Prop, Param, Init (in fact, do we need that in the name at all?)
  • JsonSerializerContext
    • DesignTimeOptions should be renamed to something like SourceGeneratedOptions or GeneratedSerializerOptions
  • JsonMetadataServices
    • CreateStackOrQueueInfo should be split into two methods so that it matches the generic versions
  • We should decide whether we should use classes or structs; there are performance trade offs for either choice (e.g. should it be passed by ref)?
namespace System.Text.Json.Serialization.Metadata
{
    // Provides serialization info about a type
    public partial class JsonTypeInfo
    {
        internal JsonTypeInfo();
    }

    // Provides serialization info about a type
    public abstract partial class JsonTypeInfo<T> : JsonTypeInfo
    {
        internal JsonTypeInfo();
        public Action<Utf8JsonWriter, T>? Serialize { get; }
    }

    // Provide serialization info about a property or field of a POCO
    public abstract partial class JsonPropertyInfo
    {
        internal JsonPropertyInfo();
    }

+   // The list of parameters in `CreatePropertyInfo<T>` below is too long, so we move the data to a struct. This should help with forward-compat since we can add members representing new serializer features.
+   public readonly struct JsonPropertyInfoValues<T>
+   {
+       public bool IsProperty { get; init; }
+       public bool IsPublic { get; init; }
+       public bool IsVirtual { get; init; }
+       public Type DeclaringType { get; init; }
+       public JsonTypeInfo PropertyTypeInfo { get; init; }
+       public JsonConverter<T>? Converter { get; init; }
+       public Func<object, T>? Getter { get; init; }
+       public Action<object, T>? Setter { get; init; }
+       public JsonIgnoreCondition? IgnoreCondition { get; init; }
+       public bool IsExtensionDataProperty { get; init; }
+       public JsonNumberHandling? NumberHandling { get; init; }
+       // The property's statically-declared name.
+       public string PropertyName { get; init; }
+       // The name to use when processing the property, specified by [JsonPropertyName(string)].
+       public string? JsonPropertyName { get; init; }
+   }

+   // For the same reason as `JsonPropertyInfoValues<T>` above, move object info to a struct.
+   // Configures information about an object with members that is deserialized using a parameterless ctor.
+   public readonly struct JsonObjectInfoValues<T>
+   {
+       // A method to create an instance of the type, using a parameterless ctor
+       public Func<T>? CreateObjectFunc { get; init; }

+       // A method to create an instance of the type, using a parameterized ctor
+       public Func<object[], T>? CreateObjectFunc { get; init; }

+       // Provides information about the type's properties and fields.
+       public Func<JsonSerializerContext, JsonPropertyInfo[]>? PropInitFunc { get; init; }

+       // Provides information about the type's ctor params.
+       public Func<JsonParameterInfo[]>? CtorParamInitFunc { get; init; }

+       // The number-handling setting for the type's properties and fields.
+       public JsonNumberHandling NumberHandling { get; init; }

+       // An optimized method to serialize instances of the type, given pre-defined serialization settings.
+       public Action<Utf8JsonWriter, T>? SerializeFunc { get; init; }
+   }

+   // Configures information about a collection
+   public readonly struct JsonCollectionInfo<T>
+   {
+       // A method to create an instance of the collection  
+       public Func<T>? CreateObjectFunc { get; init; }

+       // Serialization metadata about the collection key type, if a dictionary.
+       public JsonTypeInfo? KeyInfo { get; init; }

+       // Serialization metadata about the collection element/value type.
+       public JsonTypeInfo ElementInfo { get; init; }

+       // The number-handling setting for the collection's elements.
+       public JsonNumberHandling NumberHandling { get; init; }

+       // An optimized method to serialize instances of the collection, given pre-defined serialization settings.
+       public Action<Utf8JsonWriter, T>? SerializeFunc { get; init; }
+   }

+   // Provides serialization info about a constructor parameter
+   public readonly struct JsonParameterInfo
+   {
+       public object? DefaultValue { readonly get; init; }
+       public bool HasDefaultValue { readonly get; init; }
+       public string Name { readonly get; init; }
+       public Type ParameterType { readonly get; init; }
+       public int Position { readonly get; init; }
+   }

    // Object type and property info creators
    public static partial class JsonMetadataServices
    {
        // Creator for an object type
-       public static JsonTypeInfo<T> CreateObjectInfo<T>(JsonSerializerOptions options, Func<T>? createObjectFunc, Func<JsonSerializerContext, JsonPropertyInfo[]>? propInitFunc, JsonNumberHandling numberHandling, Action<Utf8JsonWriter, T>? serializeFunc) where T : notnull;
+       public static JsonTypeInfo<T> CreateObjectInfo<T>(JsonSerializerOptions options, JsonObjectInfoValues<T> objectInfo) where T : notnull;
        
        // Creator for an object property
-       public static JsonPropertyInfo CreatePropertyInfo<T>(JsonSerializerOptions options, bool isProperty, bool isPublic, bool isVirtual, Type declaringType, JsonTypeInfo propertyTypeInfo, JsonConverter<T>? converter, Func<object, T>? getter, Action<object, T>? setter, JsonIgnoreCondition? ignoreCondition, bool hasJsonInclude, JsonNumberHandling? numberHandling, string propertyName, string? jsonPropertyName);
+       public static JsonPropertyInfo CreatePropertyInfo<T>(JsonSerializerOptions options, JsonPropertyInfoValues<T> propertyInfo);
    }

    // Collection type info creators
    public static partial class JsonMetadataServices
    {
-       public static JsonTypeInfo<TElement[]> CreateArrayInfo<TElement>(JsonSerializerOptions options, JsonTypeInfo elementInfo, JsonNumberHandling numberHandling, Action<Utf8JsonWriter, TElement[]>? serializeFunc);
+       public static JsonTypeInfo<TElement[]> CreateArrayInfo<TElement>(JsonSerializerOptions options, JsonCollectionInfo<TElement[]> info);

-       public static JsonTypeInfo<TCollection> CreateDictionaryInfo<TCollection, TKey, TValue>(JsonSerializerOptions options, Func<TCollection> createObjectFunc, JsonTypeInfo keyInfo, JsonTypeInfo valueInfo, JsonNumberHandling numberHandling, Action<Utf8JsonWriter, TCollection>? serializeFunc) where TCollection : Dictionary<TKey, TValue> where TKey : notnull;
+       public static JsonTypeInfo<TCollection> CreateDictionaryInfo<TCollection, TKey, TValue>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : Dictionary<TKey, TValue> where TKey : notnull;

-       public static JsonTypeInfo<TCollection> CreateListInfo<TCollection, TElement>(JsonSerializerOptions options, Func<TCollection>? createObjectFunc, JsonTypeInfo elementInfo, JsonNumberHandling numberHandling, Action<Utf8JsonWriter, TCollection>? serializeFunc) where TCollection : List<TElement>;
+       public static JsonTypeInfo<TCollection> CreateListInfo<TCollection, TElement>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : List<TElement>;

+       public static JsonTypeInfo<TCollection> CreateConcurrentQueueInfo<TCollection, TElement>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : ConcurrentQueue<TElement>;
+       public static JsonTypeInfo<TCollection> CreateConcurrentStackInfo<TCollection, TElement>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : ConcurrentStack<TElement>;
+       public static JsonTypeInfo<TCollection> CreateICollectionInfo<TCollection, TElement>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : ICollection<TElement>;
+       public static JsonTypeInfo<TCollection> CreateIDictionaryInfo<TCollection>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : IDictionary;
+       public static JsonTypeInfo<TCollection> CreateIDictionaryInfo<TCollection, TKey, TValue>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : IDictionary<TKey, TValue> where TKey : notnull;
+       public static JsonTypeInfo<TCollection> CreateIEnumerableInfo<TCollection>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : IEnumerable;
+       public static JsonTypeInfo<TCollection> CreateIEnumerableInfo<TCollection, TElement>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : IEnumerable<TElement>;
+       public static JsonTypeInfo<TCollection> CreateIListInfo<TCollection>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : IList;
+       public static JsonTypeInfo<TCollection> CreateIListInfo<TCollection, TElement>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : IList<TElement>;
+       public static JsonTypeInfo<TCollection> CreateImmutableDictionaryInfo<TCollection, TKey, TValue>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info, Func<IEnumerable<KeyValuePair<TKey, TValue>>, TCollection> createRangeFunc) where TCollection : IReadOnlyDictionary<TKey, TValue> where TKey : notnull;
+       public static JsonTypeInfo<TCollection> CreateImmutableEnumerableInfo<TCollection, TElement>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info, Func<IEnumerable<TElement>, TCollection> createRangeFunc) where TCollection : IEnumerable<TElement>;
+       public static JsonTypeInfo<TCollection> CreateIReadOnlyDictionaryInfo<TCollection, TKey, TValue>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : IReadOnlyDictionary<TKey, TValue> where TKey : notnull;
+       public static JsonTypeInfo<TCollection> CreateISetInfo<TCollection, TElement>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : ISet<TElement>;
+       public static JsonTypeInfo<TCollection> CreateQueueInfo<TCollection, TElement>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : Queue<TElement>;
+       public static JsonTypeInfo<TCollection> CreateStackInfo<TCollection, TElement>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info) where TCollection : Stack<TElement>;
+       public static JsonTypeInfo<TCollection> CreateStackOrQueueInfo<TCollection>(JsonSerializerOptions options, JsonCollectionInfo<TCollection> info, Action<TCollection, object?> addFunc) where TCollection : IEnumerable;
    }
}
namespace System.Text.Json.Serialization
{
    public enum JsonKnownNamingPolicy
    {
        Unspecified = 0,
        CamelCase = 1,
    }

    public abstract partial class JsonSerializerContext
    {
-       protected JsonSerializerContext(JsonSerializerOptions? instanceOptions, JsonSerializerOptions? defaultOptions);
+       protected JsonSerializerContext(JsonSerializerOptions? options);
        public JsonSerializerOptions Options { get; }
        public abstract JsonTypeInfo? GetTypeInfo(Type type);
+       // The set of options that are compatible with generated serialization and (future) deserialization logic for types in the context.
+       protected abstract JsonSerializerOptions? DesignTimeOptions { get; }
    }

    [AttributeUsage(AttributeTargets.Class, AllowMultiple=false)]
    public partial sealed class JsonSourceGenerationOptionsAttribute : JsonAttribute
    {
        public JsonSourceGenerationOptionsAttribute();
        public JsonIgnoreCondition DefaultIgnoreCondition { get; set; }
        public bool IgnoreReadOnlyFields { get; set; }
        public bool IgnoreReadOnlyProperties { get; set; }
-       // Whether the generated source code should ignore converters added at runtime.
-       public bool IgnoreRuntimeCustomConverters { get; set; }
        public bool IncludeFields { get; set; }
        public JsonKnownNamingPolicy PropertyNamingPolicy { get; set; }
        public bool WriteIndented { get; set; }
        public JsonSourceGenerationMode GenerationMode { get; set; }
    }

    [Flags]
    public enum JsonSourceGenerationMode
    {
        Default = 0,
        Metadata = 1,
        Serialization = 2,
        // Future
        // Deserialization = 4
    }
}

@terrajobst terrajobst added api-needs-work API needs work before it is approved, it is NOT ready for implementation and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Aug 3, 2021
@layomia
Copy link
Contributor Author

layomia commented Aug 12, 2021

We've implemented the required functionality of this feature for 6.0, barring bug fixes and goodness. There'll likely be one more review of the APIs before we ship but I'll move this issue to 7.0 to make 6.0 tracking cleaner.

@layomia layomia modified the milestones: 6.0.0, 7.0.0 Aug 12, 2021
@layomia
Copy link
Contributor Author

layomia commented Sep 8, 2021

We need a final pass at the source generator APIs:

namespace System.Text.Json.Serialization.Metadata
{
    // Provides serialization info about a type
    public abstract partial class JsonTypeInfo<T> : JsonTypeInfo
    {
        internal JsonTypeInfo();
-       public Action<Utf8JsonWriter, T>? Serialize { get; }
+       // Rename to SerializeHandler
+       public Action<Utf8JsonWriter, T>? SerializeHandler { get; }
    }

    public static partial class JsonMetadataServices
    {
+       // Converter that handles JsonArray
+       public static System.Text.Json.Serialization.JsonConverter<JsonArray> JsonArrayConverter { get { throw null; } }

+       // Converter that handles JsonNode
+       public static System.Text.Json.Serialization.JsonConverter<JsonNode> JsonNodeConverter { get { throw null; } }

+       // Converter that handles JsonObject
+       public static System.Text.Json.Serialization.JsonConverter<JsonObject> JsonObjectConverter { get { throw null; } }

+       // Converter that handles JsonValue
+       public static System.Text.Json.Serialization.JsonConverter<JsonValue> JsonValueConverter { get { throw null; } }

+       // Converter that handles unsupported types.
+       public static System.Text.Json.Serialization.JsonConverter<T> GetUnsupportedTypeConverter<T>() { throw null; }
    }
}

@layomia layomia added api-ready-for-review API is ready for review, it is NOT ready for implementation and removed api-needs-work API needs work before it is approved, it is NOT ready for implementation labels Sep 13, 2021
@layomia
Copy link
Contributor Author

layomia commented Sep 13, 2021

@am11 do you have scenarios where you have a typed value i.e. some T value, but not a corresponding JsonTypeInfo<T> instance?

@am11
Copy link
Member

am11 commented Sep 13, 2021

It just felt that one of the Serialize overload is missed out in .NET 6. It might as well be by design, I am not sure..

using System;
using System.Text.Json;
using System.Text.Json.Serialization;

// instead of:
string json = JsonSerializer.Serialize(new B(), typeof(B), new MySerializerContext());

// i was expecting this to work:
// string json =  JsonSerializer.Serialize(new B(), new MySerializerContext());
//
// but it doesn't, since there is no generic overload accepting TValue and context arg, i.e.:
//    Serialize<TValue>(TValue, JsonSerializerContext)

Console.WriteLine(json);

[JsonSerializable(typeof(B))]
public partial class MySerializerContext : JsonSerializerContext { }

public class B
{
    public int Foo { get; } = 42;
}

@layomia
Copy link
Contributor Author

layomia commented Sep 13, 2021

Yes we can consider this by design until a scenario necessitates new overloads.

@terrajobst
Copy link
Member

terrajobst commented Sep 14, 2021

Video

  • Looks good as proposed
namespace System.Text.Json.Serialization.Metadata
{
    // Provides serialization info about a type
    public abstract partial class JsonTypeInfo<T> : JsonTypeInfo
    {
        internal JsonTypeInfo();
-       public Action<Utf8JsonWriter, T>? Serialize { get; }
+       // Rename to SerializeHandler
+       public Action<Utf8JsonWriter, T>? SerializeHandler { get; }
    }

    public static partial class JsonMetadataServices
    {
+       // Converter that handles JsonArray
+       public static JsonConverter<JsonArray> JsonArrayConverter { get; }

+       // Converter that handles JsonNode
+       public static JsonConverter<JsonNode> JsonNodeConverter { get; }

+       // Converter that handles JsonObject
+       public static JsonConverter<JsonObject> JsonObjectConverter { get; }

+       // Converter that handles JsonValue
+       public static JsonConverter<JsonValue> JsonValueConverter { get; }

+       // Converter that handles unsupported types.
+       public static JsonConverter<T> GetUnsupportedTypeConverter<T>();
    }
}

@terrajobst terrajobst added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Sep 14, 2021
@layomia layomia added the source-generator Indicates an issue with a source generator feature label Sep 22, 2021
@eiriktsarpalis
Copy link
Member

@layomia should we close this and open a fresh issue for 7.0.0?

@AnQueth
Copy link

AnQueth commented Nov 19, 2021

i do not see this being used for web api output. it will run the code for incomming objects, but outgoing objects show the context as null.

@eiriktsarpalis
Copy link
Member

@AnQueth I'm not sure I understand what you're saying. I would recommend creating a new issue with relevant details (and reproduction) so that we can take a closer look.

@eiriktsarpalis
Copy link
Member

Closing, since this issue defines work scoped to .NET 6.

@ghost ghost locked as resolved and limited conversation to collaborators Jun 12, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-System.Text.Json blocking Marks issues that we want to fast track in order to unblock other important work source-generator Indicates an issue with a source generator feature Team:Libraries tenet-performance Performance related issue
Projects
None yet
Development

No branches or pull requests

10 participants