diff --git a/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/AnimalJsonSubTypesConverter.cs b/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/AnimalJsonSubTypesConverter.cs index 43452e3..1d806e5 100644 --- a/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/AnimalJsonSubTypesConverter.cs +++ b/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/AnimalJsonSubTypesConverter.cs @@ -1,240 +1,71 @@ #nullable enable using System; -using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; namespace JsonSubTypes.Aot.Generated { /// Generated by JsonSubTypes.Aot. Compiled converter for global::JsonSubTypes.Aot.Generated.TestDomain.Animal. - public sealed class AnimalJsonSubTypesConverter : JsonConverter + public sealed class AnimalJsonSubTypesConverter : JsonSubTypesAotValueConverterBase { - public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Animal); + private const string DiscriminatorPropertyNameValue = "type"; + protected override string DiscriminatorPropertyName => DiscriminatorPropertyNameValue; - public override void Write(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Animal value, JsonSerializerOptions options) + protected override System.Collections.Generic.Dictionary> DiscriminatorWriters { get; } = new System.Collections.Generic.Dictionary> { - if (value is null) - { - writer.WriteNullValue(); - return; - } - Type runtimeType = value.GetType(); - string payload; - if (runtimeType == typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Animal)) - { - if (IsRegistered(runtimeType)) - { - payload = SerializeBasePayload((global::JsonSubTypes.Aot.Generated.TestDomain.Animal)value, options); - } - else - { - WriteBaseObject(writer, (global::JsonSubTypes.Aot.Generated.TestDomain.Animal)value, options); - return; - } - } - else if (IsRegistered(runtimeType)) - { - payload = JsonSerializer.Serialize(value, options.GetTypeInfo(runtimeType)); - } - else if (TryWriteNestedObject(writer, value, runtimeType, options)) - { - return; - } - else if (TryWriteDynamic(writer, value, runtimeType, options)) + [typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Cat)] = static (writer, options) => writer.WriteStringValue("cat"), + [typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Dog)] = static (writer, options) => writer.WriteNumberValue(2) + }; + + protected override Type SelectType(JsonElement root, JsonSerializerOptions options) + { + if (!TryGetValueInJson(root, DiscriminatorPropertyNameValue, options, out JsonElement discriminator)) { - return; + return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Animal); } - else + Type? staticType = ResolveStaticType(discriminator); + if (staticType != null) { - JsonSerializer.Serialize(writer, value, options.GetTypeInfo(runtimeType)); - return; + return staticType; } - using JsonDocument payloadDocument = JsonDocument.Parse(payload); - string discriminatorName = "type"; - if (options.PropertyNamingPolicy != null) + if (TryGetDynamicType(discriminator, out Type? dynamicType)) { - discriminatorName = options.PropertyNamingPolicy.ConvertName(discriminatorName); + return dynamicType!; } - writer.WriteStartObject(); - writer.WritePropertyName(discriminatorName); - WriteDiscriminatorValue(writer, runtimeType, options); - foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject()) + if (CustomTypeNameResolver is not null) { - if (!property.NameEquals(discriminatorName)) + Type? customType = CustomTypeNameResolver(GetDiscriminatorKey(discriminator)); + if (customType != null) { - property.WriteTo(writer); + return customType; } } - writer.WriteEndObject(); + return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Animal); } - public override global::JsonSubTypes.Aot.Generated.TestDomain.Animal? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + private static Type? ResolveStaticType(JsonElement discriminator) { - if (reader.TokenType == JsonTokenType.Null) - { - return null; - } - if (reader.TokenType != JsonTokenType.StartObject) - { - throw new JsonException("Unrecognized token: " + reader.TokenType); - } - using JsonDocument document = JsonDocument.ParseValue(ref reader); - JsonElement root = document.RootElement; - Type target = SelectType(root, options); - if (target == typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Animal)) + switch (discriminator.ValueKind) { - return DeserializeBase(root, options); - } - return (global::JsonSubTypes.Aot.Generated.TestDomain.Animal?)JsonSerializer.Deserialize(root.GetRawText(), options.GetTypeInfo(target)); - } - - private Type SelectType(JsonElement root, JsonSerializerOptions options) - { - if (TryGetValueInJson(root, "type", options, out JsonElement discriminator)) - { - - if (discriminator.ValueKind == JsonValueKind.String) - { - switch (discriminator.GetString()) - { + case JsonValueKind.String: + switch (discriminator.GetString()) + { case "cat": return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Cat); - } - } - if (discriminator.ValueKind == JsonValueKind.Number) - { - switch (discriminator.GetRawText()) - { + } + break; + case JsonValueKind.Number: + switch (discriminator.GetRawText()) + { case "2": return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Dog); - } - } - if (TryGetDynamicType(discriminator, out Type? dynamicType)) - { - return dynamicType!; - } - if (CustomTypeNameResolver is not null) - { - Type? customType = CustomTypeNameResolver(GetDiscriminatorKey(discriminator)); - if (customType != null) - { - return customType; + } + break; } - } - return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Animal); - } - return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Animal); + return null; } - private static readonly System.Collections.Generic.Dictionary> DiscriminatorWriters = new System.Collections.Generic.Dictionary> - { - [typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Cat)] = static (writer, options) => writer.WriteStringValue("cat"), - [typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Dog)] = static (writer, options) => writer.WriteNumberValue(2) - }; - - private static bool IsRegistered(Type runtimeType) - { - return DiscriminatorWriters.ContainsKey(runtimeType); - } - - private static void WriteDiscriminatorValue(Utf8JsonWriter writer, Type runtimeType, JsonSerializerOptions options) - { - if (DiscriminatorWriters.TryGetValue(runtimeType, out System.Action? write)) - { - write(writer, options); - return; - } - throw new JsonException("Impossible to serialize type: " + runtimeType.FullName + " because there is no registered mapping for the discriminator property"); - } - - public readonly System.Collections.Concurrent.ConcurrentDictionary DynamicSubtypes = new System.Collections.Concurrent.ConcurrentDictionary(); - private readonly System.Collections.Concurrent.ConcurrentDictionary _dynamicReverse = new System.Collections.Concurrent.ConcurrentDictionary(); - - public void RegisterDynamicSubtype(object discriminator, Type type) - { - DynamicSubtypes[discriminator] = type; - _dynamicReverse[type] = discriminator; // last registration wins, like the builder - } - - /// - /// Custom discriminator-to-type resolution hook, invoked after the static - /// registrations and DynamicSubtypes. Assign it to implement your own - /// name-based lookup (e.g. assembly scanning, a DI registry). The resolved - /// type must be resolvable by the TypeInfoResolver (in the source-gen context - /// for Native AOT). - /// - public Func? CustomTypeNameResolver { get; set; } - - private static object? GetDiscriminatorKey(JsonElement discriminator) - { - switch (discriminator.ValueKind) - { - case JsonValueKind.String: - return discriminator.GetString(); - case JsonValueKind.Number when int.TryParse(discriminator.GetRawText(), out int keyInt): - return keyInt; - default: - return discriminator.GetRawText(); - } - } - - private bool TryGetDynamicType(JsonElement discriminator, out Type? dynamicType) - { - switch (discriminator.ValueKind) - { - case JsonValueKind.String: - return DynamicSubtypes.TryGetValue(discriminator.GetString()!, out dynamicType); - case JsonValueKind.Number: - if (int.TryParse(discriminator.GetRawText(), out int dynamicInt) && DynamicSubtypes.TryGetValue(dynamicInt, out dynamicType)) - { - return true; - } - return DynamicSubtypes.TryGetValue(discriminator.GetRawText(), out dynamicType); - default: - return DynamicSubtypes.TryGetValue(discriminator.GetRawText(), out dynamicType); - } - } - - private static bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Animal value, Type runtimeType, JsonSerializerOptions options) - { - - return false; - } - - private bool TryWriteDynamic(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Animal value, Type runtimeType, JsonSerializerOptions options) - { - if (_dynamicReverse.TryGetValue(runtimeType, out object? dynamicDiscriminator)) + protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Animal value, JsonSerializerOptions options) { writer.WriteStartObject(); - string dynamicDiscriminatorName = "type"; - if (options.PropertyNamingPolicy != null) - { - dynamicDiscriminatorName = options.PropertyNamingPolicy.ConvertName(dynamicDiscriminatorName); - } - writer.WritePropertyName(dynamicDiscriminatorName); - writer.WriteRawValue(JsonSerializer.Serialize(dynamicDiscriminator, options.GetTypeInfo(dynamicDiscriminator.GetType()))); - string payload = JsonSerializer.Serialize(value, options.GetTypeInfo(runtimeType)); - using JsonDocument payloadDocument = JsonDocument.Parse(payload); - foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject()) - { - property.WriteTo(writer); - } - writer.WriteEndObject(); - return true; - } - return false; - } - private static string SerializeBasePayload(global::JsonSubTypes.Aot.Generated.TestDomain.Animal value, JsonSerializerOptions options) - { - using System.IO.MemoryStream stream = new System.IO.MemoryStream(); - using (Utf8JsonWriter writer = new Utf8JsonWriter(stream)) - { - WriteBaseObject(writer, value, options); - } - return System.Text.Encoding.UTF8.GetString(stream.ToArray()); - } - - private static void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Animal value, JsonSerializerOptions options) - { - writer.WriteStartObject(); string nameAge = "Age"; if (options.PropertyNamingPolicy != null && true) { @@ -242,66 +73,20 @@ private static void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes. } writer.WritePropertyName(nameAge); JsonSerializer.Serialize(writer, value.Age, options.GetTypeInfo(typeof(int))); - writer.WriteEndObject(); - } - - private static global::JsonSubTypes.Aot.Generated.TestDomain.Animal DeserializeBase(JsonElement root, JsonSerializerOptions options) - { - global::JsonSubTypes.Aot.Generated.TestDomain.Animal instance = new global::JsonSubTypes.Aot.Generated.TestDomain.Animal(); + writer.WriteEndObject(); + } + protected override global::JsonSubTypes.Aot.Generated.TestDomain.Animal DeserializeBase(JsonElement root, JsonSerializerOptions options) + { + global::JsonSubTypes.Aot.Generated.TestDomain.Animal instance = new global::JsonSubTypes.Aot.Generated.TestDomain.Animal(); if (TryGetProperty(root, "Age", options, out JsonElement AgeValue)) { instance.Age = (int)JsonSerializer.Deserialize(AgeValue.GetRawText(), options.GetTypeInfo(typeof(int)))!; } - return instance; - } - - private static bool TryGetProperty(JsonElement root, string name, JsonSerializerOptions options, out JsonElement value) - { - if (root.TryGetProperty(name, out value)) - { - return true; + return instance; } - string? convertedName = options.PropertyNamingPolicy?.ConvertName(name); - if (convertedName != null && convertedName != name && root.TryGetProperty(convertedName, out value)) + protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Animal value, Type runtimeType, JsonSerializerOptions options) { - return true; + return false; } - if (options.PropertyNameCaseInsensitive) - { - foreach (JsonProperty property in root.EnumerateObject()) - { - if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase)) - { - value = property.Value; - return true; - } - } - } - return false; - } - - private static bool TryGetValueInJson(JsonElement root, string propertyName, JsonSerializerOptions options, out JsonElement value) - { - if (TryGetProperty(root, propertyName, options, out value)) - { - return true; - } - if (propertyName.IndexOf('.') >= 0) - { - string[] segments = propertyName.Split('.'); - JsonElement current = root; - foreach (string segment in segments) - { - if (!TryGetProperty(current, segment, options, out current)) - { - value = default; - return false; - } - } - value = current; - return true; - } - return false; - } } } \ No newline at end of file diff --git a/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/DottedGadgetJsonSubTypesConverter.cs b/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/DottedGadgetJsonSubTypesConverter.cs index 099e419..842aa3a 100644 --- a/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/DottedGadgetJsonSubTypesConverter.cs +++ b/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/DottedGadgetJsonSubTypesConverter.cs @@ -1,233 +1,64 @@ #nullable enable using System; -using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; namespace JsonSubTypes.Aot.Generated { /// Generated by JsonSubTypes.Aot. Compiled converter for global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget. - public sealed class DottedGadgetJsonSubTypesConverter : JsonConverter + public sealed class DottedGadgetJsonSubTypesConverter : JsonSubTypesAotValueConverterBase { - public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget); + private const string DiscriminatorPropertyNameValue = "nested.type"; + protected override string DiscriminatorPropertyName => DiscriminatorPropertyNameValue; - public override void Write(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget value, JsonSerializerOptions options) + protected override System.Collections.Generic.Dictionary> DiscriminatorWriters { get; } = new System.Collections.Generic.Dictionary> { - if (value is null) - { - writer.WriteNullValue(); - return; - } - Type runtimeType = value.GetType(); - string payload; - if (runtimeType == typeof(global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget)) - { - if (IsRegistered(runtimeType)) - { - payload = SerializeBasePayload((global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget)value, options); - } - else - { - WriteBaseObject(writer, (global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget)value, options); - return; - } - } - else if (IsRegistered(runtimeType)) - { - payload = JsonSerializer.Serialize(value, options.GetTypeInfo(runtimeType)); - } - else if (TryWriteNestedObject(writer, value, runtimeType, options)) - { - return; - } - else if (TryWriteDynamic(writer, value, runtimeType, options)) + [typeof(global::JsonSubTypes.Aot.Generated.TestDomain.DottedElectronic)] = static (writer, options) => writer.WriteStringValue("electronic") + }; + + protected override Type SelectType(JsonElement root, JsonSerializerOptions options) + { + if (!TryGetValueInJson(root, DiscriminatorPropertyNameValue, options, out JsonElement discriminator)) { - return; + return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget); } - else + Type? staticType = ResolveStaticType(discriminator); + if (staticType != null) { - JsonSerializer.Serialize(writer, value, options.GetTypeInfo(runtimeType)); - return; + return staticType; } - using JsonDocument payloadDocument = JsonDocument.Parse(payload); - string discriminatorName = "nested.type"; - if (options.PropertyNamingPolicy != null) + if (TryGetDynamicType(discriminator, out Type? dynamicType)) { - discriminatorName = options.PropertyNamingPolicy.ConvertName(discriminatorName); + return dynamicType!; } - writer.WriteStartObject(); - writer.WritePropertyName(discriminatorName); - WriteDiscriminatorValue(writer, runtimeType, options); - foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject()) + if (CustomTypeNameResolver is not null) { - if (!property.NameEquals(discriminatorName)) + Type? customType = CustomTypeNameResolver(GetDiscriminatorKey(discriminator)); + if (customType != null) { - property.WriteTo(writer); + return customType; } } - writer.WriteEndObject(); + return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget); } - public override global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + private static Type? ResolveStaticType(JsonElement discriminator) { - if (reader.TokenType == JsonTokenType.Null) - { - return null; - } - if (reader.TokenType != JsonTokenType.StartObject) - { - throw new JsonException("Unrecognized token: " + reader.TokenType); - } - using JsonDocument document = JsonDocument.ParseValue(ref reader); - JsonElement root = document.RootElement; - Type target = SelectType(root, options); - if (target == typeof(global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget)) + switch (discriminator.ValueKind) { - return DeserializeBase(root, options); - } - return (global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget?)JsonSerializer.Deserialize(root.GetRawText(), options.GetTypeInfo(target)); - } - - private Type SelectType(JsonElement root, JsonSerializerOptions options) - { - if (TryGetValueInJson(root, "nested.type", options, out JsonElement discriminator)) - { - - if (discriminator.ValueKind == JsonValueKind.String) - { - switch (discriminator.GetString()) - { + case JsonValueKind.String: + switch (discriminator.GetString()) + { case "electronic": return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.DottedElectronic); - } - } - - if (TryGetDynamicType(discriminator, out Type? dynamicType)) - { - return dynamicType!; - } - if (CustomTypeNameResolver is not null) - { - Type? customType = CustomTypeNameResolver(GetDiscriminatorKey(discriminator)); - if (customType != null) - { - return customType; + } + break; } + return null; } - return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget); - } - return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget); - } - - private static readonly System.Collections.Generic.Dictionary> DiscriminatorWriters = new System.Collections.Generic.Dictionary> - { - [typeof(global::JsonSubTypes.Aot.Generated.TestDomain.DottedElectronic)] = static (writer, options) => writer.WriteStringValue("electronic") - }; - - private static bool IsRegistered(Type runtimeType) - { - return DiscriminatorWriters.ContainsKey(runtimeType); - } - - private static void WriteDiscriminatorValue(Utf8JsonWriter writer, Type runtimeType, JsonSerializerOptions options) - { - if (DiscriminatorWriters.TryGetValue(runtimeType, out System.Action? write)) - { - write(writer, options); - return; - } - throw new JsonException("Impossible to serialize type: " + runtimeType.FullName + " because there is no registered mapping for the discriminator property"); - } - - public readonly System.Collections.Concurrent.ConcurrentDictionary DynamicSubtypes = new System.Collections.Concurrent.ConcurrentDictionary(); - private readonly System.Collections.Concurrent.ConcurrentDictionary _dynamicReverse = new System.Collections.Concurrent.ConcurrentDictionary(); - - public void RegisterDynamicSubtype(object discriminator, Type type) - { - DynamicSubtypes[discriminator] = type; - _dynamicReverse[type] = discriminator; // last registration wins, like the builder - } - - /// - /// Custom discriminator-to-type resolution hook, invoked after the static - /// registrations and DynamicSubtypes. Assign it to implement your own - /// name-based lookup (e.g. assembly scanning, a DI registry). The resolved - /// type must be resolvable by the TypeInfoResolver (in the source-gen context - /// for Native AOT). - /// - public Func? CustomTypeNameResolver { get; set; } - - private static object? GetDiscriminatorKey(JsonElement discriminator) - { - switch (discriminator.ValueKind) - { - case JsonValueKind.String: - return discriminator.GetString(); - case JsonValueKind.Number when int.TryParse(discriminator.GetRawText(), out int keyInt): - return keyInt; - default: - return discriminator.GetRawText(); - } - } - - private bool TryGetDynamicType(JsonElement discriminator, out Type? dynamicType) - { - switch (discriminator.ValueKind) - { - case JsonValueKind.String: - return DynamicSubtypes.TryGetValue(discriminator.GetString()!, out dynamicType); - case JsonValueKind.Number: - if (int.TryParse(discriminator.GetRawText(), out int dynamicInt) && DynamicSubtypes.TryGetValue(dynamicInt, out dynamicType)) - { - return true; - } - return DynamicSubtypes.TryGetValue(discriminator.GetRawText(), out dynamicType); - default: - return DynamicSubtypes.TryGetValue(discriminator.GetRawText(), out dynamicType); - } - } - - private static bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget value, Type runtimeType, JsonSerializerOptions options) - { - - return false; - } - private bool TryWriteDynamic(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget value, Type runtimeType, JsonSerializerOptions options) - { - if (_dynamicReverse.TryGetValue(runtimeType, out object? dynamicDiscriminator)) + protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget value, JsonSerializerOptions options) { writer.WriteStartObject(); - string dynamicDiscriminatorName = "nested.type"; - if (options.PropertyNamingPolicy != null) - { - dynamicDiscriminatorName = options.PropertyNamingPolicy.ConvertName(dynamicDiscriminatorName); - } - writer.WritePropertyName(dynamicDiscriminatorName); - writer.WriteRawValue(JsonSerializer.Serialize(dynamicDiscriminator, options.GetTypeInfo(dynamicDiscriminator.GetType()))); - string payload = JsonSerializer.Serialize(value, options.GetTypeInfo(runtimeType)); - using JsonDocument payloadDocument = JsonDocument.Parse(payload); - foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject()) - { - property.WriteTo(writer); - } - writer.WriteEndObject(); - return true; - } - return false; - } - private static string SerializeBasePayload(global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget value, JsonSerializerOptions options) - { - using System.IO.MemoryStream stream = new System.IO.MemoryStream(); - using (Utf8JsonWriter writer = new Utf8JsonWriter(stream)) - { - WriteBaseObject(writer, value, options); - } - return System.Text.Encoding.UTF8.GetString(stream.ToArray()); - } - - private static void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget value, JsonSerializerOptions options) - { - writer.WriteStartObject(); string nameAge = "Age"; if (options.PropertyNamingPolicy != null && true) { @@ -235,66 +66,20 @@ private static void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes. } writer.WritePropertyName(nameAge); JsonSerializer.Serialize(writer, value.Age, options.GetTypeInfo(typeof(int))); - writer.WriteEndObject(); - } - - private static global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget DeserializeBase(JsonElement root, JsonSerializerOptions options) - { - global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget instance = new global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget(); + writer.WriteEndObject(); + } + protected override global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget DeserializeBase(JsonElement root, JsonSerializerOptions options) + { + global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget instance = new global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget(); if (TryGetProperty(root, "Age", options, out JsonElement AgeValue)) { instance.Age = (int)JsonSerializer.Deserialize(AgeValue.GetRawText(), options.GetTypeInfo(typeof(int)))!; } - return instance; - } - - private static bool TryGetProperty(JsonElement root, string name, JsonSerializerOptions options, out JsonElement value) - { - if (root.TryGetProperty(name, out value)) - { - return true; - } - string? convertedName = options.PropertyNamingPolicy?.ConvertName(name); - if (convertedName != null && convertedName != name && root.TryGetProperty(convertedName, out value)) - { - return true; - } - if (options.PropertyNameCaseInsensitive) - { - foreach (JsonProperty property in root.EnumerateObject()) - { - if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase)) - { - value = property.Value; - return true; - } - } - } - return false; - } - - private static bool TryGetValueInJson(JsonElement root, string propertyName, JsonSerializerOptions options, out JsonElement value) - { - if (TryGetProperty(root, propertyName, options, out value)) - { - return true; + return instance; } - if (propertyName.IndexOf('.') >= 0) + protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget value, Type runtimeType, JsonSerializerOptions options) { - string[] segments = propertyName.Split('.'); - JsonElement current = root; - foreach (string segment in segments) - { - if (!TryGetProperty(current, segment, options, out current)) - { - value = default; - return false; - } - } - value = current; - return true; + return false; } - return false; - } } } \ No newline at end of file diff --git a/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/GadgetJsonSubTypesConverter.cs b/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/GadgetJsonSubTypesConverter.cs index 494fa45..5de618f 100644 --- a/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/GadgetJsonSubTypesConverter.cs +++ b/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/GadgetJsonSubTypesConverter.cs @@ -1,239 +1,70 @@ #nullable enable using System; -using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; namespace JsonSubTypes.Aot.Generated { /// Generated by JsonSubTypes.Aot. Compiled converter for global::JsonSubTypes.Aot.Generated.TestDomain.Gadget. - public sealed class GadgetJsonSubTypesConverter : JsonConverter + public sealed class GadgetJsonSubTypesConverter : JsonSubTypesAotValueConverterBase { - public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Gadget); + private const string DiscriminatorPropertyNameValue = "kind"; + protected override string DiscriminatorPropertyName => DiscriminatorPropertyNameValue; - public override void Write(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Gadget value, JsonSerializerOptions options) + protected override System.Collections.Generic.Dictionary> DiscriminatorWriters { get; } = new System.Collections.Generic.Dictionary> { - if (value is null) - { - writer.WriteNullValue(); - return; - } - Type runtimeType = value.GetType(); - string payload; - if (runtimeType == typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Gadget)) - { - if (IsRegistered(runtimeType)) - { - payload = SerializeBasePayload((global::JsonSubTypes.Aot.Generated.TestDomain.Gadget)value, options); - } - else - { - WriteBaseObject(writer, (global::JsonSubTypes.Aot.Generated.TestDomain.Gadget)value, options); - return; - } - } - else if (IsRegistered(runtimeType)) - { - payload = JsonSerializer.Serialize(value, options.GetTypeInfo(runtimeType)); - } - else if (TryWriteNestedObject(writer, value, runtimeType, options)) + [typeof(global::JsonSubTypes.Aot.Generated.TestDomain.ElectronicCat)] = static (writer, options) => writer.WriteRawValue(JsonSerializer.Serialize(global::JsonSubTypes.Aot.Generated.TestDomain.GadgetKind.ElectronicCat, options.GetTypeInfo(typeof(global::JsonSubTypes.Aot.Generated.TestDomain.GadgetKind)))) + }; + + protected override Type SelectType(JsonElement root, JsonSerializerOptions options) + { + if (!TryGetValueInJson(root, DiscriminatorPropertyNameValue, options, out JsonElement discriminator)) { - return; + return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Gadget); } - else if (TryWriteDynamic(writer, value, runtimeType, options)) + Type? staticType = ResolveStaticType(discriminator); + if (staticType != null) { - return; + return staticType; } - else + if (TryGetDynamicType(discriminator, out Type? dynamicType)) { - JsonSerializer.Serialize(writer, value, options.GetTypeInfo(runtimeType)); - return; + return dynamicType!; } - using JsonDocument payloadDocument = JsonDocument.Parse(payload); - string discriminatorName = "kind"; - if (options.PropertyNamingPolicy != null) + if (CustomTypeNameResolver is not null) { - discriminatorName = options.PropertyNamingPolicy.ConvertName(discriminatorName); - } - writer.WriteStartObject(); - writer.WritePropertyName(discriminatorName); - WriteDiscriminatorValue(writer, runtimeType, options); - foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject()) - { - if (!property.NameEquals(discriminatorName)) + Type? customType = CustomTypeNameResolver(GetDiscriminatorKey(discriminator)); + if (customType != null) { - property.WriteTo(writer); + return customType; } } - writer.WriteEndObject(); + return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Gadget); } - public override global::JsonSubTypes.Aot.Generated.TestDomain.Gadget? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + private static Type? ResolveStaticType(JsonElement discriminator) { - if (reader.TokenType == JsonTokenType.Null) - { - return null; - } - if (reader.TokenType != JsonTokenType.StartObject) + switch (discriminator.ValueKind) { - throw new JsonException("Unrecognized token: " + reader.TokenType); - } - using JsonDocument document = JsonDocument.ParseValue(ref reader); - JsonElement root = document.RootElement; - Type target = SelectType(root, options); - if (target == typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Gadget)) - { - return DeserializeBase(root, options); - } - return (global::JsonSubTypes.Aot.Generated.TestDomain.Gadget?)JsonSerializer.Deserialize(root.GetRawText(), options.GetTypeInfo(target)); - } - - private Type SelectType(JsonElement root, JsonSerializerOptions options) - { - if (TryGetValueInJson(root, "kind", options, out JsonElement discriminator)) - { - - if (discriminator.ValueKind == JsonValueKind.String) - { - switch (discriminator.GetString()) - { + case JsonValueKind.String: + switch (discriminator.GetString()) + { case "ElectronicCat": return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.ElectronicCat); - } - } - if (discriminator.ValueKind == JsonValueKind.Number) - { - switch (discriminator.GetRawText()) - { + } + break; + case JsonValueKind.Number: + switch (discriminator.GetRawText()) + { case "0": return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.ElectronicCat); - } - } - if (TryGetDynamicType(discriminator, out Type? dynamicType)) - { - return dynamicType!; - } - if (CustomTypeNameResolver is not null) - { - Type? customType = CustomTypeNameResolver(GetDiscriminatorKey(discriminator)); - if (customType != null) - { - return customType; + } + break; } + return null; } - return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Gadget); - } - return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Gadget); - } - - private static readonly System.Collections.Generic.Dictionary> DiscriminatorWriters = new System.Collections.Generic.Dictionary> - { - [typeof(global::JsonSubTypes.Aot.Generated.TestDomain.ElectronicCat)] = static (writer, options) => writer.WriteRawValue(JsonSerializer.Serialize(global::JsonSubTypes.Aot.Generated.TestDomain.GadgetKind.ElectronicCat, options.GetTypeInfo(typeof(global::JsonSubTypes.Aot.Generated.TestDomain.GadgetKind)))) - }; - private static bool IsRegistered(Type runtimeType) - { - return DiscriminatorWriters.ContainsKey(runtimeType); - } - - private static void WriteDiscriminatorValue(Utf8JsonWriter writer, Type runtimeType, JsonSerializerOptions options) - { - if (DiscriminatorWriters.TryGetValue(runtimeType, out System.Action? write)) - { - write(writer, options); - return; - } - throw new JsonException("Impossible to serialize type: " + runtimeType.FullName + " because there is no registered mapping for the discriminator property"); - } - - public readonly System.Collections.Concurrent.ConcurrentDictionary DynamicSubtypes = new System.Collections.Concurrent.ConcurrentDictionary(); - private readonly System.Collections.Concurrent.ConcurrentDictionary _dynamicReverse = new System.Collections.Concurrent.ConcurrentDictionary(); - - public void RegisterDynamicSubtype(object discriminator, Type type) - { - DynamicSubtypes[discriminator] = type; - _dynamicReverse[type] = discriminator; // last registration wins, like the builder - } - - /// - /// Custom discriminator-to-type resolution hook, invoked after the static - /// registrations and DynamicSubtypes. Assign it to implement your own - /// name-based lookup (e.g. assembly scanning, a DI registry). The resolved - /// type must be resolvable by the TypeInfoResolver (in the source-gen context - /// for Native AOT). - /// - public Func? CustomTypeNameResolver { get; set; } - - private static object? GetDiscriminatorKey(JsonElement discriminator) - { - switch (discriminator.ValueKind) - { - case JsonValueKind.String: - return discriminator.GetString(); - case JsonValueKind.Number when int.TryParse(discriminator.GetRawText(), out int keyInt): - return keyInt; - default: - return discriminator.GetRawText(); - } - } - - private bool TryGetDynamicType(JsonElement discriminator, out Type? dynamicType) - { - switch (discriminator.ValueKind) - { - case JsonValueKind.String: - return DynamicSubtypes.TryGetValue(discriminator.GetString()!, out dynamicType); - case JsonValueKind.Number: - if (int.TryParse(discriminator.GetRawText(), out int dynamicInt) && DynamicSubtypes.TryGetValue(dynamicInt, out dynamicType)) - { - return true; - } - return DynamicSubtypes.TryGetValue(discriminator.GetRawText(), out dynamicType); - default: - return DynamicSubtypes.TryGetValue(discriminator.GetRawText(), out dynamicType); - } - } - - private static bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Gadget value, Type runtimeType, JsonSerializerOptions options) - { - - return false; - } - - private bool TryWriteDynamic(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Gadget value, Type runtimeType, JsonSerializerOptions options) - { - if (_dynamicReverse.TryGetValue(runtimeType, out object? dynamicDiscriminator)) + protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Gadget value, JsonSerializerOptions options) { writer.WriteStartObject(); - string dynamicDiscriminatorName = "kind"; - if (options.PropertyNamingPolicy != null) - { - dynamicDiscriminatorName = options.PropertyNamingPolicy.ConvertName(dynamicDiscriminatorName); - } - writer.WritePropertyName(dynamicDiscriminatorName); - writer.WriteRawValue(JsonSerializer.Serialize(dynamicDiscriminator, options.GetTypeInfo(dynamicDiscriminator.GetType()))); - string payload = JsonSerializer.Serialize(value, options.GetTypeInfo(runtimeType)); - using JsonDocument payloadDocument = JsonDocument.Parse(payload); - foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject()) - { - property.WriteTo(writer); - } - writer.WriteEndObject(); - return true; - } - return false; - } - private static string SerializeBasePayload(global::JsonSubTypes.Aot.Generated.TestDomain.Gadget value, JsonSerializerOptions options) - { - using System.IO.MemoryStream stream = new System.IO.MemoryStream(); - using (Utf8JsonWriter writer = new Utf8JsonWriter(stream)) - { - WriteBaseObject(writer, value, options); - } - return System.Text.Encoding.UTF8.GetString(stream.ToArray()); - } - - private static void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Gadget value, JsonSerializerOptions options) - { - writer.WriteStartObject(); string nameAge = "Age"; if (options.PropertyNamingPolicy != null && true) { @@ -241,66 +72,20 @@ private static void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes. } writer.WritePropertyName(nameAge); JsonSerializer.Serialize(writer, value.Age, options.GetTypeInfo(typeof(int))); - writer.WriteEndObject(); - } - - private static global::JsonSubTypes.Aot.Generated.TestDomain.Gadget DeserializeBase(JsonElement root, JsonSerializerOptions options) - { - global::JsonSubTypes.Aot.Generated.TestDomain.Gadget instance = new global::JsonSubTypes.Aot.Generated.TestDomain.Gadget(); + writer.WriteEndObject(); + } + protected override global::JsonSubTypes.Aot.Generated.TestDomain.Gadget DeserializeBase(JsonElement root, JsonSerializerOptions options) + { + global::JsonSubTypes.Aot.Generated.TestDomain.Gadget instance = new global::JsonSubTypes.Aot.Generated.TestDomain.Gadget(); if (TryGetProperty(root, "Age", options, out JsonElement AgeValue)) { instance.Age = (int)JsonSerializer.Deserialize(AgeValue.GetRawText(), options.GetTypeInfo(typeof(int)))!; } - return instance; - } - - private static bool TryGetProperty(JsonElement root, string name, JsonSerializerOptions options, out JsonElement value) - { - if (root.TryGetProperty(name, out value)) - { - return true; - } - string? convertedName = options.PropertyNamingPolicy?.ConvertName(name); - if (convertedName != null && convertedName != name && root.TryGetProperty(convertedName, out value)) - { - return true; - } - if (options.PropertyNameCaseInsensitive) - { - foreach (JsonProperty property in root.EnumerateObject()) - { - if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase)) - { - value = property.Value; - return true; - } - } - } - return false; - } - - private static bool TryGetValueInJson(JsonElement root, string propertyName, JsonSerializerOptions options, out JsonElement value) - { - if (TryGetProperty(root, propertyName, options, out value)) - { - return true; + return instance; } - if (propertyName.IndexOf('.') >= 0) + protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Gadget value, Type runtimeType, JsonSerializerOptions options) { - string[] segments = propertyName.Split('.'); - JsonElement current = root; - foreach (string segment in segments) - { - if (!TryGetProperty(current, segment, options, out current)) - { - value = default; - return false; - } - } - value = current; - return true; + return false; } - return false; - } } } \ No newline at end of file diff --git a/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/GameJsonSubTypesConverter.cs b/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/GameJsonSubTypesConverter.cs index 378086b..48171e5 100644 --- a/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/GameJsonSubTypesConverter.cs +++ b/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/GameJsonSubTypesConverter.cs @@ -1,300 +1,85 @@ #nullable enable using System; -using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; namespace JsonSubTypes.Aot.Generated { /// Generated by JsonSubTypes.Aot. Compiled converter for global::JsonSubTypes.Aot.Generated.TestDomain.Game. - public sealed class GameJsonSubTypesConverter : JsonConverter + public sealed class GameJsonSubTypesConverter : JsonSubTypesAotValueConverterBase { - public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Game); + private const string DiscriminatorPropertyNameValue = "$GameKind"; + protected override string DiscriminatorPropertyName => DiscriminatorPropertyNameValue; - public override void Write(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Game value, JsonSerializerOptions options) + protected override System.Collections.Generic.Dictionary> DiscriminatorWriters { get; } = new System.Collections.Generic.Dictionary> { - if (value is null) - { - writer.WriteNullValue(); - return; - } - Type runtimeType = value.GetType(); - string payload; - if (runtimeType == typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Game)) - { - if (IsRegistered(runtimeType)) - { - payload = SerializeBasePayload((global::JsonSubTypes.Aot.Generated.TestDomain.Game)value, options); - } - else - { - WriteBaseObject(writer, (global::JsonSubTypes.Aot.Generated.TestDomain.Game)value, options); - return; - } - } - else if (IsRegistered(runtimeType)) - { - payload = JsonSerializer.Serialize(value, options.GetTypeInfo(runtimeType)); - } - else if (TryWriteNestedObject(writer, value, runtimeType, options)) - { - return; - } - else if (TryWriteDynamic(writer, value, runtimeType, options)) + [typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Run)] = static (writer, options) => writer.WriteRawValue(JsonSerializer.Serialize(global::JsonSubTypes.Aot.Generated.TestDomain.GameDiscriminator.RUN, options.GetTypeInfo(typeof(global::JsonSubTypes.Aot.Generated.TestDomain.GameDiscriminator)))), + [typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Walk)] = static (writer, options) => writer.WriteRawValue(JsonSerializer.Serialize(global::JsonSubTypes.Aot.Generated.TestDomain.GameDiscriminator.WALK, options.GetTypeInfo(typeof(global::JsonSubTypes.Aot.Generated.TestDomain.GameDiscriminator)))) + }; + + protected override Type SelectType(JsonElement root, JsonSerializerOptions options) + { + if (!TryGetValueInJson(root, DiscriminatorPropertyNameValue, options, out JsonElement discriminator)) { - return; + return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Game); } - else + Type? staticType = ResolveStaticType(discriminator); + if (staticType != null) { - JsonSerializer.Serialize(writer, value, options.GetTypeInfo(runtimeType)); - return; + return staticType; } - using JsonDocument payloadDocument = JsonDocument.Parse(payload); - string discriminatorName = "$GameKind"; - if (options.PropertyNamingPolicy != null) + if (TryGetDynamicType(discriminator, out Type? dynamicType)) { - discriminatorName = options.PropertyNamingPolicy.ConvertName(discriminatorName); + return dynamicType!; } - writer.WriteStartObject(); - writer.WritePropertyName(discriminatorName); - WriteDiscriminatorValue(writer, runtimeType, options); - foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject()) + if (CustomTypeNameResolver is not null) { - if (!property.NameEquals(discriminatorName)) + Type? customType = CustomTypeNameResolver(GetDiscriminatorKey(discriminator)); + if (customType != null) { - property.WriteTo(writer); + return customType; } } - writer.WriteEndObject(); + return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Game); } - public override global::JsonSubTypes.Aot.Generated.TestDomain.Game? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + private static Type? ResolveStaticType(JsonElement discriminator) { - if (reader.TokenType == JsonTokenType.Null) - { - return null; - } - if (reader.TokenType != JsonTokenType.StartObject) - { - throw new JsonException("Unrecognized token: " + reader.TokenType); - } - using JsonDocument document = JsonDocument.ParseValue(ref reader); - JsonElement root = document.RootElement; - Type target = SelectType(root, options); - if (target == typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Game)) + switch (discriminator.ValueKind) { - return DeserializeBase(root, options); - } - return (global::JsonSubTypes.Aot.Generated.TestDomain.Game?)JsonSerializer.Deserialize(root.GetRawText(), options.GetTypeInfo(target)); - } - - private Type SelectType(JsonElement root, JsonSerializerOptions options) - { - if (TryGetValueInJson(root, "$GameKind", options, out JsonElement discriminator)) - { - - if (discriminator.ValueKind == JsonValueKind.String) - { - switch (discriminator.GetString()) - { + case JsonValueKind.String: + switch (discriminator.GetString()) + { case "RUN": return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Run); case "WALK": return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Walk); - } - } - if (discriminator.ValueKind == JsonValueKind.Number) - { - switch (discriminator.GetRawText()) - { + } + break; + case JsonValueKind.Number: + switch (discriminator.GetRawText()) + { case "0": return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Run); case "1": return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Walk); - } - } - if (TryGetDynamicType(discriminator, out Type? dynamicType)) - { - return dynamicType!; - } - if (CustomTypeNameResolver is not null) - { - Type? customType = CustomTypeNameResolver(GetDiscriminatorKey(discriminator)); - if (customType != null) - { - return customType; + } + break; } + return null; } - return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Game); - } - return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Game); - } - - private static readonly System.Collections.Generic.Dictionary> DiscriminatorWriters = new System.Collections.Generic.Dictionary> - { - [typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Run)] = static (writer, options) => writer.WriteRawValue(JsonSerializer.Serialize(global::JsonSubTypes.Aot.Generated.TestDomain.GameDiscriminator.RUN, options.GetTypeInfo(typeof(global::JsonSubTypes.Aot.Generated.TestDomain.GameDiscriminator)))), - [typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Walk)] = static (writer, options) => writer.WriteRawValue(JsonSerializer.Serialize(global::JsonSubTypes.Aot.Generated.TestDomain.GameDiscriminator.WALK, options.GetTypeInfo(typeof(global::JsonSubTypes.Aot.Generated.TestDomain.GameDiscriminator)))) - }; - private static bool IsRegistered(Type runtimeType) - { - return DiscriminatorWriters.ContainsKey(runtimeType); - } - - private static void WriteDiscriminatorValue(Utf8JsonWriter writer, Type runtimeType, JsonSerializerOptions options) - { - if (DiscriminatorWriters.TryGetValue(runtimeType, out System.Action? write)) - { - write(writer, options); - return; - } - throw new JsonException("Impossible to serialize type: " + runtimeType.FullName + " because there is no registered mapping for the discriminator property"); - } - - public readonly System.Collections.Concurrent.ConcurrentDictionary DynamicSubtypes = new System.Collections.Concurrent.ConcurrentDictionary(); - private readonly System.Collections.Concurrent.ConcurrentDictionary _dynamicReverse = new System.Collections.Concurrent.ConcurrentDictionary(); - - public void RegisterDynamicSubtype(object discriminator, Type type) - { - DynamicSubtypes[discriminator] = type; - _dynamicReverse[type] = discriminator; // last registration wins, like the builder - } - - /// - /// Custom discriminator-to-type resolution hook, invoked after the static - /// registrations and DynamicSubtypes. Assign it to implement your own - /// name-based lookup (e.g. assembly scanning, a DI registry). The resolved - /// type must be resolvable by the TypeInfoResolver (in the source-gen context - /// for Native AOT). - /// - public Func? CustomTypeNameResolver { get; set; } - - private static object? GetDiscriminatorKey(JsonElement discriminator) - { - switch (discriminator.ValueKind) - { - case JsonValueKind.String: - return discriminator.GetString(); - case JsonValueKind.Number when int.TryParse(discriminator.GetRawText(), out int keyInt): - return keyInt; - default: - return discriminator.GetRawText(); - } - } - - private bool TryGetDynamicType(JsonElement discriminator, out Type? dynamicType) - { - switch (discriminator.ValueKind) - { - case JsonValueKind.String: - return DynamicSubtypes.TryGetValue(discriminator.GetString()!, out dynamicType); - case JsonValueKind.Number: - if (int.TryParse(discriminator.GetRawText(), out int dynamicInt) && DynamicSubtypes.TryGetValue(dynamicInt, out dynamicType)) - { - return true; - } - return DynamicSubtypes.TryGetValue(discriminator.GetRawText(), out dynamicType); - default: - return DynamicSubtypes.TryGetValue(discriminator.GetRawText(), out dynamicType); - } - } - - private static bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Game value, Type runtimeType, JsonSerializerOptions options) - { - - return false; - } - - private bool TryWriteDynamic(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Game value, Type runtimeType, JsonSerializerOptions options) - { - if (_dynamicReverse.TryGetValue(runtimeType, out object? dynamicDiscriminator)) + protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Game value, JsonSerializerOptions options) { writer.WriteStartObject(); - string dynamicDiscriminatorName = "$GameKind"; - if (options.PropertyNamingPolicy != null) - { - dynamicDiscriminatorName = options.PropertyNamingPolicy.ConvertName(dynamicDiscriminatorName); - } - writer.WritePropertyName(dynamicDiscriminatorName); - writer.WriteRawValue(JsonSerializer.Serialize(dynamicDiscriminator, options.GetTypeInfo(dynamicDiscriminator.GetType()))); - string payload = JsonSerializer.Serialize(value, options.GetTypeInfo(runtimeType)); - using JsonDocument payloadDocument = JsonDocument.Parse(payload); - foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject()) - { - property.WriteTo(writer); - } - writer.WriteEndObject(); - return true; - } - return false; - } - private static string SerializeBasePayload(global::JsonSubTypes.Aot.Generated.TestDomain.Game value, JsonSerializerOptions options) - { - using System.IO.MemoryStream stream = new System.IO.MemoryStream(); - using (Utf8JsonWriter writer = new Utf8JsonWriter(stream)) - { - WriteBaseObject(writer, value, options); - } - return System.Text.Encoding.UTF8.GetString(stream.ToArray()); - } - - private static void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Game value, JsonSerializerOptions options) - { - writer.WriteStartObject(); - - writer.WriteEndObject(); - } - - private static global::JsonSubTypes.Aot.Generated.TestDomain.Game DeserializeBase(JsonElement root, JsonSerializerOptions options) - { - global::JsonSubTypes.Aot.Generated.TestDomain.Game instance = new global::JsonSubTypes.Aot.Generated.TestDomain.Game(); - - return instance; - } - private static bool TryGetProperty(JsonElement root, string name, JsonSerializerOptions options, out JsonElement value) - { - if (root.TryGetProperty(name, out value)) - { - return true; - } - string? convertedName = options.PropertyNamingPolicy?.ConvertName(name); - if (convertedName != null && convertedName != name && root.TryGetProperty(convertedName, out value)) - { - return true; + writer.WriteEndObject(); } - if (options.PropertyNameCaseInsensitive) + protected override global::JsonSubTypes.Aot.Generated.TestDomain.Game DeserializeBase(JsonElement root, JsonSerializerOptions options) { - foreach (JsonProperty property in root.EnumerateObject()) - { - if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase)) - { - value = property.Value; - return true; - } - } - } - return false; - } + global::JsonSubTypes.Aot.Generated.TestDomain.Game instance = new global::JsonSubTypes.Aot.Generated.TestDomain.Game(); - private static bool TryGetValueInJson(JsonElement root, string propertyName, JsonSerializerOptions options, out JsonElement value) - { - if (TryGetProperty(root, propertyName, options, out value)) - { - return true; + return instance; } - if (propertyName.IndexOf('.') >= 0) + protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Game value, Type runtimeType, JsonSerializerOptions options) { - string[] segments = propertyName.Split('.'); - JsonElement current = root; - foreach (string segment in segments) - { - if (!TryGetProperty(current, segment, options, out current)) - { - value = default; - return false; - } - } - value = current; - return true; + return false; } - return false; - } } } \ No newline at end of file diff --git a/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/JsonSubTypesAotConverterBases.cs b/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/JsonSubTypesAotConverterBases.cs new file mode 100644 index 0000000..4b65802 --- /dev/null +++ b/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/JsonSubTypesAotConverterBases.cs @@ -0,0 +1,281 @@ +#nullable enable +using System; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace JsonSubTypes.Aot.Generated +{ + /// Generated by JsonSubTypes.Aot. Shared converter skeleton: Read, the dynamic-subtype machinery and the base-object helpers live here once so every converter test exercises them. + public abstract class JsonSubTypesAotConverterBase : JsonConverter where T : class + { + public sealed override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(T); + + public sealed override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("Unrecognized token: " + reader.TokenType); + } + using JsonDocument document = JsonDocument.ParseValue(ref reader); + JsonElement root = document.RootElement; + Type target = SelectType(root, options); + if (target == typeof(T)) + { + return DeserializeBase(root, options); + } + return (T?)JsonSerializer.Deserialize(root.GetRawText(), options.GetTypeInfo(target)); + } + + // Presence-mode write: the base object is written through WriteBaseObject, + // registered subtypes through their own resolver-provided converters, and + // unknown types through the type info resolver. Value-mode converters + // override Write to inject the discriminator. + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + if (value is null) + { + writer.WriteNullValue(); + return; + } + Type runtimeType = value.GetType(); + if (runtimeType == typeof(T)) + { + WriteBaseObject(writer, value, options); + return; + } + JsonSerializer.Serialize(writer, value, options.GetTypeInfo(runtimeType)); + } + + protected abstract Type SelectType(JsonElement root, JsonSerializerOptions options); + protected abstract T DeserializeBase(JsonElement root, JsonSerializerOptions options); + protected abstract void WriteBaseObject(Utf8JsonWriter writer, T value, JsonSerializerOptions options); + protected abstract bool TryWriteNestedObject(Utf8JsonWriter writer, T value, Type runtimeType, JsonSerializerOptions options); + + // Discriminator property name used by the dynamic write path; unused by + // presence-mode converters. + protected virtual string DiscriminatorPropertyName => ""; + + public System.Collections.Concurrent.ConcurrentDictionary DynamicSubtypes { get; } = new System.Collections.Concurrent.ConcurrentDictionary(); + private readonly System.Collections.Concurrent.ConcurrentDictionary _dynamicReverse = new System.Collections.Concurrent.ConcurrentDictionary(); + + public void RegisterDynamicSubtype(object discriminator, Type type) + { + DynamicSubtypes[discriminator] = type; + _dynamicReverse[type] = discriminator; // last registration wins, like the builder + } + + /// + /// Custom discriminator-to-type resolution hook, invoked after the static + /// registrations and DynamicSubtypes. Assign it to implement your own + /// name-based lookup (e.g. assembly scanning, a DI registry). The resolved + /// type must be resolvable by the TypeInfoResolver (in the source-gen context + /// for Native AOT). + /// + public Func? CustomTypeNameResolver { get; set; } + + protected static object? GetDiscriminatorKey(JsonElement discriminator) + { + switch (discriminator.ValueKind) + { + case JsonValueKind.String: + return discriminator.GetString(); + case JsonValueKind.Number when int.TryParse(discriminator.GetRawText(), out int keyInt): + return keyInt; + default: + return discriminator.GetRawText(); + } + } + + protected bool TryGetDynamicType(JsonElement discriminator, out Type? dynamicType) + { + switch (discriminator.ValueKind) + { + case JsonValueKind.String: + return DynamicSubtypes.TryGetValue(discriminator.GetString()!, out dynamicType); + case JsonValueKind.Number: + if (int.TryParse(discriminator.GetRawText(), out int dynamicInt) && DynamicSubtypes.TryGetValue(dynamicInt, out dynamicType)) + { + return true; + } + return DynamicSubtypes.TryGetValue(discriminator.GetRawText(), out dynamicType); + default: + return DynamicSubtypes.TryGetValue(discriminator.GetRawText(), out dynamicType); + } + } + + protected bool TryWriteDynamic(Utf8JsonWriter writer, T value, Type runtimeType, JsonSerializerOptions options) + { + if (_dynamicReverse.TryGetValue(runtimeType, out object? dynamicDiscriminator)) + { + writer.WriteStartObject(); + string dynamicDiscriminatorName = DiscriminatorPropertyName; + if (options.PropertyNamingPolicy != null) + { + dynamicDiscriminatorName = options.PropertyNamingPolicy.ConvertName(dynamicDiscriminatorName); + } + writer.WritePropertyName(dynamicDiscriminatorName); + writer.WriteRawValue(JsonSerializer.Serialize(dynamicDiscriminator, options.GetTypeInfo(dynamicDiscriminator.GetType()))); + string payload = JsonSerializer.Serialize(value, options.GetTypeInfo(runtimeType)); + using JsonDocument payloadDocument = JsonDocument.Parse(payload); + foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject()) + { + property.WriteTo(writer); + } + writer.WriteEndObject(); + return true; + } + return false; + } + + protected string SerializeBasePayload(T value, JsonSerializerOptions options) + { + using System.IO.MemoryStream stream = new System.IO.MemoryStream(); + using (Utf8JsonWriter writer = new Utf8JsonWriter(stream)) + { + WriteBaseObject(writer, value, options); + } + return System.Text.Encoding.UTF8.GetString(stream.ToArray()); + } + + protected static bool TryGetProperty(JsonElement root, string name, JsonSerializerOptions options, out JsonElement value) + { + if (root.TryGetProperty(name, out value)) + { + return true; + } + string? convertedName = options.PropertyNamingPolicy?.ConvertName(name); + if (convertedName != null && convertedName != name && root.TryGetProperty(convertedName, out value)) + { + return true; + } + if (options.PropertyNameCaseInsensitive) + { + foreach (JsonProperty property in root.EnumerateObject().Where(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase))) + { + value = property.Value; + return true; + } + } + return false; + } + + protected static bool TryGetValueInJson(JsonElement root, string propertyName, JsonSerializerOptions options, out JsonElement value) + { + if (TryGetProperty(root, propertyName, options, out value)) + { + return true; + } + if (propertyName.Contains('.')) + { + string[] segments = propertyName.Split('.'); + JsonElement current = root; + foreach (string segment in segments) + { + if (!TryGetProperty(current, segment, options, out current)) + { + value = default; + return false; + } + } + value = current; + return true; + } + return false; + } + } + + /// Generated by JsonSubTypes.Aot. Value-mode converter skeleton: adds the discriminator-injection Write on top of JsonSubTypesAotConverterBase. + public abstract class JsonSubTypesAotValueConverterBase : JsonSubTypesAotConverterBase where T : class + { + public sealed override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + if (value is null) + { + writer.WriteNullValue(); + return; + } + + Type runtimeType = value.GetType(); + if (!TryGetPayload(writer, value, runtimeType, options, out string payload)) + { + return; + } + + using JsonDocument payloadDocument = JsonDocument.Parse(payload); + string discriminatorName = options.PropertyNamingPolicy?.ConvertName(DiscriminatorPropertyName) ?? DiscriminatorPropertyName; + writer.WriteStartObject(); + if (AddDiscriminatorFirst) + { + writer.WritePropertyName(discriminatorName); + WriteDiscriminatorValue(writer, runtimeType, options); + } + foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject().Where(p => !p.NameEquals(discriminatorName))) + { + property.WriteTo(writer); + } + if (!AddDiscriminatorFirst) + { + writer.WritePropertyName(discriminatorName); + WriteDiscriminatorValue(writer, runtimeType, options); + } + writer.WriteEndObject(); + } + + // Resolves the serialized payload of the runtime type. Returns false + // when the value was already written directly (base fallback, nested + // chain, dynamic subtype or unregistered runtime type). + private bool TryGetPayload(Utf8JsonWriter writer, T value, Type runtimeType, JsonSerializerOptions options, out string payload) + { + payload = ""; + if (runtimeType == typeof(T)) + { + if (IsRegistered(runtimeType)) + { + payload = SerializeBasePayload(value, options); + return true; + } + WriteBaseObject(writer, value, options); + return false; + } + if (IsRegistered(runtimeType)) + { + payload = JsonSerializer.Serialize(value, options.GetTypeInfo(runtimeType)); + return true; + } + if (TryWriteNestedObject(writer, value, runtimeType, options)) + { + return false; + } + if (TryWriteDynamic(writer, value, runtimeType, options)) + { + return false; + } + JsonSerializer.Serialize(writer, value, options.GetTypeInfo(runtimeType)); + return false; + } + + protected virtual System.Collections.Generic.Dictionary> DiscriminatorWriters { get; } = new System.Collections.Generic.Dictionary>(); + + protected bool IsRegistered(Type runtimeType) + { + return DiscriminatorWriters.ContainsKey(runtimeType); + } + + protected virtual bool AddDiscriminatorFirst => true; + + private void WriteDiscriminatorValue(Utf8JsonWriter writer, Type runtimeType, JsonSerializerOptions options) + { + if (DiscriminatorWriters.TryGetValue(runtimeType, out System.Action? write)) + { + write(writer, options); + return; + } + throw new JsonException("Impossible to serialize type: " + runtimeType.FullName + " because there is no registered mapping for the discriminator property"); + } + } +} \ No newline at end of file diff --git a/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/PayloadJsonSubTypesConverter.cs b/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/PayloadJsonSubTypesConverter.cs index d51b729..a6a3f99 100644 --- a/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/PayloadJsonSubTypesConverter.cs +++ b/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/PayloadJsonSubTypesConverter.cs @@ -1,206 +1,88 @@ #nullable enable using System; -using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; namespace JsonSubTypes.Aot.Generated { /// Generated by JsonSubTypes.Aot. Compiled converter for global::JsonSubTypes.Aot.Generated.TestDomain.Payload. - public sealed class PayloadJsonSubTypesConverter : JsonConverter + public sealed class PayloadJsonSubTypesConverter : JsonSubTypesAotValueConverterBase { - public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Payload); + private const string DiscriminatorPropertyNameValue = "$PayloadKind"; + protected override string DiscriminatorPropertyName => DiscriminatorPropertyNameValue; - public override void Write(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Payload value, JsonSerializerOptions options) + protected override System.Collections.Generic.Dictionary> DiscriminatorWriters { get; } = new System.Collections.Generic.Dictionary> { - if (value is null) - { - writer.WriteNullValue(); - return; - } - Type runtimeType = value.GetType(); - string payload; - if (runtimeType == typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Payload)) - { - if (IsRegistered(runtimeType)) - { - payload = SerializeBasePayload((global::JsonSubTypes.Aot.Generated.TestDomain.Payload)value, options); - } - else - { - WriteBaseObject(writer, (global::JsonSubTypes.Aot.Generated.TestDomain.Payload)value, options); - return; - } - } - else if (IsRegistered(runtimeType)) - { - payload = JsonSerializer.Serialize(value, options.GetTypeInfo(runtimeType)); - } - else if (TryWriteNestedObject(writer, value, runtimeType, options)) + [typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Game)] = static (writer, options) => writer.WriteRawValue(JsonSerializer.Serialize(global::JsonSubTypes.Aot.Generated.TestDomain.PayloadDiscriminator.GAME, options.GetTypeInfo(typeof(global::JsonSubTypes.Aot.Generated.TestDomain.PayloadDiscriminator)))), + [typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Com)] = static (writer, options) => writer.WriteRawValue(JsonSerializer.Serialize(global::JsonSubTypes.Aot.Generated.TestDomain.PayloadDiscriminator.COM, options.GetTypeInfo(typeof(global::JsonSubTypes.Aot.Generated.TestDomain.PayloadDiscriminator)))) + }; + + protected override Type SelectType(JsonElement root, JsonSerializerOptions options) + { + if (!TryGetValueInJson(root, DiscriminatorPropertyNameValue, options, out JsonElement discriminator)) { - return; + return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Payload); } - else if (TryWriteDynamic(writer, value, runtimeType, options)) + Type? staticType = ResolveStaticType(discriminator); + if (staticType != null) { - return; + return staticType; } - else + if (TryGetDynamicType(discriminator, out Type? dynamicType)) { - JsonSerializer.Serialize(writer, value, options.GetTypeInfo(runtimeType)); - return; + return dynamicType!; } - using JsonDocument payloadDocument = JsonDocument.Parse(payload); - string discriminatorName = "$PayloadKind"; - if (options.PropertyNamingPolicy != null) + if (CustomTypeNameResolver is not null) { - discriminatorName = options.PropertyNamingPolicy.ConvertName(discriminatorName); - } - writer.WriteStartObject(); - writer.WritePropertyName(discriminatorName); - WriteDiscriminatorValue(writer, runtimeType, options); - foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject()) - { - if (!property.NameEquals(discriminatorName)) + Type? customType = CustomTypeNameResolver(GetDiscriminatorKey(discriminator)); + if (customType != null) { - property.WriteTo(writer); + return customType; } } - writer.WriteEndObject(); + return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Payload); } - public override global::JsonSubTypes.Aot.Generated.TestDomain.Payload? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + private static Type? ResolveStaticType(JsonElement discriminator) { - if (reader.TokenType == JsonTokenType.Null) - { - return null; - } - if (reader.TokenType != JsonTokenType.StartObject) - { - throw new JsonException("Unrecognized token: " + reader.TokenType); - } - using JsonDocument document = JsonDocument.ParseValue(ref reader); - JsonElement root = document.RootElement; - Type target = SelectType(root, options); - if (target == typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Payload)) + switch (discriminator.ValueKind) { - return DeserializeBase(root, options); - } - return (global::JsonSubTypes.Aot.Generated.TestDomain.Payload?)JsonSerializer.Deserialize(root.GetRawText(), options.GetTypeInfo(target)); - } - - private Type SelectType(JsonElement root, JsonSerializerOptions options) - { - if (TryGetValueInJson(root, "$PayloadKind", options, out JsonElement discriminator)) - { - - if (discriminator.ValueKind == JsonValueKind.String) - { - switch (discriminator.GetString()) - { + case JsonValueKind.String: + switch (discriminator.GetString()) + { case "GAME": return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Game); case "COM": return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Com); - } - } - if (discriminator.ValueKind == JsonValueKind.Number) - { - switch (discriminator.GetRawText()) - { + } + break; + case JsonValueKind.Number: + switch (discriminator.GetRawText()) + { case "0": return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Game); case "1": return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Com); - } - } - if (TryGetDynamicType(discriminator, out Type? dynamicType)) - { - return dynamicType!; - } - if (CustomTypeNameResolver is not null) - { - Type? customType = CustomTypeNameResolver(GetDiscriminatorKey(discriminator)); - if (customType != null) - { - return customType; + } + break; } + return null; } - return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Payload); - } - return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Payload); - } - - private static readonly System.Collections.Generic.Dictionary> DiscriminatorWriters = new System.Collections.Generic.Dictionary> - { - [typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Game)] = static (writer, options) => writer.WriteRawValue(JsonSerializer.Serialize(global::JsonSubTypes.Aot.Generated.TestDomain.PayloadDiscriminator.GAME, options.GetTypeInfo(typeof(global::JsonSubTypes.Aot.Generated.TestDomain.PayloadDiscriminator)))), - [typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Com)] = static (writer, options) => writer.WriteRawValue(JsonSerializer.Serialize(global::JsonSubTypes.Aot.Generated.TestDomain.PayloadDiscriminator.COM, options.GetTypeInfo(typeof(global::JsonSubTypes.Aot.Generated.TestDomain.PayloadDiscriminator)))) - }; - - private static bool IsRegistered(Type runtimeType) - { - return DiscriminatorWriters.ContainsKey(runtimeType); - } - private static void WriteDiscriminatorValue(Utf8JsonWriter writer, Type runtimeType, JsonSerializerOptions options) - { - if (DiscriminatorWriters.TryGetValue(runtimeType, out System.Action? write)) + protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Payload value, JsonSerializerOptions options) { - write(writer, options); - return; - } - throw new JsonException("Impossible to serialize type: " + runtimeType.FullName + " because there is no registered mapping for the discriminator property"); - } - - public readonly System.Collections.Concurrent.ConcurrentDictionary DynamicSubtypes = new System.Collections.Concurrent.ConcurrentDictionary(); - private readonly System.Collections.Concurrent.ConcurrentDictionary _dynamicReverse = new System.Collections.Concurrent.ConcurrentDictionary(); - - public void RegisterDynamicSubtype(object discriminator, Type type) - { - DynamicSubtypes[discriminator] = type; - _dynamicReverse[type] = discriminator; // last registration wins, like the builder - } - - /// - /// Custom discriminator-to-type resolution hook, invoked after the static - /// registrations and DynamicSubtypes. Assign it to implement your own - /// name-based lookup (e.g. assembly scanning, a DI registry). The resolved - /// type must be resolvable by the TypeInfoResolver (in the source-gen context - /// for Native AOT). - /// - public Func? CustomTypeNameResolver { get; set; } + writer.WriteStartObject(); - private static object? GetDiscriminatorKey(JsonElement discriminator) - { - switch (discriminator.ValueKind) - { - case JsonValueKind.String: - return discriminator.GetString(); - case JsonValueKind.Number when int.TryParse(discriminator.GetRawText(), out int keyInt): - return keyInt; - default: - return discriminator.GetRawText(); + writer.WriteEndObject(); } - } - - private bool TryGetDynamicType(JsonElement discriminator, out Type? dynamicType) - { - switch (discriminator.ValueKind) + protected override global::JsonSubTypes.Aot.Generated.TestDomain.Payload DeserializeBase(JsonElement root, JsonSerializerOptions options) { - case JsonValueKind.String: - return DynamicSubtypes.TryGetValue(discriminator.GetString()!, out dynamicType); - case JsonValueKind.Number: - if (int.TryParse(discriminator.GetRawText(), out int dynamicInt) && DynamicSubtypes.TryGetValue(dynamicInt, out dynamicType)) - { - return true; - } - return DynamicSubtypes.TryGetValue(discriminator.GetRawText(), out dynamicType); - default: - return DynamicSubtypes.TryGetValue(discriminator.GetRawText(), out dynamicType); - } - } + global::JsonSubTypes.Aot.Generated.TestDomain.Payload instance = new global::JsonSubTypes.Aot.Generated.TestDomain.Payload(); - private static bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Payload value, Type runtimeType, JsonSerializerOptions options) - { + return instance; + } + protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Payload value, Type runtimeType, JsonSerializerOptions options) + { if (runtimeType == typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Run)) { writer.WriteStartObject(); - writer.WritePropertyName("$PayloadKind"); + writer.WritePropertyName(DiscriminatorPropertyNameValue); writer.WriteRawValue(JsonSerializer.Serialize(global::JsonSubTypes.Aot.Generated.TestDomain.PayloadDiscriminator.GAME, options.GetTypeInfo(typeof(global::JsonSubTypes.Aot.Generated.TestDomain.PayloadDiscriminator)))); writer.WritePropertyName("$GameKind"); writer.WriteRawValue(JsonSerializer.Serialize(global::JsonSubTypes.Aot.Generated.TestDomain.GameDiscriminator.RUN, options.GetTypeInfo(typeof(global::JsonSubTypes.Aot.Generated.TestDomain.GameDiscriminator)))); @@ -216,7 +98,7 @@ private static bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubT if (runtimeType == typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Walk)) { writer.WriteStartObject(); - writer.WritePropertyName("$PayloadKind"); + writer.WritePropertyName(DiscriminatorPropertyNameValue); writer.WriteRawValue(JsonSerializer.Serialize(global::JsonSubTypes.Aot.Generated.TestDomain.PayloadDiscriminator.GAME, options.GetTypeInfo(typeof(global::JsonSubTypes.Aot.Generated.TestDomain.PayloadDiscriminator)))); writer.WritePropertyName("$GameKind"); writer.WriteRawValue(JsonSerializer.Serialize(global::JsonSubTypes.Aot.Generated.TestDomain.GameDiscriminator.WALK, options.GetTypeInfo(typeof(global::JsonSubTypes.Aot.Generated.TestDomain.GameDiscriminator)))); @@ -229,103 +111,7 @@ private static bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubT writer.WriteEndObject(); return true; } - return false; - } - - private bool TryWriteDynamic(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Payload value, Type runtimeType, JsonSerializerOptions options) - { - if (_dynamicReverse.TryGetValue(runtimeType, out object? dynamicDiscriminator)) - { - writer.WriteStartObject(); - string dynamicDiscriminatorName = "$PayloadKind"; - if (options.PropertyNamingPolicy != null) - { - dynamicDiscriminatorName = options.PropertyNamingPolicy.ConvertName(dynamicDiscriminatorName); - } - writer.WritePropertyName(dynamicDiscriminatorName); - writer.WriteRawValue(JsonSerializer.Serialize(dynamicDiscriminator, options.GetTypeInfo(dynamicDiscriminator.GetType()))); - string payload = JsonSerializer.Serialize(value, options.GetTypeInfo(runtimeType)); - using JsonDocument payloadDocument = JsonDocument.Parse(payload); - foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject()) - { - property.WriteTo(writer); - } - writer.WriteEndObject(); - return true; - } - return false; - } - private static string SerializeBasePayload(global::JsonSubTypes.Aot.Generated.TestDomain.Payload value, JsonSerializerOptions options) - { - using System.IO.MemoryStream stream = new System.IO.MemoryStream(); - using (Utf8JsonWriter writer = new Utf8JsonWriter(stream)) - { - WriteBaseObject(writer, value, options); - } - return System.Text.Encoding.UTF8.GetString(stream.ToArray()); - } - - private static void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Payload value, JsonSerializerOptions options) - { - writer.WriteStartObject(); - - writer.WriteEndObject(); - } - - private static global::JsonSubTypes.Aot.Generated.TestDomain.Payload DeserializeBase(JsonElement root, JsonSerializerOptions options) - { - global::JsonSubTypes.Aot.Generated.TestDomain.Payload instance = new global::JsonSubTypes.Aot.Generated.TestDomain.Payload(); - - return instance; - } - - private static bool TryGetProperty(JsonElement root, string name, JsonSerializerOptions options, out JsonElement value) - { - if (root.TryGetProperty(name, out value)) - { - return true; - } - string? convertedName = options.PropertyNamingPolicy?.ConvertName(name); - if (convertedName != null && convertedName != name && root.TryGetProperty(convertedName, out value)) - { - return true; - } - if (options.PropertyNameCaseInsensitive) - { - foreach (JsonProperty property in root.EnumerateObject()) - { - if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase)) - { - value = property.Value; - return true; - } - } - } - return false; - } - - private static bool TryGetValueInJson(JsonElement root, string propertyName, JsonSerializerOptions options, out JsonElement value) - { - if (TryGetProperty(root, propertyName, options, out value)) - { - return true; - } - if (propertyName.IndexOf('.') >= 0) - { - string[] segments = propertyName.Split('.'); - JsonElement current = root; - foreach (string segment in segments) - { - if (!TryGetProperty(current, segment, options, out current)) - { - value = default; - return false; - } - } - value = current; - return true; + return false; } - return false; - } } } \ No newline at end of file diff --git a/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/PersonJsonSubTypesConverter.cs b/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/PersonJsonSubTypesConverter.cs index 0e6c9cf..902773a 100644 --- a/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/PersonJsonSubTypesConverter.cs +++ b/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/PersonJsonSubTypesConverter.cs @@ -7,82 +7,33 @@ namespace JsonSubTypes.Aot.Generated { /// Generated by JsonSubTypes.Aot. Compiled converter for global::JsonSubTypes.Aot.Generated.TestDomain.Person. - public sealed class PersonJsonSubTypesConverter : JsonConverter + public sealed class PersonJsonSubTypesConverter : JsonSubTypesAotConverterBase { - public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Person); - - public override void Write(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Person value, JsonSerializerOptions options) + protected override Type SelectType(JsonElement root, JsonSerializerOptions options) { - if (value is null) - { - writer.WriteNullValue(); - return; - } - Type runtimeType = value.GetType(); - if (runtimeType == typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Person)) - { - WriteBaseObject(writer, (global::JsonSubTypes.Aot.Generated.TestDomain.Person)value, options); - return; - } - JsonSerializer.Serialize(writer, value, options.GetTypeInfo(runtimeType)); + System.Collections.Generic.HashSet matches = new System.Collections.Generic.HashSet(); + if (root.TryGetProperty("JobTitle", out _)) + { + matches.Add(typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Employee)); } - - public override global::JsonSubTypes.Aot.Generated.TestDomain.Person? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + if (root.TryGetProperty("Skill", out _)) { - if (reader.TokenType == JsonTokenType.Null) - { - return null; - } - if (reader.TokenType != JsonTokenType.StartObject) + matches.Add(typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Artist)); + } + if (matches.Count == 1) { - throw new JsonException("Unrecognized token: " + reader.TokenType); + return matches.First(); } - using JsonDocument document = JsonDocument.ParseValue(ref reader); - JsonElement root = document.RootElement; - Type target = SelectType(root, options); - if (target == typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Person)) + if (matches.Count > 1) { - return DeserializeBase(root, options); + throw new JsonException("Ambiguous type resolution, expected only one type but got: " + string.Join(", ", matches.Select(t => t.FullName))); } - return (global::JsonSubTypes.Aot.Generated.TestDomain.Person?)JsonSerializer.Deserialize(root.GetRawText(), options.GetTypeInfo(target)); - } - - private Type SelectType(JsonElement root, JsonSerializerOptions options) - { - System.Collections.Generic.HashSet matches = new System.Collections.Generic.HashSet(); -if (root.TryGetProperty("JobTitle", out _)) -{ - matches.Add(typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Employee)); -} -if (root.TryGetProperty("Skill", out _)) -{ - matches.Add(typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Artist)); -} - if (matches.Count == 1) - { - return matches.First(); - } - if (matches.Count > 1) - { - throw new JsonException("Ambiguous type resolution, expected only one type but got: " + string.Join(", ", matches.Select(t => t.FullName))); - } - return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Person); + return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Person); } - - private static string SerializeBasePayload(global::JsonSubTypes.Aot.Generated.TestDomain.Person value, JsonSerializerOptions options) - { - using System.IO.MemoryStream stream = new System.IO.MemoryStream(); - using (Utf8JsonWriter writer = new Utf8JsonWriter(stream)) + protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Person value, JsonSerializerOptions options) { - WriteBaseObject(writer, value, options); - } - return System.Text.Encoding.UTF8.GetString(stream.ToArray()); - } - - private static void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Person value, JsonSerializerOptions options) - { - writer.WriteStartObject(); + writer.WriteStartObject(); string nameFirstName = "FirstName"; if (options.PropertyNamingPolicy != null && true) { @@ -90,66 +41,20 @@ private static void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes. } writer.WritePropertyName(nameFirstName); JsonSerializer.Serialize(writer, value.FirstName, options.GetTypeInfo(typeof(string))); - writer.WriteEndObject(); - } - - private static global::JsonSubTypes.Aot.Generated.TestDomain.Person DeserializeBase(JsonElement root, JsonSerializerOptions options) - { - global::JsonSubTypes.Aot.Generated.TestDomain.Person instance = new global::JsonSubTypes.Aot.Generated.TestDomain.Person(); + writer.WriteEndObject(); + } + protected override global::JsonSubTypes.Aot.Generated.TestDomain.Person DeserializeBase(JsonElement root, JsonSerializerOptions options) + { + global::JsonSubTypes.Aot.Generated.TestDomain.Person instance = new global::JsonSubTypes.Aot.Generated.TestDomain.Person(); if (TryGetProperty(root, "FirstName", options, out JsonElement FirstNameValue)) { instance.FirstName = (string)JsonSerializer.Deserialize(FirstNameValue.GetRawText(), options.GetTypeInfo(typeof(string)))!; } - return instance; - } - - private static bool TryGetProperty(JsonElement root, string name, JsonSerializerOptions options, out JsonElement value) - { - if (root.TryGetProperty(name, out value)) - { - return true; - } - string? convertedName = options.PropertyNamingPolicy?.ConvertName(name); - if (convertedName != null && convertedName != name && root.TryGetProperty(convertedName, out value)) - { - return true; - } - if (options.PropertyNameCaseInsensitive) - { - foreach (JsonProperty property in root.EnumerateObject()) - { - if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase)) - { - value = property.Value; - return true; - } - } - } - return false; - } - - private static bool TryGetValueInJson(JsonElement root, string propertyName, JsonSerializerOptions options, out JsonElement value) - { - if (TryGetProperty(root, propertyName, options, out value)) - { - return true; + return instance; } - if (propertyName.IndexOf('.') >= 0) + protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Person value, Type runtimeType, JsonSerializerOptions options) { - string[] segments = propertyName.Split('.'); - JsonElement current = root; - foreach (string segment in segments) - { - if (!TryGetProperty(current, segment, options, out current)) - { - value = default; - return false; - } - } - value = current; - return true; + return false; } - return false; - } } } \ No newline at end of file diff --git a/JsonSubTypes.Aot.Generator.Tests/CommittedGeneratedConverterTests.cs b/JsonSubTypes.Aot.Generator.Tests/CommittedGeneratedConverterTests.cs index a1ab4d7..1a60d1d 100644 --- a/JsonSubTypes.Aot.Generator.Tests/CommittedGeneratedConverterTests.cs +++ b/JsonSubTypes.Aot.Generator.Tests/CommittedGeneratedConverterTests.cs @@ -117,29 +117,35 @@ public void DottedGadget_DeserializeNestedDiscriminator_ReturnsSubtype() // ---- Payload / Game (nested multi-level hierarchy) ---- + private static readonly JsonSerializerOptions PayloadAndGameOptions = new() + { + TypeInfoResolver = TestDomainJsonContext.Default, + Converters = + { + (JsonConverter)RegistryConverter(typeof(Payload)), + (JsonConverter)RegistryConverter(typeof(Game)) + } + }; + + private static readonly JsonSerializerOptions PayloadOptions = new() + { + TypeInfoResolver = TestDomainJsonContext.Default, + Converters = { (JsonConverter)RegistryConverter(typeof(Payload)) } + }; + [Test] public void Payload_DeserializeNestedGameKind_ReturnsRun() { - var options = new JsonSerializerOptions - { - TypeInfoResolver = TestDomainJsonContext.Default, - Converters = { (JsonConverter)RegistryConverter(typeof(Payload)), (JsonConverter)RegistryConverter(typeof(Game)) } - }; Payload? result = JsonSerializer.Deserialize( - "{\"$PayloadKind\":0,\"$GameKind\":0}", options); + "{\"$PayloadKind\":0,\"$GameKind\":0}", PayloadAndGameOptions); Assert.That(result, Is.InstanceOf()); } [Test] public void Payload_DeserializeCom_ReturnsCom() { - var options = new JsonSerializerOptions - { - TypeInfoResolver = TestDomainJsonContext.Default, - Converters = { (JsonConverter)RegistryConverter(typeof(Payload)) } - }; Payload? result = JsonSerializer.Deserialize( - "{\"$PayloadKind\":1}", options); + "{\"$PayloadKind\":1}", PayloadOptions); Assert.That(result, Is.InstanceOf()); } } diff --git a/JsonSubTypes.Aot.Generator.Tests/GeneratedCodeExecutionTests.cs b/JsonSubTypes.Aot.Generator.Tests/GeneratedCodeExecutionTests.cs index bce3f94..b3dcaa3 100644 --- a/JsonSubTypes.Aot.Generator.Tests/GeneratedCodeExecutionTests.cs +++ b/JsonSubTypes.Aot.Generator.Tests/GeneratedCodeExecutionTests.cs @@ -31,6 +31,15 @@ public class Cat : Animal { public int Lives { get; set; } } public class Dog : Animal { public bool CanHunt { get; set; } } "; + private static readonly (Type AnimalType, Type CatType, object Converter) Loaded = CompileAndLoad(); + + // The converter is a static singleton (the fixture compiles and loads once); + // the options instance is shared by every test so it is created only once. + private static readonly JsonSerializerOptions Options = new() + { + Converters = { (JsonConverter)Loaded.Converter } + }; + private static (Type animalType, Type catType, object converter) CompileAndLoad() { GeneratorRun run = GeneratorDriverRunner.GetRun(Hierarchy); @@ -57,13 +66,11 @@ private static (Type animalType, Type catType, object converter) CompileAndLoad( [Test] public void Serialize_Cat_WritesDiscriminator() { - (Type animalType, Type catType, object converter) = CompileAndLoad(); - object cat = Activator.CreateInstance(catType)!; - catType.GetProperty("Age")!.SetValue(cat, 3); - catType.GetProperty("Lives")!.SetValue(cat, 9); + object cat = Activator.CreateInstance(Loaded.CatType)!; + Loaded.CatType.GetProperty("Age")!.SetValue(cat, 3); + Loaded.CatType.GetProperty("Lives")!.SetValue(cat, 9); - var options = new JsonSerializerOptions { Converters = { (JsonConverter)converter } }; - string json = JsonSerializer.Serialize(cat, animalType, options); + string json = JsonSerializer.Serialize(cat, Loaded.AnimalType, Options); Assert.That(json, Does.Contain("\"type\":\"cat\"")); } @@ -71,10 +78,7 @@ public void Serialize_Cat_WritesDiscriminator() [Test] public void Deserialize_CatDiscriminator_ReturnsCat() { - (Type animalType, _, object converter) = CompileAndLoad(); - - var options = new JsonSerializerOptions { Converters = { (JsonConverter)converter } }; - object? result = JsonSerializer.Deserialize("{\"type\":\"cat\",\"Lives\":9,\"Age\":3}", animalType, options); + object? result = JsonSerializer.Deserialize("{\"type\":\"cat\",\"Lives\":9,\"Age\":3}", Loaded.AnimalType, Options); Assert.That(result, Is.Not.Null); Assert.That(result!.GetType().Name, Is.EqualTo("Cat")); @@ -83,10 +87,7 @@ public void Deserialize_CatDiscriminator_ReturnsCat() [Test] public void Deserialize_UnknownDiscriminator_FallsBackToBase() { - (Type animalType, _, object converter) = CompileAndLoad(); - - var options = new JsonSerializerOptions { Converters = { (JsonConverter)converter } }; - object? result = JsonSerializer.Deserialize("{\"type\":\"fish\",\"Age\":3}", animalType, options); + object? result = JsonSerializer.Deserialize("{\"type\":\"fish\",\"Age\":3}", Loaded.AnimalType, Options); Assert.That(result, Is.Not.Null); Assert.That(result!.GetType().Name, Is.EqualTo("Animal")); diff --git a/JsonSubTypes.Aot.Generator.Tests/GeneratorDriverRunner.cs b/JsonSubTypes.Aot.Generator.Tests/GeneratorDriverRunner.cs index 493a877..fc54151 100644 --- a/JsonSubTypes.Aot.Generator.Tests/GeneratorDriverRunner.cs +++ b/JsonSubTypes.Aot.Generator.Tests/GeneratorDriverRunner.cs @@ -26,6 +26,7 @@ internal static class GeneratorDriverRunner MetadataReference.CreateFromFile(Assembly.Load("System.Private.CoreLib").Location), MetadataReference.CreateFromFile(Assembly.Load("netstandard").Location), MetadataReference.CreateFromFile(typeof(System.Collections.Generic.List<>).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.Linq.Enumerable).Assembly.Location), MetadataReference.CreateFromFile(Assembly.Load("System.Memory").Location), MetadataReference.CreateFromFile(Assembly.Load("System.Collections.Concurrent").Location), MetadataReference.CreateFromFile(Assembly.Load("System.Text.Json").Location), diff --git a/JsonSubTypes.Aot.Generator.Tests/GeneratorDriverTests.cs b/JsonSubTypes.Aot.Generator.Tests/GeneratorDriverTests.cs index 9589887..8fae5f9 100644 --- a/JsonSubTypes.Aot.Generator.Tests/GeneratorDriverTests.cs +++ b/JsonSubTypes.Aot.Generator.Tests/GeneratorDriverTests.cs @@ -23,6 +23,11 @@ public class Cat : Animal { public int Lives { get; set; } } public class Dog : Animal { public bool CanHunt { get; set; } } "; + private static readonly string[] SingleBaseExpectedHints = + { + "AnimalJsonSubTypesConverter.g.cs", "JsonSubTypesAotConverterBases.g.cs", "JsonSubTypesAotConverters.g.cs" + }; + [Test] public void Generate_SingleBase_EmitsConverterAndRegistry() { @@ -31,9 +36,7 @@ public void Generate_SingleBase_EmitsConverterAndRegistry() string[] hints = run.DriverResults.Results .SelectMany(r => r.GeneratedSources) .Select(s => s.HintName).OrderBy(h => h).ToArray(); - CollectionAssert.AreEqual( - new[] { "AnimalJsonSubTypesConverter.g.cs", "JsonSubTypesAotConverters.g.cs" }, - hints); + CollectionAssert.AreEqual(SingleBaseExpectedHints, hints); } [Test] @@ -53,7 +56,7 @@ public void Generate_SingleBase_ConverterRoutesSubtypes() string? text = GeneratorDriverRunner.GetGeneratedSource(run, "AnimalJsonSubTypesConverter.g.cs"); Assert.That(text, Is.Not.Null); - StringAssert.Contains("class AnimalJsonSubTypesConverter : JsonConverter<", text!); + StringAssert.Contains("class AnimalJsonSubTypesConverter : JsonSubTypesAotValueConverterBase<", text!); StringAssert.Contains("\"cat\"", text!); StringAssert.Contains("\"dog\"", text!); } diff --git a/JsonSubTypes.Aot/JsonSubTypesGenerator.cs b/JsonSubTypes.Aot/JsonSubTypesGenerator.cs index 01a8e71..11d3e19 100644 --- a/JsonSubTypes.Aot/JsonSubTypesGenerator.cs +++ b/JsonSubTypes.Aot/JsonSubTypesGenerator.cs @@ -73,6 +73,7 @@ .. bases AssignUniqueConverterNames(generated); spc.AddSource("JsonSubTypesAotConverters.g.cs", SourceText.From(EmitRegistry(generated), System.Text.Encoding.UTF8)); + spc.AddSource("JsonSubTypesAotConverterBases.g.cs", SourceText.From(JsonSubTypesAotConverterBasesSource, System.Text.Encoding.UTF8)); foreach (BaseTypeInfo baseInfo in generated) { spc.AddSource($"{baseInfo.ConverterName}.g.cs", SourceText.From(EmitConverter(baseInfo), System.Text.Encoding.UTF8)); @@ -241,7 +242,7 @@ private static void ReportUnsupportedDiscriminator(SourceProductionContext spc, { cancellationToken.ThrowIfCancellationRequested(); spc.ReportDiagnostic(Diagnostic.Create(UnsupportedDiscriminator, - attr.ApplicationSyntaxReference?.GetSyntax().GetLocation(), + attr.ApplicationSyntaxReference?.GetSyntax(cancellationToken).GetLocation(), attr.ConstructorArguments[1].Type?.Name ?? "null", subtype.Name)); } @@ -605,14 +606,15 @@ public static class JsonSubTypesAotConverters """; } - private static string EmitConverter(BaseTypeInfo info) - { - string write = EmitWriteMethod(info); - string read = EmitReadMethod(info); - string selectType = EmitSelectTypeMethod(info); - string discriminatorWriter = info.IsValueMode ? EmitDiscriminatorWriter(info) : ""; - string baseHelpers = EmitBaseHelpers(info); - return $$""" + private const string MemberOpenBrace = " {\n"; + private const string MemberCloseBrace = " }"; + + // The shared skeleton: emitted once, exercised by every converter test. + // Presence-mode converters inherit JsonSubTypesAotConverterBase (its Write + // is the simplified resolver path); value-mode converters inherit + // JsonSubTypesAotValueConverterBase, which adds the discriminator-injection + // Write. Each generated converter only overrides the per-hierarchy members. + private const string JsonSubTypesAotConverterBasesSource = """ // #nullable enable using System; @@ -622,152 +624,324 @@ private static string EmitConverter(BaseTypeInfo info) namespace JsonSubTypes.Aot.Generated { - /// Generated by JsonSubTypes.Aot. Compiled converter for {{info.FullyQualifiedName}}. + /// Generated by JsonSubTypes.Aot. Shared converter skeleton: Read, the dynamic-subtype machinery and the base-object helpers live here once so every converter test exercises them. [global::System.CodeDom.Compiler.GeneratedCode("JsonSubTypes.Aot", "1.0.0")] - public sealed class {{info.ConverterName}} : JsonConverter<{{info.FullyQualifiedName}}> + public abstract class JsonSubTypesAotConverterBase : JsonConverter where T : class { - public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof({{info.FullyQualifiedName}}); + public sealed override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(T); - public override void Write(Utf8JsonWriter writer, {{info.FullyQualifiedName}} value, JsonSerializerOptions options) + public sealed override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - {{write}} - } - - public override {{info.FullyQualifiedName}}? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - {{read}} + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("Unrecognized token: " + reader.TokenType); + } + using JsonDocument document = JsonDocument.ParseValue(ref reader); + JsonElement root = document.RootElement; + Type target = SelectType(root, options); + if (target == typeof(T)) + { + return DeserializeBase(root, options); + } + return (T?)JsonSerializer.Deserialize(root.GetRawText(), options.GetTypeInfo(target)); } - private Type SelectType(JsonElement root, JsonSerializerOptions options) + // Presence-mode write: the base object is written through WriteBaseObject, + // registered subtypes through their own resolver-provided converters, and + // unknown types through the type info resolver. Value-mode converters + // override Write to inject the discriminator. + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { - {{selectType}} - } - - {{discriminatorWriter}} - {{baseHelpers}} - } - } - """; - } - - private static string EmitWriteMethod(BaseTypeInfo info) - { - if (!info.IsValueMode) - { - // presence mode is read-only: subtypes serialize through the resolver, base via WriteBaseObject - return $$""" if (value is null) { writer.WriteNullValue(); return; } Type runtimeType = value.GetType(); - if (runtimeType == typeof({{info.FullyQualifiedName}})) + if (runtimeType == typeof(T)) { - WriteBaseObject(writer, ({{info.FullyQualifiedName}})value, options); + WriteBaseObject(writer, value, options); return; } JsonSerializer.Serialize(writer, value, options.GetTypeInfo(runtimeType)); - """; - } + } - string payloadSelection = $$""" - Type runtimeType = value.GetType(); - string payload; - if (runtimeType == typeof({{info.FullyQualifiedName}})) + protected abstract Type SelectType(JsonElement root, JsonSerializerOptions options); + protected abstract T DeserializeBase(JsonElement root, JsonSerializerOptions options); + protected abstract void WriteBaseObject(Utf8JsonWriter writer, T value, JsonSerializerOptions options); + protected abstract bool TryWriteNestedObject(Utf8JsonWriter writer, T value, Type runtimeType, JsonSerializerOptions options); + + // Discriminator property name used by the dynamic write path; unused by + // presence-mode converters. + protected virtual string DiscriminatorPropertyName => ""; + + public System.Collections.Concurrent.ConcurrentDictionary DynamicSubtypes { get; } = new System.Collections.Concurrent.ConcurrentDictionary(); + private readonly System.Collections.Concurrent.ConcurrentDictionary _dynamicReverse = new System.Collections.Concurrent.ConcurrentDictionary(); + + public void RegisterDynamicSubtype(object discriminator, Type type) + { + DynamicSubtypes[discriminator] = type; + _dynamicReverse[type] = discriminator; // last registration wins, like the builder + } + + /// + /// Custom discriminator-to-type resolution hook, invoked after the static + /// registrations and DynamicSubtypes. Assign it to implement your own + /// name-based lookup (e.g. assembly scanning, a DI registry). The resolved + /// type must be resolvable by the TypeInfoResolver (in the source-gen context + /// for Native AOT). + /// + public Func? CustomTypeNameResolver { get; set; } + + protected static object? GetDiscriminatorKey(JsonElement discriminator) + { + switch (discriminator.ValueKind) { - if (IsRegistered(runtimeType)) + case JsonValueKind.String: + return discriminator.GetString(); + case JsonValueKind.Number when int.TryParse(discriminator.GetRawText(), out int keyInt): + return keyInt; + default: + return discriminator.GetRawText(); + } + } + + protected bool TryGetDynamicType(JsonElement discriminator, out Type? dynamicType) + { + switch (discriminator.ValueKind) + { + case JsonValueKind.String: + return DynamicSubtypes.TryGetValue(discriminator.GetString()!, out dynamicType); + case JsonValueKind.Number: + if (int.TryParse(discriminator.GetRawText(), out int dynamicInt) && DynamicSubtypes.TryGetValue(dynamicInt, out dynamicType)) + { + return true; + } + return DynamicSubtypes.TryGetValue(discriminator.GetRawText(), out dynamicType); + default: + return DynamicSubtypes.TryGetValue(discriminator.GetRawText(), out dynamicType); + } + } + + protected bool TryWriteDynamic(Utf8JsonWriter writer, T value, Type runtimeType, JsonSerializerOptions options) + { + if (_dynamicReverse.TryGetValue(runtimeType, out object? dynamicDiscriminator)) + { + writer.WriteStartObject(); + string dynamicDiscriminatorName = DiscriminatorPropertyName; + if (options.PropertyNamingPolicy != null) { - payload = SerializeBasePayload(({{info.FullyQualifiedName}})value, options); + dynamicDiscriminatorName = options.PropertyNamingPolicy.ConvertName(dynamicDiscriminatorName); } - else + writer.WritePropertyName(dynamicDiscriminatorName); + writer.WriteRawValue(JsonSerializer.Serialize(dynamicDiscriminator, options.GetTypeInfo(dynamicDiscriminator.GetType()))); + string payload = JsonSerializer.Serialize(value, options.GetTypeInfo(runtimeType)); + using JsonDocument payloadDocument = JsonDocument.Parse(payload); + foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject()) { - WriteBaseObject(writer, ({{info.FullyQualifiedName}})value, options); - return; + property.WriteTo(writer); } + writer.WriteEndObject(); + return true; } - else if (IsRegistered(runtimeType)) - { - payload = JsonSerializer.Serialize(value, options.GetTypeInfo(runtimeType)); - } - else if (TryWriteNestedObject(writer, value, runtimeType, options)) + return false; + } + + protected string SerializeBasePayload(T value, JsonSerializerOptions options) + { + using System.IO.MemoryStream stream = new System.IO.MemoryStream(); + using (Utf8JsonWriter writer = new Utf8JsonWriter(stream)) { - return; + WriteBaseObject(writer, value, options); } - else if (TryWriteDynamic(writer, value, runtimeType, options)) + return System.Text.Encoding.UTF8.GetString(stream.ToArray()); + } + + protected static bool TryGetProperty(JsonElement root, string name, JsonSerializerOptions options, out JsonElement value) + { + if (root.TryGetProperty(name, out value)) { - return; + return true; } - else + string? convertedName = options.PropertyNamingPolicy?.ConvertName(name); + if (convertedName != null && convertedName != name && root.TryGetProperty(convertedName, out value)) { - JsonSerializer.Serialize(writer, value, options.GetTypeInfo(runtimeType)); - return; + return true; } - """; - string order = info.AddDiscriminatorFirst - ? $$""" - writer.WritePropertyName(discriminatorName); - WriteDiscriminatorValue(writer, runtimeType, options); - foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject()) + if (options.PropertyNameCaseInsensitive) { - if (!property.NameEquals(discriminatorName)) + foreach (JsonProperty property in root.EnumerateObject().Where(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase))) { - property.WriteTo(writer); + value = property.Value; + return true; } } - """ - : $$""" - foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject()) + return false; + } + + protected static bool TryGetValueInJson(JsonElement root, string propertyName, JsonSerializerOptions options, out JsonElement value) + { + if (TryGetProperty(root, propertyName, options, out value)) + { + return true; + } + if (propertyName.Contains('.')) { - if (!property.NameEquals(discriminatorName)) + string[] segments = propertyName.Split('.'); + JsonElement current = root; + foreach (string segment in segments) { - property.WriteTo(writer); + if (!TryGetProperty(current, segment, options, out current)) + { + value = default; + return false; + } } + value = current; + return true; } - writer.WritePropertyName(discriminatorName); - WriteDiscriminatorValue(writer, runtimeType, options); - """; - return $$""" + return false; + } + } + + /// Generated by JsonSubTypes.Aot. Value-mode converter skeleton: adds the discriminator-injection Write on top of JsonSubTypesAotConverterBase. + [global::System.CodeDom.Compiler.GeneratedCode("JsonSubTypes.Aot", "1.0.0")] + public abstract class JsonSubTypesAotValueConverterBase : JsonSubTypesAotConverterBase where T : class + { + public sealed override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { if (value is null) { writer.WriteNullValue(); return; } - {{payloadSelection}} - using JsonDocument payloadDocument = JsonDocument.Parse(payload); - string discriminatorName = {{SymbolDisplay.FormatLiteral(info.DiscriminatorPropertyName!, quote: true)}}; - if (options.PropertyNamingPolicy != null) + + Type runtimeType = value.GetType(); + if (!TryGetPayload(writer, value, runtimeType, options, out string payload)) { - discriminatorName = options.PropertyNamingPolicy.ConvertName(discriminatorName); + return; } + + using JsonDocument payloadDocument = JsonDocument.Parse(payload); + string discriminatorName = options.PropertyNamingPolicy?.ConvertName(DiscriminatorPropertyName) ?? DiscriminatorPropertyName; writer.WriteStartObject(); - {{order}} + if (AddDiscriminatorFirst) + { + writer.WritePropertyName(discriminatorName); + WriteDiscriminatorValue(writer, runtimeType, options); + } + foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject().Where(p => !p.NameEquals(discriminatorName))) + { + property.WriteTo(writer); + } + if (!AddDiscriminatorFirst) + { + writer.WritePropertyName(discriminatorName); + WriteDiscriminatorValue(writer, runtimeType, options); + } writer.WriteEndObject(); - """; - } + } - private static string EmitReadMethod(BaseTypeInfo info) - { - return $$""" - if (reader.TokenType == JsonTokenType.Null) + // Resolves the serialized payload of the runtime type. Returns false + // when the value was already written directly (base fallback, nested + // chain, dynamic subtype or unregistered runtime type). + private bool TryGetPayload(Utf8JsonWriter writer, T value, Type runtimeType, JsonSerializerOptions options, out string payload) + { + payload = ""; + if (runtimeType == typeof(T)) { - return null; + if (IsRegistered(runtimeType)) + { + payload = SerializeBasePayload(value, options); + return true; + } + WriteBaseObject(writer, value, options); + return false; } - if (reader.TokenType != JsonTokenType.StartObject) + if (IsRegistered(runtimeType)) { - throw new JsonException("Unrecognized token: " + reader.TokenType); + payload = JsonSerializer.Serialize(value, options.GetTypeInfo(runtimeType)); + return true; } - using JsonDocument document = JsonDocument.ParseValue(ref reader); - JsonElement root = document.RootElement; - Type target = SelectType(root, options); - if (target == typeof({{info.FullyQualifiedName}})) + if (TryWriteNestedObject(writer, value, runtimeType, options)) { - return DeserializeBase(root, options); + return false; + } + if (TryWriteDynamic(writer, value, runtimeType, options)) + { + return false; } - return ({{info.FullyQualifiedName}}?)JsonSerializer.Deserialize(root.GetRawText(), options.GetTypeInfo(target)); + JsonSerializer.Serialize(writer, value, options.GetTypeInfo(runtimeType)); + return false; + } + + protected virtual System.Collections.Generic.Dictionary> DiscriminatorWriters { get; } = new System.Collections.Generic.Dictionary>(); + + protected bool IsRegistered(Type runtimeType) + { + return DiscriminatorWriters.ContainsKey(runtimeType); + } + + protected virtual bool AddDiscriminatorFirst => true; + + private void WriteDiscriminatorValue(Utf8JsonWriter writer, Type runtimeType, JsonSerializerOptions options) + { + if (DiscriminatorWriters.TryGetValue(runtimeType, out System.Action? write)) + { + write(writer, options); + return; + } + throw new JsonException("Impossible to serialize type: " + runtimeType.FullName + " because there is no registered mapping for the discriminator property"); + } + } + } + """; + + private static string EmitConverter(BaseTypeInfo info) + { + string usingLinq = info.IsValueMode ? "" : "using System.Linq;\n"; + string baseClass = info.IsValueMode + ? $"JsonSubTypesAotValueConverterBase<{info.FullyQualifiedName}>" + : $"JsonSubTypesAotConverterBase<{info.FullyQualifiedName}>"; + string valueModeMembers = info.IsValueMode ? EmitValueModeMembers(info) : ""; + + return $$""" + // + #nullable enable + using System; + {{usingLinq}}using System.Text.Json; + using System.Text.Json.Serialization; + + namespace JsonSubTypes.Aot.Generated + { + /// Generated by JsonSubTypes.Aot. Compiled converter for {{info.FullyQualifiedName}}. + [global::System.CodeDom.Compiler.GeneratedCode("JsonSubTypes.Aot", "1.0.0")] + public sealed class {{info.ConverterName}} : {{baseClass}} + { + {{valueModeMembers}}{{EmitSelectTypeMethod(info)}} + + {{EmitWriteBaseObject(info)}} + {{EmitDeserializeBase(info)}} + {{EmitTryWriteNestedObject(info)}} + } + } """; } + private static string EmitValueModeMembers(BaseTypeInfo info) + { + string discriminatorProperty = + $" private const string DiscriminatorPropertyNameValue = {SymbolDisplay.FormatLiteral(info.DiscriminatorPropertyName!, quote: true)};\n" + + " protected override string DiscriminatorPropertyName => DiscriminatorPropertyNameValue;\n\n"; + string addDiscriminatorFirst = info.AddDiscriminatorFirst ? "" : " protected override bool AddDiscriminatorFirst => false;\n\n"; + string discriminatorWriters = EmitDiscriminatorWriters(info) + "\n\n"; + return discriminatorProperty + addDiscriminatorFirst + discriminatorWriters; + } + private static string EmitSelectTypeMethod(BaseTypeInfo info) { return info.IsValueMode ? EmitValueModeSelectType(info) : EmitPresenceModeSelectType(info); @@ -775,82 +949,85 @@ private static string EmitSelectTypeMethod(BaseTypeInfo info) private static string EmitValueModeSelectType(BaseTypeInfo info) { - string? nullCases = null; List nulls = [.. info.Subtypes.Where(s => s.DiscriminatorKind == "null")]; - if (nulls.Count > 0) - { - // dedupe so consecutive null registrations do not emit unreachable returns - nullCases = " " + string.Join("\n" + " ", - nulls.Select(r => r.FullyQualifiedName).Distinct() - .Select(t => $"return typeof({t});")); - } + string nullReturns = string.Join("\n", + nulls.Select(r => r.FullyQualifiedName).Distinct() + .Select(t => $" return typeof({t});")); List strings = [.. info.Subtypes.Where(s => s.DiscriminatorKind == "string")]; List enums = [.. info.Subtypes.Where(s => s.DiscriminatorKind == "enum")]; - string? stringCases = null; + string stringCases = string.Join("\n", + strings.Select(r => $"case {r.DiscriminatorLiteral}: return typeof({r.FullyQualifiedName});") + .Concat(enums.Select(r => $"case {SymbolDisplay.FormatLiteral(r.EnumMemberName!, quote: true)}: return typeof({r.FullyQualifiedName});"))); + + List ints = [.. info.Subtypes.Where(s => s.DiscriminatorKind == "int")]; + string numberCases = string.Join("\n", + ints.Select(r => $"case {SymbolDisplay.FormatLiteral(r.DiscriminatorLiteral, quote: true)}: return typeof({r.FullyQualifiedName});") + .Concat(enums.Select(r => $"case {SymbolDisplay.FormatLiteral(r.EnumUnderlyingValue!, quote: true)}: return typeof({r.FullyQualifiedName});"))); + + string switchBody = ""; + if (nulls.Count > 0) + { + switchBody += " case JsonValueKind.Null:\n" + nullReturns + "\n"; + } if (strings.Count + enums.Count > 0) { - IEnumerable cases = strings.Select(r => $"case {r.DiscriminatorLiteral}: return typeof({r.FullyQualifiedName});") - .Concat(enums.Select(r => $"case {SymbolDisplay.FormatLiteral(r.EnumMemberName!, quote: true)}: return typeof({r.FullyQualifiedName});")); - stringCases = " " + string.Join("\n" + " ", cases); + switchBody += " case JsonValueKind.String:\n" + + " switch (discriminator.GetString())\n" + + " {\n" + + " " + stringCases.Replace("\n", "\n ") + "\n" + + " }\n" + + " break;\n"; } - - List ints = [.. info.Subtypes.Where(s => s.DiscriminatorKind == "int")]; - string? numberCases = null; if (ints.Count + enums.Count > 0) { - IEnumerable cases = ints.Select(r => $"case {SymbolDisplay.FormatLiteral(r.DiscriminatorLiteral, quote: true)}: return typeof({r.FullyQualifiedName});") - .Concat(enums.Select(r => $"case {SymbolDisplay.FormatLiteral(r.EnumUnderlyingValue!, quote: true)}: return typeof({r.FullyQualifiedName});")); - numberCases = " " + string.Join("\n" + " ", cases); + switchBody += " case JsonValueKind.Number:\n" + + " switch (discriminator.GetRawText())\n" + + " {\n" + + " " + numberCases.Replace("\n", "\n ") + "\n" + + " }\n" + + " break;\n"; } - string nullBlock = nullCases == null ? "" : $$""" - if (discriminator.ValueKind == JsonValueKind.Null) - { - {{nullCases}} - } - """; - string stringBlock = stringCases == null ? "" : $$""" - if (discriminator.ValueKind == JsonValueKind.String) - { - switch (discriminator.GetString()) - { - {{stringCases}} - } - } - """; - string numberBlock = numberCases == null ? "" : $$""" - if (discriminator.ValueKind == JsonValueKind.Number) - { - switch (discriminator.GetRawText()) - { - {{numberCases}} - } - } - """; - - return $$""" - if (TryGetValueInJson(root, {{SymbolDisplay.FormatLiteral(info.DiscriminatorPropertyName!, quote: true)}}, options, out JsonElement discriminator)) - { - {{nullBlock}} - {{stringBlock}} - {{numberBlock}} - if (TryGetDynamicType(discriminator, out Type? dynamicType)) - { - return dynamicType!; - } - if (CustomTypeNameResolver is not null) - { - Type? customType = CustomTypeNameResolver(GetDiscriminatorKey(discriminator)); - if (customType != null) - { - return customType; - } - } - return typeof({{info.FallbackType}}); - } - return typeof({{info.FallbackType}}); - """; + string selectType = + " protected override Type SelectType(JsonElement root, JsonSerializerOptions options)\n" + + MemberOpenBrace + + " if (!TryGetValueInJson(root, DiscriminatorPropertyNameValue, options, out JsonElement discriminator))\n" + + " {\n" + + " return typeof(" + info.FallbackType + ");\n" + + " }\n" + + " Type? staticType = ResolveStaticType(discriminator);\n" + + " if (staticType != null)\n" + + " {\n" + + " return staticType;\n" + + " }\n" + + " if (TryGetDynamicType(discriminator, out Type? dynamicType))\n" + + " {\n" + + " return dynamicType!;\n" + + " }\n" + + " if (CustomTypeNameResolver is not null)\n" + + " {\n" + + " Type? customType = CustomTypeNameResolver(GetDiscriminatorKey(discriminator));\n" + + " if (customType != null)\n" + + " {\n" + + " return customType;\n" + + " }\n" + + " }\n" + + " return typeof(" + info.FallbackType + ");\n" + + MemberCloseBrace; + + string resolveStaticType = + "\n\n" + + " private static Type? ResolveStaticType(JsonElement discriminator)\n" + + MemberOpenBrace + + " switch (discriminator.ValueKind)\n" + + " {\n" + + switchBody + + " }\n" + + " return null;\n" + + MemberCloseBrace; + + return selectType + resolveStaticType; } private static string EmitPresenceModeSelectType(BaseTypeInfo info) @@ -859,37 +1036,34 @@ private static string EmitPresenceModeSelectType(BaseTypeInfo info) foreach (PropertyPresenceRegistration reg in info.PropertyPresences) { checks.Add(reg.StopLookupOnMatch - ? $$""" - if (root.TryGetProperty({{SymbolDisplay.FormatLiteral(reg.PropertyName, quote: true)}}, out _)) - { - return typeof({{reg.FullyQualifiedName}}); - } - """ - : $$""" - if (root.TryGetProperty({{SymbolDisplay.FormatLiteral(reg.PropertyName, quote: true)}}, out _)) - { - matches.Add(typeof({{reg.FullyQualifiedName}})); - } - """); + ? " if (root.TryGetProperty(" + SymbolDisplay.FormatLiteral(reg.PropertyName, quote: true) + ", out _))\n" + + MemberOpenBrace + + " return typeof(" + reg.FullyQualifiedName + ");\n" + + MemberCloseBrace + : " if (root.TryGetProperty(" + SymbolDisplay.FormatLiteral(reg.PropertyName, quote: true) + ", out _))\n" + + MemberOpenBrace + + " matches.Add(typeof(" + reg.FullyQualifiedName + "));\n" + + MemberCloseBrace); } string presenceChecks = string.Join("\n", checks); - return $$""" - System.Collections.Generic.HashSet matches = new System.Collections.Generic.HashSet(); - {{presenceChecks}} - if (matches.Count == 1) - { - return matches.First(); - } - if (matches.Count > 1) - { - throw new JsonException("Ambiguous type resolution, expected only one type but got: " + string.Join(", ", matches.Select(t => t.FullName))); - } - return typeof({{info.FallbackType}}); - """; + return " protected override Type SelectType(JsonElement root, JsonSerializerOptions options)\n" + + MemberOpenBrace + + " System.Collections.Generic.HashSet matches = new System.Collections.Generic.HashSet();\n" + + presenceChecks + "\n" + + " if (matches.Count == 1)\n" + + " {\n" + + " return matches.First();\n" + + " }\n" + + " if (matches.Count > 1)\n" + + " {\n" + + " throw new JsonException(\"Ambiguous type resolution, expected only one type but got: \" + string.Join(\", \", matches.Select(t => t.FullName)));\n" + + " }\n" + + " return typeof(" + info.FallbackType + ");\n" + + MemberCloseBrace; } - private static string EmitDiscriminatorWriter(BaseTypeInfo info) + private static string EmitDiscriminatorWriters(BaseTypeInfo info) { Dictionary byType = new(); foreach (SubtypeRegistration reg in info.Subtypes) @@ -907,110 +1081,13 @@ private static string EmitDiscriminatorWriter(BaseTypeInfo info) "enum" => $"writer.WriteRawValue(JsonSerializer.Serialize({reg.EnumReference}, options.GetTypeInfo(typeof({reg.EnumTypeName}))));", _ => "writer.WriteNullValue();" }; - dictionaryEntries.Add($$""" - [typeof({{reg.FullyQualifiedName}})] = static (writer, options) => {{value.TrimEnd(';')}} - """); + dictionaryEntries.Add($" [typeof({reg.FullyQualifiedName})] = static (writer, options) => {value.TrimEnd(';')}"); } - return $$""" - private static readonly System.Collections.Generic.Dictionary> DiscriminatorWriters = new System.Collections.Generic.Dictionary> - { - {{string.Join(",\n", dictionaryEntries)}} - }; - - private static bool IsRegistered(Type runtimeType) - { - return DiscriminatorWriters.ContainsKey(runtimeType); - } - - private static void WriteDiscriminatorValue(Utf8JsonWriter writer, Type runtimeType, JsonSerializerOptions options) - { - if (DiscriminatorWriters.TryGetValue(runtimeType, out System.Action? write)) - { - write(writer, options); - return; - } - throw new JsonException("Impossible to serialize type: " + runtimeType.FullName + " because there is no registered mapping for the discriminator property"); - } - - public readonly System.Collections.Concurrent.ConcurrentDictionary DynamicSubtypes = new System.Collections.Concurrent.ConcurrentDictionary(); - private readonly System.Collections.Concurrent.ConcurrentDictionary _dynamicReverse = new System.Collections.Concurrent.ConcurrentDictionary(); - - public void RegisterDynamicSubtype(object discriminator, Type type) - { - DynamicSubtypes[discriminator] = type; - _dynamicReverse[type] = discriminator; // last registration wins, like the builder - } - - /// - /// Custom discriminator-to-type resolution hook, invoked after the static - /// registrations and DynamicSubtypes. Assign it to implement your own - /// name-based lookup (e.g. assembly scanning, a DI registry). The resolved - /// type must be resolvable by the TypeInfoResolver (in the source-gen context - /// for Native AOT). - /// - public Func? CustomTypeNameResolver { get; set; } - - private static object? GetDiscriminatorKey(JsonElement discriminator) - { - switch (discriminator.ValueKind) - { - case JsonValueKind.String: - return discriminator.GetString(); - case JsonValueKind.Number when int.TryParse(discriminator.GetRawText(), out int keyInt): - return keyInt; - default: - return discriminator.GetRawText(); - } - } - - private bool TryGetDynamicType(JsonElement discriminator, out Type? dynamicType) - { - switch (discriminator.ValueKind) - { - case JsonValueKind.String: - return DynamicSubtypes.TryGetValue(discriminator.GetString()!, out dynamicType); - case JsonValueKind.Number: - if (int.TryParse(discriminator.GetRawText(), out int dynamicInt) && DynamicSubtypes.TryGetValue(dynamicInt, out dynamicType)) - { - return true; - } - return DynamicSubtypes.TryGetValue(discriminator.GetRawText(), out dynamicType); - default: - return DynamicSubtypes.TryGetValue(discriminator.GetRawText(), out dynamicType); - } - } - - private static bool TryWriteNestedObject(Utf8JsonWriter writer, {{info.FullyQualifiedName}} value, Type runtimeType, JsonSerializerOptions options) - { - {{EmitNestedCases(info)}} - return false; - } - - private bool TryWriteDynamic(Utf8JsonWriter writer, {{info.FullyQualifiedName}} value, Type runtimeType, JsonSerializerOptions options) - { - if (_dynamicReverse.TryGetValue(runtimeType, out object? dynamicDiscriminator)) - { - writer.WriteStartObject(); - string dynamicDiscriminatorName = {{SymbolDisplay.FormatLiteral(info.DiscriminatorPropertyName!, quote: true)}}; - if (options.PropertyNamingPolicy != null) - { - dynamicDiscriminatorName = options.PropertyNamingPolicy.ConvertName(dynamicDiscriminatorName); - } - writer.WritePropertyName(dynamicDiscriminatorName); - writer.WriteRawValue(JsonSerializer.Serialize(dynamicDiscriminator, options.GetTypeInfo(dynamicDiscriminator.GetType()))); - string payload = JsonSerializer.Serialize(value, options.GetTypeInfo(runtimeType)); - using JsonDocument payloadDocument = JsonDocument.Parse(payload); - foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject()) - { - property.WriteTo(writer); - } - writer.WriteEndObject(); - return true; - } - return false; - } - """; + return " protected override System.Collections.Generic.Dictionary> DiscriminatorWriters { get; } = new System.Collections.Generic.Dictionary>\n" + + MemberOpenBrace + + string.Join(",\n", dictionaryEntries) + "\n" + + " };"; } private static string EmitNestedCases(BaseTypeInfo info) @@ -1026,7 +1103,10 @@ private static string EmitNestedCases(BaseTypeInfo info) List discLines = []; foreach (ChainEntry entry in nested.Chain) { - discLines.Add($" writer.WritePropertyName({SymbolDisplay.FormatLiteral(entry.DiscriminatorName, quote: true)});"); + string discriminatorName = entry.DiscriminatorName == info.DiscriminatorPropertyName + ? "DiscriminatorPropertyNameValue" + : SymbolDisplay.FormatLiteral(entry.DiscriminatorName, quote: true); + discLines.Add($" writer.WritePropertyName({discriminatorName});"); discLines.Add($" {EmitDiscriminatorValueStatement(entry.Discriminator)}"); } string payload = $$""" @@ -1063,7 +1143,7 @@ private static string EmitDiscriminatorValueStatement(SubtypeRegistration reg) }; } - private static string EmitBaseHelpers(BaseTypeInfo info) + private static string EmitWriteBaseObject(BaseTypeInfo info) { List writeProperties = []; foreach (BaseProperty prop in info.Properties) @@ -1085,6 +1165,33 @@ private static string EmitBaseHelpers(BaseTypeInfo info) """); } + string body = string.Join("\n", writeProperties); + return " protected override void WriteBaseObject(Utf8JsonWriter writer, " + info.FullyQualifiedName + " value, JsonSerializerOptions options)\n" + + MemberOpenBrace + + " writer.WriteStartObject();\n" + + body + "\n" + + " writer.WriteEndObject();\n" + + MemberCloseBrace; + } + + private static string EmitDeserializeBase(BaseTypeInfo info) + { + if (info.BaseIsAbstractOrInterface) + { + return " protected override " + info.FullyQualifiedName + " DeserializeBase(JsonElement root, JsonSerializerOptions options)\n" + + MemberOpenBrace + + " throw new JsonException(\"Could not create an instance of type " + info.FullyQualifiedName + ". Type is an interface or abstract class and cannot be instantiated.\");\n" + + MemberCloseBrace; + } + + if (!info.BaseHasParameterlessConstructor) + { + return " protected override " + info.FullyQualifiedName + " DeserializeBase(JsonElement root, JsonSerializerOptions options)\n" + + MemberOpenBrace + + " throw new JsonException(\"Could not create an instance of type " + info.FullyQualifiedName + ": a parameterless constructor is required to fall back to the base type.\");\n" + + MemberCloseBrace; + } + List readProperties = []; foreach (BaseProperty prop in info.Properties) { @@ -1101,106 +1208,23 @@ private static string EmitBaseHelpers(BaseTypeInfo info) """); } - string deserializeBase; - if (info.BaseIsAbstractOrInterface) - { - deserializeBase = $$""" - private static {{info.FullyQualifiedName}} DeserializeBase(JsonElement root, JsonSerializerOptions options) - { - throw new JsonException("Could not create an instance of type {{info.FullyQualifiedName}}. Type is an interface or abstract class and cannot be instantiated."); - } - """; - } - else if (!info.BaseHasParameterlessConstructor) - { - deserializeBase = $$""" - private static {{info.FullyQualifiedName}} DeserializeBase(JsonElement root, JsonSerializerOptions options) - { - throw new JsonException("Could not create an instance of type {{info.FullyQualifiedName}}: a parameterless constructor is required to fall back to the base type."); - } - """; - } - else - { - deserializeBase = $$""" - private static {{info.FullyQualifiedName}} DeserializeBase(JsonElement root, JsonSerializerOptions options) - { - {{info.FullyQualifiedName}} instance = new {{info.FullyQualifiedName}}(); - {{string.Join("\n", readProperties)}} - return instance; - } - """; - } - - return $$""" - private static string SerializeBasePayload({{info.FullyQualifiedName}} value, JsonSerializerOptions options) - { - using System.IO.MemoryStream stream = new System.IO.MemoryStream(); - using (Utf8JsonWriter writer = new Utf8JsonWriter(stream)) - { - WriteBaseObject(writer, value, options); - } - return System.Text.Encoding.UTF8.GetString(stream.ToArray()); - } - - private static void WriteBaseObject(Utf8JsonWriter writer, {{info.FullyQualifiedName}} value, JsonSerializerOptions options) - { - writer.WriteStartObject(); - {{string.Join("\n", writeProperties)}} - writer.WriteEndObject(); - } - - {{deserializeBase}} - - private static bool TryGetProperty(JsonElement root, string name, JsonSerializerOptions options, out JsonElement value) - { - if (root.TryGetProperty(name, out value)) - { - return true; - } - string? convertedName = options.PropertyNamingPolicy?.ConvertName(name); - if (convertedName != null && convertedName != name && root.TryGetProperty(convertedName, out value)) - { - return true; - } - if (options.PropertyNameCaseInsensitive) - { - foreach (JsonProperty property in root.EnumerateObject()) - { - if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase)) - { - value = property.Value; - return true; - } - } - } - return false; - } + string body = string.Join("\n", readProperties); + return " protected override " + info.FullyQualifiedName + " DeserializeBase(JsonElement root, JsonSerializerOptions options)\n" + + MemberOpenBrace + + " " + info.FullyQualifiedName + " instance = new " + info.FullyQualifiedName + "();\n" + + body + "\n" + + " return instance;\n" + + MemberCloseBrace; + } - private static bool TryGetValueInJson(JsonElement root, string propertyName, JsonSerializerOptions options, out JsonElement value) - { - if (TryGetProperty(root, propertyName, options, out value)) - { - return true; - } - if (propertyName.IndexOf('.') >= 0) - { - string[] segments = propertyName.Split('.'); - JsonElement current = root; - foreach (string segment in segments) - { - if (!TryGetProperty(current, segment, options, out current)) - { - value = default; - return false; - } - } - value = current; - return true; - } - return false; - } - """; + private static string EmitTryWriteNestedObject(BaseTypeInfo info) + { + string nestedCases = EmitNestedCases(info); + return " protected override bool TryWriteNestedObject(Utf8JsonWriter writer, " + info.FullyQualifiedName + " value, Type runtimeType, JsonSerializerOptions options)\n" + + MemberOpenBrace + + (nestedCases.Length == 0 ? "" : nestedCases + "\n") + + " return false;\n" + + MemberCloseBrace; } } } diff --git a/JsonSubTypes/JsonSubtypes.cs b/JsonSubTypes/JsonSubtypes.cs index 9adc851..0e25612 100644 --- a/JsonSubTypes/JsonSubtypes.cs +++ b/JsonSubTypes/JsonSubtypes.cs @@ -312,9 +312,11 @@ private Type GetTypeByPropertyPresence(JObject jObject, Type parentType) var knownSubTypeAttributes = GetTypesByPropertyPresence(parentType); HashSet typesFound = new HashSet(); + foreach (TypeWithPropertyMatchingAttributes knownTypeItem in knownSubTypeAttributes) { Type matchingKnownType = null; + if (TryGetValueInJson(jObject, knownTypeItem.JsonPropertyName, out JToken _)) { matchingKnownType = knownTypeItem.Type; @@ -328,34 +330,24 @@ private Type GetTypeByPropertyPresence(JObject jObject, Type parentType) } } - if (matchingKnownType != null) - { - if (knownTypeItem.StopLookupOnMatch) - { - return knownTypeItem.Type; - } - typesFound.Add(matchingKnownType); - } - } + if (matchingKnownType == null) + continue; - Type result = null; - bool ambiguous = false; - foreach (Type matchingType in typesFound) - { - if (result != null) - { - ambiguous = true; - break; - } - result = matchingType; + if (knownTypeItem.StopLookupOnMatch) + return knownTypeItem.Type; + + typesFound.Add(matchingKnownType); } - if (ambiguous) + if (typesFound.Count > 1) { - throw new JsonSerializationException("Ambiguous type resolution, expected only one type but got: " + String.Join(", ", typesFound.Select(t => t.FullName).ToArray())); + throw new JsonSerializationException( + "Ambiguous type resolution, expected only one type but got: " + + string.Join(", ", typesFound.Select(t => t.FullName).ToArray()) + ); } - return result; + return typesFound.SingleOrDefault(); } internal virtual List GetTypesByPropertyPresence(Type parentType)