-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Provide fast-path serialization logic in JSON source generator #51945
Comments
Tagging subscribers to this area: @eiriktsarpalis, @layomia Issue DetailsThe JSON source generator (#45448) takes a metadata-based approach where reflection-based type metadata gathering is moved from run-time to compile time. This primarily helps improve start-up time, privates bytes usage, and app size. For simple POCOs and simple [assembly: JsonSerializable(typeof(JsonMessage))]
public struct JsonMessage
{
public string message { get; set; }
}
JsonTypeInfo<JsonMessage> messageInfo = JsonContext.Default.JsonMessage;
var ms = new MemoryStream();
using (var writer = new Utf8JsonWriter(ms))
{
messageInfo.SerializeObject!(writer, message);
}
// Recall metadata can also be passed to serializer and be used when nested in other object graphs:
JsonSerializer.SerializeToUtf8Bytes(message, messageInfo);
|
"Simple options": Run-time options (JsonSerializerOptions)
Design-time options (Attributes)
|
Are the fallbacks per type or global?, i.e. if I add a converter is fast-path disabled for the type in the converter or for all types? |
@Tornhoof the fallbacks would be per type. So if a custom converter is used for the type or one of its members, a fast-path action will not be generated. Other types that fit the characteristics would have fast-path logic generated. |
Other source generators are often adopting a pattern of partial methods that get filled in by the source generator. In this case, it would look like this:
Would this be a better alternative for the lean fast serializers? What are the pros and cons of this simple static method that just gets the job done vs. what is proposed above? |
@jkotas We could absolutely adopt a pattern like this. It is succinct and efficient. The downside is that it cannot be used within the Applications/services that use non-trivial features of For improved usability for really simple scenarios, I think it would be good to add such a pattern alongside the one initially added above. The generator's implementation could be following, depending on configuration: Assuming the default generation mode ( [GeneratedJsonSerializer(JsonSerializerDefaults.General)]
partial static void Serialize(Utf8JsonWriter writer, MyType value)
{
writer.WriteStartObject();
...
writer.WriteEndObject();
} Assuming In this case we could the code generated with [assembly: JsonSourceGenerationMode(JsonSourceGenerationMode.Metadata)]
partial static void Serialize(Utf8JsonWriter writer, MyType value)
{
JsonContext context = JsonContext.GetOrAdd(JsonSerializerDefaults.General); // Lazy create and cache a compatible context
context.MyType.Serialize(writer, value);
} |
Do we know exactly what is the rich feature set needed by Bing, etc., that would not be available via the simple fast mode? For example, do they really need Also, should we have a similar simple fast deserialization path?
|
For Bing, the major features are async (de)serialization & custom converters. To represent other use cases, I highlighted all the unavailable serialization features in #51945 (comment).
Bing would not be a likely user of this feature. It was included for scenarios where multiple options instances with different values are used in a project. We can pull it out if it's not a first-class consideration. If we do pull it out, we would not need the
Yes, this issue is focused for the preview 5 goal to add fast-path serialization. Deserialization is planned for p6. Here's the expected matrix of support: Run-time options
Design-time options
|
@jkotas the idea of using static abstract methods on interfaces helps with the issue of how generated metadata or serialization logic can be used within interface IJsonSerializable
{
void Write(IJsonSerializeable value, Utf8JsonWriter writer);
static IJsonSerialiable Read(Utf8JsonReader reader);
}
static class JsonSerializer
{
static byte[] SerializeToUtf8Bytes(IJsonSerializable value) { }
static IJsonSerializable Deserialize(ReadOnlySpan<byte> json) { throw null; }
} |
Yes, I think simpler would be better. I have not realized that
Directly including json serialization logic in the type implementation itself via interface is antipatern that should be avoided. For example, it is unfriendly to IL trimming - IL trimming will end up keeping the serialization logic even on types that are never actually serialized by the application since it won't be able to prove that the interface is not used indirectly. Also, I am not sure how the shape you have proposed actually works. Where would |
namespace System.Text.Json.Serialization.Metadata
{
public abstract partial class JsonTypeInfo<T>
{
// Existing:
// internal JsonTypeInfo() { }
public Action<Utf8JsonWriter, T>? Serialize { get; set; }
}
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)]
public class JsonSerializerOptionsAttribute : JsonAttribute
{
public JsonIgnoreCondition DefaultIgnoreCondition { get; set; }
public bool IgnoreReadOnlyFields { get; set; }
public bool IgnoreReadOnlyProperties { get; set; }
public bool IgnoreRuntimeCustomConverters { get; set; }
public bool IncludeFields { get; set; }
public JsonKnownNamingPolicy NamingPolicy { get; set; }
public bool WriteIndented { get; set; }
}
}
namespace System.Text.Json.Serialization
{
// Existing: [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
[AttributeUsage(AttributeTargets.Assembly |
AttributeTargets.Class |
AttributeTargets.Struct |
AttributeTargets.Interface, AllowMultiple = true)]
public partial class JsonSerializableAttribute : Attribute
{
public JsonSourceGenerationMode GenerationMode { get; set; }
}
public enum JsonKnownNamingPolicy
{
Unspecified = 0,
BuiltInCamelCase = 1
}
public enum JsonSourceGenerationMode
{
MetadataAndSerialization = 0,
Metadata = 1,
Serialization = 2
}
} |
In the updated proposal following API review, the generator would by default generate both metadata and serialization code for all types. It can be changed to serialization code only on a per-type basis. This is what the user would provide to kick off generation for serialization logic only (with default options): [JsonSerializable(typeof(JsonMessage), GenerationMode = JsonSourceGenerationMode.Serialization)]
public partial class MyJsonContext : JsonSerializerContext
{
} Then to call the generated fast-path serialization code: using MemoryStream ms = new();
using Utf8JsonWriter writer = new(ms);
MyJsonContext.Default.JsonMessage.Serialize!(writer, new JsonMessage { Message: "Hello" });
Makes sense to avoid a pattern that roots all serialization logic/metadata even when not used. cc @eerhardt / @davidfowl who are interested interface approach.
This should be |
Why do I need the |
It would be [JsonSerializable(typeof(JsonMessage), GenerationMode = JsonSourceGenerationMode.Metadata)]
public partial class MyJsonContext : JsonSerializerContext
{
} User would use the generated code as follows, calling the serializer. There'd still be be some throughput gain (for small POCOs, 10-15%) by passing the type metadata directly (and avoiding a dictionary look up to fetch it like done in existing serializer methods). byte[] json = JsonSerializer.SerializeToUtf8Bytes(messageInstance, MyJsonContext.Default.JsonMessage); |
It feels like unnecessary ceremony to me. I would like to write just: [JsonSerializable(typeof(JsonMessage))]
public partial class MyGeneratedJsonSerializers : JsonSerializerContext
{
}
MyGeneratedJsonSerializers.Serialize(writer, new JsonMessage { Message: "Hello" }); |
As it aligns with the proposal, my understanding is that this feedback is to:
I've noted the feedback for discussion in the next review. The second bullet sounds really good to me. The first one needs a policy discussion on what scenario we want the least ceremony for. In API review, we landed on the mode that works for all serializer features being the default. Definitely open to change when we look at it again. Concretely, we could also add a [JsonSourceGenerationMode(JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(JsonMessage))]
[JsonSerializable(typeof(Foo))]
[JsonSerializable(typeof(Bar))]
public partial class MyGeneratedJsonSerializers : JsonSerializerContext
{
}
MyGeneratedJsonSerializers.Serialize(writer, new JsonMessage { Message: "Hello" });
MyGeneratedJsonSerializers.Serialize(writer, new Foo());
MyGeneratedJsonSerializers.Serialize(writer, new Bar()); The per-type option would override the global one when generating for a type. The property would change to nullable so that we know when nothing was specified: [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class JsonSerializableAttribute : Attribute
{
// Existing:
// public string? TypeInfoPropertyName { get; set; }
// public JsonSerializableAttribute(Type type) { }
// Instructs the generator on what to generate for the type.
public JsonSourceGenerationMode? GenerationMode { get; set; } // Now nullable
} |
An issue is all the overloads that
And as we add new overloads in the future, we would need to add the overloads to the generated class as well. |
Yup, it would be nice to only generate the shapes that user actually wants, e.g. specifying the desired shape using partial method (#51945 (comment)). It would also allow pay-for-play async shapes. |
Closing this issue as done. #55043 tracks fast-path deserialization using the reader. |
The JSON source generator (#45448) takes a metadata-based approach where reflection-based type metadata gathering is moved from run-time to compile time. This primarily helps improve start-up time, privates bytes usage, and app size.
For simple
JsonSerializerOptions
usages, we can also generate serialization logic usingUtf8JsonWriter
directly. which can help improve serialization throughput.API Proposal
Feature behavior
[JsonSerializerOptionsAttribute]
is used.JsonContext.Default
property will generate/use an options populated with the values from the[JsonSerializerOptionsAttribute]
.Scenarios
Given a simple type:
Calling fast-path directly
Using fast-path via JsonSerializer
Some features like reference-loop handling & async (de)serialization are not supported in generated fast-path logic. For those cases, the context or type info should be passed to the serializer directly. The serializer will detect when the fast path can be called or not. For example for reference-handling, the serializer would know that it can call the fast-path logic for primitives and structs.
The text was updated successfully, but these errors were encountered: