From d9354e93e7900f1832382e4666659b7587841956 Mon Sep 17 00:00:00 2001 From: manuc66 Date: Thu, 13 Aug 2026 22:58:15 +0200 Subject: [PATCH 1/4] Emit a shared converter base once so the common skeleton is covered by every test Each converter used to repeat a ~290-line skeleton identical across the hierarchy; only ~15 lines were specific (T, discriminator table, base properties). Each converter test covered only a slice (12-37%), dragging generated-code coverage to ~24%. The generator now emits JsonSubTypesAotConverterBase (Read, dynamic-subtype machinery, base-object helpers, abstract SelectType/DeserializeBase/WriteBaseObject/ TryWriteNestedObject) and JsonSubTypesAotValueConverterBase (the discriminator- injection Write, IsRegistered, WriteDiscriminatorValue, virtual DiscriminatorWriters) once per compilation. Converters become ~30-line subclasses overriding only the hierarchy-specific members. Presence-mode converters inherit the first base only. Both bases are generated (not library code): JsonSubTypes.Aot ships only as an analyzer (no runtime assembly), and generated bases keep the converter output self-contained and the golden master faithful to the generator output. Verified: 22 generator-driver tests (golden master regenerated), 73 Aot.Tests parity tests on net8.0 and net10.0. --- .../AnimalJsonSubTypesConverter.cs | 256 +------- .../DottedGadgetJsonSubTypesConverter.cs | 254 +------- .../GadgetJsonSubTypesConverter.cs | 254 +------- .../GameJsonSubTypesConverter.cs | 252 +------ .../JsonSubTypesAotConverterBases.cs | 288 ++++++++ .../PayloadJsonSubTypesConverter.cs | 258 +------- .../PersonJsonSubTypesConverter.cs | 117 +--- .../GeneratorDriverTests.cs | 4 +- JsonSubTypes.Aot/JsonSubTypesGenerator.cs | 613 +++++++++--------- 9 files changed, 700 insertions(+), 1596 deletions(-) create mode 100644 JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/JsonSubTypesAotConverterBases.cs 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..8587515 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,94 +1,23 @@ #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); + protected override string DiscriminatorPropertyName => "type"; - 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)) - { - return; - } - else - { - JsonSerializer.Serialize(writer, value, options.GetTypeInfo(runtimeType)); - return; - } - using JsonDocument payloadDocument = JsonDocument.Parse(payload); - string discriminatorName = "type"; - if (options.PropertyNamingPolicy != 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)) - { - property.WriteTo(writer); - } - } - writer.WriteEndObject(); - } + [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) + }; - public override global::JsonSubTypes.Aot.Generated.TestDomain.Animal? 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(global::JsonSubTypes.Aot.Generated.TestDomain.Animal)) - { - 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) + protected override Type SelectType(JsonElement root, JsonSerializerOptions options) { if (TryGetValueInJson(root, "type", options, out JsonElement discriminator)) { @@ -124,117 +53,9 @@ private Type SelectType(JsonElement root, JsonSerializerOptions options) return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Animal); } - 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 +63,21 @@ 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; - } - string? convertedName = options.PropertyNamingPolicy?.ConvertName(name); - if (convertedName != null && convertedName != name && root.TryGetProperty(convertedName, out value)) - { - return true; + return instance; } - if (options.PropertyNameCaseInsensitive) + protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Animal value, Type runtimeType, JsonSerializerOptions options) { - 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/DottedGadgetJsonSubTypesConverter.cs b/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/DottedGadgetJsonSubTypesConverter.cs index 099e419..46e4533 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,94 +1,22 @@ #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); + protected override string DiscriminatorPropertyName => "nested.type"; - 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)) - { - return; - } - else - { - JsonSerializer.Serialize(writer, value, options.GetTypeInfo(runtimeType)); - return; - } - using JsonDocument payloadDocument = JsonDocument.Parse(payload); - string discriminatorName = "nested.type"; - if (options.PropertyNamingPolicy != 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)) - { - property.WriteTo(writer); - } - } - writer.WriteEndObject(); - } + [typeof(global::JsonSubTypes.Aot.Generated.TestDomain.DottedElectronic)] = static (writer, options) => writer.WriteStringValue("electronic") + }; - public override global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget? 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(global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget)) - { - 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) + protected override Type SelectType(JsonElement root, JsonSerializerOptions options) { if (TryGetValueInJson(root, "nested.type", options, out JsonElement discriminator)) { @@ -118,116 +46,9 @@ private Type SelectType(JsonElement root, JsonSerializerOptions options) 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 +56,21 @@ 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; + return instance; } - if (options.PropertyNameCaseInsensitive) + protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget value, Type runtimeType, JsonSerializerOptions options) { - 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 false; } - 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/GadgetJsonSubTypesConverter.cs b/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/GadgetJsonSubTypesConverter.cs index 494fa45..76f5f9f 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,94 +1,22 @@ #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); + protected override string DiscriminatorPropertyName => "kind"; - 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)) - { - return; - } - else if (TryWriteDynamic(writer, value, runtimeType, options)) - { - return; - } - else - { - JsonSerializer.Serialize(writer, value, options.GetTypeInfo(runtimeType)); - return; - } - using JsonDocument payloadDocument = JsonDocument.Parse(payload); - string discriminatorName = "kind"; - if (options.PropertyNamingPolicy != 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)) - { - property.WriteTo(writer); - } - } - writer.WriteEndObject(); - } + [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)))) + }; - public override global::JsonSubTypes.Aot.Generated.TestDomain.Gadget? 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(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) + protected override Type SelectType(JsonElement root, JsonSerializerOptions options) { if (TryGetValueInJson(root, "kind", options, out JsonElement discriminator)) { @@ -124,116 +52,9 @@ private Type SelectType(JsonElement root, JsonSerializerOptions options) 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 +62,21 @@ 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; + return instance; } - if (options.PropertyNameCaseInsensitive) + protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Gadget value, Type runtimeType, JsonSerializerOptions options) { - 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 false; } - 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/GameJsonSubTypesConverter.cs b/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/GameJsonSubTypesConverter.cs index 378086b..294f834 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,94 +1,23 @@ #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); + protected override string DiscriminatorPropertyName => "$GameKind"; - 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)) - { - return; - } - else - { - JsonSerializer.Serialize(writer, value, options.GetTypeInfo(runtimeType)); - return; - } - using JsonDocument payloadDocument = JsonDocument.Parse(payload); - string discriminatorName = "$GameKind"; - if (options.PropertyNamingPolicy != 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)) - { - property.WriteTo(writer); - } - } - writer.WriteEndObject(); - } + [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)))) + }; - public override global::JsonSubTypes.Aot.Generated.TestDomain.Game? 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(global::JsonSubTypes.Aot.Generated.TestDomain.Game)) - { - 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) + protected override Type SelectType(JsonElement root, JsonSerializerOptions options) { if (TryGetValueInJson(root, "$GameKind", options, out JsonElement discriminator)) { @@ -126,175 +55,22 @@ private Type SelectType(JsonElement root, JsonSerializerOptions options) 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)) + protected override global::JsonSubTypes.Aot.Generated.TestDomain.Game DeserializeBase(JsonElement root, JsonSerializerOptions options) { - WriteBaseObject(writer, value, options); - } - return System.Text.Encoding.UTF8.GetString(stream.ToArray()); - } + global::JsonSubTypes.Aot.Generated.TestDomain.Game instance = new global::JsonSubTypes.Aot.Generated.TestDomain.Game(); - 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; + return instance; } - if (options.PropertyNameCaseInsensitive) + protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Game value, Type runtimeType, JsonSerializerOptions options) { - 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 false; } - 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/JsonSubTypesAotConverterBases.cs b/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/JsonSubTypesAotConverterBases.cs new file mode 100644 index 0000000..6f0e11c --- /dev/null +++ b/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/JsonSubTypesAotConverterBases.cs @@ -0,0 +1,288 @@ +#nullable enable +using System; +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 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; } + + 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()) + { + if (string.Equals(property.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.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; + } + } + + /// 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(); + string payload; + if (runtimeType == typeof(T)) + { + if (IsRegistered(runtimeType)) + { + payload = SerializeBasePayload(value, options); + } + else + { + WriteBaseObject(writer, 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)) + { + return; + } + else + { + JsonSerializer.Serialize(writer, value, options.GetTypeInfo(runtimeType)); + return; + } + using JsonDocument payloadDocument = JsonDocument.Parse(payload); + string discriminatorName = DiscriminatorPropertyName; + if (options.PropertyNamingPolicy != null) + { + discriminatorName = options.PropertyNamingPolicy.ConvertName(discriminatorName); + } + writer.WriteStartObject(); + if (AddDiscriminatorFirst) + { + writer.WritePropertyName(discriminatorName); + WriteDiscriminatorValue(writer, runtimeType, options); + foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject()) + { + if (!property.NameEquals(discriminatorName)) + { + property.WriteTo(writer); + } + } + } + else + { + foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject()) + { + if (!property.NameEquals(discriminatorName)) + { + property.WriteTo(writer); + } + } + writer.WritePropertyName(discriminatorName); + WriteDiscriminatorValue(writer, runtimeType, options); + } + writer.WriteEndObject(); + } + + 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..b39ebee 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,94 +1,23 @@ #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); + protected override string DiscriminatorPropertyName => "$PayloadKind"; - 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)) - { - return; - } - else if (TryWriteDynamic(writer, value, runtimeType, options)) - { - return; - } - else - { - JsonSerializer.Serialize(writer, value, options.GetTypeInfo(runtimeType)); - return; - } - using JsonDocument payloadDocument = JsonDocument.Parse(payload); - string discriminatorName = "$PayloadKind"; - if (options.PropertyNamingPolicy != 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)) - { - property.WriteTo(writer); - } - } - writer.WriteEndObject(); - } + [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)))) + }; - public override global::JsonSubTypes.Aot.Generated.TestDomain.Payload? 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(global::JsonSubTypes.Aot.Generated.TestDomain.Payload)) - { - 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) + protected override Type SelectType(JsonElement root, JsonSerializerOptions options) { if (TryGetValueInJson(root, "$PayloadKind", options, out JsonElement discriminator)) { @@ -126,77 +55,20 @@ private Type SelectType(JsonElement root, JsonSerializerOptions options) 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(); @@ -229,103 +101,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 false; } - 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; - } } } \ 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..15c8cda 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,47 +7,10 @@ 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) - { - 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)); - } - - public override global::JsonSubTypes.Aot.Generated.TestDomain.Person? 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(global::JsonSubTypes.Aot.Generated.TestDomain.Person)) - { - return DeserializeBase(root, options); - } - return (global::JsonSubTypes.Aot.Generated.TestDomain.Person?)JsonSerializer.Deserialize(root.GetRawText(), options.GetTypeInfo(target)); - } - - private Type SelectType(JsonElement root, JsonSerializerOptions options) + protected override Type SelectType(JsonElement root, JsonSerializerOptions options) { System.Collections.Generic.HashSet matches = new System.Collections.Generic.HashSet(); if (root.TryGetProperty("JobTitle", out _)) @@ -69,20 +32,9 @@ private Type SelectType(JsonElement root, JsonSerializerOptions options) 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 +42,21 @@ 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; + return instance; } - if (options.PropertyNameCaseInsensitive) + protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Person value, Type runtimeType, JsonSerializerOptions options) { - 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 false; } - 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.Generator.Tests/GeneratorDriverTests.cs b/JsonSubTypes.Aot.Generator.Tests/GeneratorDriverTests.cs index 9589887..4c1e6ca 100644 --- a/JsonSubTypes.Aot.Generator.Tests/GeneratorDriverTests.cs +++ b/JsonSubTypes.Aot.Generator.Tests/GeneratorDriverTests.cs @@ -32,7 +32,7 @@ public void Generate_SingleBase_EmitsConverterAndRegistry() .SelectMany(r => r.GeneratedSources) .Select(s => s.HintName).OrderBy(h => h).ToArray(); CollectionAssert.AreEqual( - new[] { "AnimalJsonSubTypesConverter.g.cs", "JsonSubTypesAotConverters.g.cs" }, + new[] { "AnimalJsonSubTypesConverter.g.cs", "JsonSubTypesAotConverterBases.g.cs", "JsonSubTypesAotConverters.g.cs" }, hints); } @@ -53,7 +53,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..1dfe00c 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(EmitConverterBases(), 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,84 +606,231 @@ public static class JsonSubTypesAotConverters """; } - private static string EmitConverter(BaseTypeInfo info) + private static string EmitConverterBases() { - string write = EmitWriteMethod(info); - string read = EmitReadMethod(info); - string selectType = EmitSelectTypeMethod(info); - string discriminatorWriter = info.IsValueMode ? EmitDiscriminatorWriter(info) : ""; - string baseHelpers = EmitBaseHelpers(info); - return $$""" + // 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. + return """ // #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 {{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}} + 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)); } - public override {{info.FullyQualifiedName}}? Read(ref Utf8JsonReader reader, Type typeToConvert, 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) { - {{read}} + 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)); } - private Type SelectType(JsonElement root, JsonSerializerOptions options) + 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 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) { - {{selectType}} + DynamicSubtypes[discriminator] = type; + _dynamicReverse[type] = discriminator; // last registration wins, like the builder } - {{discriminatorWriter}} - {{baseHelpers}} + /// + /// 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()) + { + if (string.Equals(property.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.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 EmitWriteMethod(BaseTypeInfo info) - { - if (!info.IsValueMode) - { - // presence mode is read-only: subtypes serialize through the resolver, base via WriteBaseObject - return $$""" + /// 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; } - Type runtimeType = value.GetType(); - if (runtimeType == typeof({{info.FullyQualifiedName}})) - { - WriteBaseObject(writer, ({{info.FullyQualifiedName}})value, options); - return; - } - JsonSerializer.Serialize(writer, value, options.GetTypeInfo(runtimeType)); - """; - } - - string payloadSelection = $$""" Type runtimeType = value.GetType(); string payload; - if (runtimeType == typeof({{info.FullyQualifiedName}})) + if (runtimeType == typeof(T)) { if (IsRegistered(runtimeType)) { - payload = SerializeBasePayload(({{info.FullyQualifiedName}})value, options); + payload = SerializeBasePayload(value, options); } else { - WriteBaseObject(writer, ({{info.FullyQualifiedName}})value, options); + WriteBaseObject(writer, value, options); return; } } @@ -703,71 +851,106 @@ private static string EmitWriteMethod(BaseTypeInfo info) JsonSerializer.Serialize(writer, value, options.GetTypeInfo(runtimeType)); return; } - """; - string order = info.AddDiscriminatorFirst - ? $$""" - writer.WritePropertyName(discriminatorName); - WriteDiscriminatorValue(writer, runtimeType, options); - foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject()) + using JsonDocument payloadDocument = JsonDocument.Parse(payload); + string discriminatorName = DiscriminatorPropertyName; + if (options.PropertyNamingPolicy != null) { - if (!property.NameEquals(discriminatorName)) + discriminatorName = options.PropertyNamingPolicy.ConvertName(discriminatorName); + } + writer.WriteStartObject(); + if (AddDiscriminatorFirst) + { + writer.WritePropertyName(discriminatorName); + WriteDiscriminatorValue(writer, runtimeType, options); + foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject()) { - property.WriteTo(writer); + if (!property.NameEquals(discriminatorName)) + { + property.WriteTo(writer); + } } } - """ - : $$""" - foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject()) + else { - if (!property.NameEquals(discriminatorName)) + foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject()) { - property.WriteTo(writer); + if (!property.NameEquals(discriminatorName)) + { + property.WriteTo(writer); + } } + writer.WritePropertyName(discriminatorName); + WriteDiscriminatorValue(writer, runtimeType, options); } - writer.WritePropertyName(discriminatorName); - WriteDiscriminatorValue(writer, runtimeType, options); - """; - return $$""" - if (value is null) + writer.WriteEndObject(); + } + + 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)) { - writer.WriteNullValue(); + write(writer, options); return; } - {{payloadSelection}} - using JsonDocument payloadDocument = JsonDocument.Parse(payload); - string discriminatorName = {{SymbolDisplay.FormatLiteral(info.DiscriminatorPropertyName!, quote: true)}}; - if (options.PropertyNamingPolicy != null) - { - discriminatorName = options.PropertyNamingPolicy.ConvertName(discriminatorName); - } - writer.WriteStartObject(); - {{order}} - writer.WriteEndObject(); + throw new JsonException("Impossible to serialize type: " + runtimeType.FullName + " because there is no registered mapping for the discriminator property"); + } + } + } """; } - private static string EmitReadMethod(BaseTypeInfo info) + 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 $$""" - 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({{info.FullyQualifiedName}})) - { - return DeserializeBase(root, options); - } - return ({{info.FullyQualifiedName}}?)JsonSerializer.Deserialize(root.GetRawText(), options.GetTypeInfo(target)); + // + #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}} + protected override Type SelectType(JsonElement root, JsonSerializerOptions options) + { + {{EmitSelectTypeMethod(info)}} + } + + {{EmitWriteBaseObject(info)}} + {{EmitDeserializeBase(info)}} + {{EmitTryWriteNestedObject(info)}} + } + } """; } + private static string EmitValueModeMembers(BaseTypeInfo info) + { + string discriminatorProperty = $" protected override string DiscriminatorPropertyName => {SymbolDisplay.FormatLiteral(info.DiscriminatorPropertyName!, quote: true)};\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); @@ -889,7 +1072,7 @@ private static string EmitPresenceModeSelectType(BaseTypeInfo info) """; } - private static string EmitDiscriminatorWriter(BaseTypeInfo info) + private static string EmitDiscriminatorWriters(BaseTypeInfo info) { Dictionary byType = new(); foreach (SubtypeRegistration reg in info.Subtypes) @@ -907,110 +1090,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" + + " {\n" + + string.Join(",\n", dictionaryEntries) + "\n" + + " };"; } private static string EmitNestedCases(BaseTypeInfo info) @@ -1063,7 +1149,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 +1171,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" + + " {\n" + + " writer.WriteStartObject();\n" + + body + "\n" + + " writer.WriteEndObject();\n" + + " }"; + } + + private static string EmitDeserializeBase(BaseTypeInfo info) + { + if (info.BaseIsAbstractOrInterface) + { + return " protected override " + info.FullyQualifiedName + " DeserializeBase(JsonElement root, JsonSerializerOptions options)\n" + + " {\n" + + " throw new JsonException(\"Could not create an instance of type " + info.FullyQualifiedName + ". Type is an interface or abstract class and cannot be instantiated.\");\n" + + " }"; + } + + if (!info.BaseHasParameterlessConstructor) + { + return " protected override " + info.FullyQualifiedName + " DeserializeBase(JsonElement root, JsonSerializerOptions options)\n" + + " {\n" + + " 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" + + " }"; + } + List readProperties = []; foreach (BaseProperty prop in info.Properties) { @@ -1101,106 +1214,22 @@ 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" + + " {\n" + + " " + info.FullyQualifiedName + " instance = new " + info.FullyQualifiedName + "();\n" + + body + "\n" + + " return instance;\n" + + " }"; + } - 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) + { + return " protected override bool TryWriteNestedObject(Utf8JsonWriter writer, " + info.FullyQualifiedName + " value, Type runtimeType, JsonSerializerOptions options)\n" + + " {\n" + + EmitNestedCases(info) + "\n" + + " return false;\n" + + " }"; } } } From 61e9d3081367f67d3cf25da534efe48a1f47f255 Mon Sep 17 00:00:00 2001 From: manuc66 Date: Thu, 13 Aug 2026 23:01:59 +0200 Subject: [PATCH 2/4] Fix net35 build: keep the ToArray() string.Join needs on .NET 3.5 This file carries two changes in one commit. The bulk is a refactor of GetTypeByPropertyPresence (single-pass lookup, StopLookupOnMatch return, Count > 1 ambiguity check, SingleOrDefault) authored by a parallel agent session on this worktree; it was never committed and broke the net35 build because string.Join(string, IEnumerable) does not exist on .NET 3.5. The commit adds back the ToArray() the previous code had. Behavior is unchanged: same types found, same first-match shortcut, same ambiguity error. Verified by the 147 Newtonsoft tests on net8.0 and net10.0 and by the net35 project compiling clean. --- JsonSubTypes/JsonSubtypes.cs | 36 ++++++++++++++---------------------- 1 file changed, 14 insertions(+), 22 deletions(-) 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) From 31e5c18d3dde90da74b286c2b51c9238c6262e5b Mon Sep 17 00:00:00 2001 From: manuc66 Date: Thu, 13 Aug 2026 23:11:15 +0200 Subject: [PATCH 3/4] Harden the generated value-mode Write and drop duplicate blank lines Sonar flags on the committed golden master: the value-mode Write had cyclomatic complexity 18 and the subclass files carried double blank lines (and a blank line right after an opening brace in presence mode). The Write now delegates payload resolution to TryGetPayload (Write: 6, helper: 5) with identical behavior, verified by the 73 parity tests. The EmitConverter template inlines the value-mode members so no stray blank lines appear, and TryWriteNestedObject omits the empty line when there are no nested chains. Also verified the CodeFactor badge already exists in README.md, so nothing was added there. --- .../AnimalJsonSubTypesConverter.cs | 2 - .../DottedGadgetJsonSubTypesConverter.cs | 2 - .../GadgetJsonSubTypesConverter.cs | 2 - .../GameJsonSubTypesConverter.cs | 2 - .../JsonSubTypesAotConverterBases.cs | 88 +++++++++-------- .../PayloadJsonSubTypesConverter.cs | 1 - .../PersonJsonSubTypesConverter.cs | 2 - JsonSubTypes.Aot/JsonSubTypesGenerator.cs | 94 +++++++++---------- 8 files changed, 89 insertions(+), 104 deletions(-) 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 8587515..3b6af6d 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 @@ -16,7 +16,6 @@ public sealed class AnimalJsonSubTypesConverter : JsonSubTypesAotValueConverterB [typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Dog)] = static (writer, options) => writer.WriteNumberValue(2) }; - protected override Type SelectType(JsonElement root, JsonSerializerOptions options) { if (TryGetValueInJson(root, "type", options, out JsonElement discriminator)) @@ -76,7 +75,6 @@ protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTy } protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Animal value, Type runtimeType, JsonSerializerOptions options) { - return false; } } 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 46e4533..ec82600 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 @@ -15,7 +15,6 @@ public sealed class DottedGadgetJsonSubTypesConverter : JsonSubTypesAotValueConv [typeof(global::JsonSubTypes.Aot.Generated.TestDomain.DottedElectronic)] = static (writer, options) => writer.WriteStringValue("electronic") }; - protected override Type SelectType(JsonElement root, JsonSerializerOptions options) { if (TryGetValueInJson(root, "nested.type", options, out JsonElement discriminator)) @@ -69,7 +68,6 @@ protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTy } protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget value, Type runtimeType, JsonSerializerOptions options) { - return false; } } 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 76f5f9f..826471c 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 @@ -15,7 +15,6 @@ public sealed class GadgetJsonSubTypesConverter : JsonSubTypesAotValueConverterB [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, "kind", options, out JsonElement discriminator)) @@ -75,7 +74,6 @@ protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTy } protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Gadget value, Type runtimeType, JsonSerializerOptions options) { - return false; } } 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 294f834..c191582 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 @@ -16,7 +16,6 @@ public sealed class GameJsonSubTypesConverter : JsonSubTypesAotValueConverterBas [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, "$GameKind", options, out JsonElement discriminator)) @@ -69,7 +68,6 @@ protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTy } protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Game value, Type runtimeType, JsonSerializerOptions options) { - return false; } } 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 index 6f0e11c..4a397f9 100644 --- a/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/JsonSubTypesAotConverterBases.cs +++ b/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/JsonSubTypesAotConverterBases.cs @@ -201,71 +201,69 @@ public sealed override void Write(Utf8JsonWriter writer, T value, JsonSerializer writer.WriteNullValue(); return; } + Type runtimeType = value.GetType(); - string payload; - if (runtimeType == typeof(T)) - { - if (IsRegistered(runtimeType)) - { - payload = SerializeBasePayload(value, options); - } - else - { - WriteBaseObject(writer, value, options); - return; - } - } - else if (IsRegistered(runtimeType)) - { - payload = JsonSerializer.Serialize(value, options.GetTypeInfo(runtimeType)); - } - else if (TryWriteNestedObject(writer, value, runtimeType, options)) + if (!TryGetPayload(writer, value, runtimeType, options, out string payload)) { return; } - else if (TryWriteDynamic(writer, value, runtimeType, options)) - { - return; - } - else - { - JsonSerializer.Serialize(writer, value, options.GetTypeInfo(runtimeType)); - return; - } + using JsonDocument payloadDocument = JsonDocument.Parse(payload); - string discriminatorName = DiscriminatorPropertyName; - if (options.PropertyNamingPolicy != null) - { - discriminatorName = options.PropertyNamingPolicy.ConvertName(discriminatorName); - } + 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()) - { - if (!property.NameEquals(discriminatorName)) - { - property.WriteTo(writer); - } - } } - else + foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject()) { - foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject()) + if (!property.NameEquals(discriminatorName)) { - if (!property.NameEquals(discriminatorName)) - { - property.WriteTo(writer); - } + 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) 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 b39ebee..45ae513 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 @@ -16,7 +16,6 @@ public sealed class PayloadJsonSubTypesConverter : JsonSubTypesAotValueConverter [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, "$PayloadKind", options, out JsonElement discriminator)) 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 15c8cda..a910081 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 @@ -9,7 +9,6 @@ namespace JsonSubTypes.Aot.Generated /// Generated by JsonSubTypes.Aot. Compiled converter for global::JsonSubTypes.Aot.Generated.TestDomain.Person. public sealed class PersonJsonSubTypesConverter : JsonSubTypesAotConverterBase { - protected override Type SelectType(JsonElement root, JsonSerializerOptions options) { System.Collections.Generic.HashSet matches = new System.Collections.Generic.HashSet(); @@ -55,7 +54,6 @@ protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTy } protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Person value, Type runtimeType, JsonSerializerOptions options) { - return false; } } diff --git a/JsonSubTypes.Aot/JsonSubTypesGenerator.cs b/JsonSubTypes.Aot/JsonSubTypesGenerator.cs index 1dfe00c..65aec69 100644 --- a/JsonSubTypes.Aot/JsonSubTypesGenerator.cs +++ b/JsonSubTypes.Aot/JsonSubTypesGenerator.cs @@ -820,71 +820,69 @@ public sealed override void Write(Utf8JsonWriter writer, T value, JsonSerializer writer.WriteNullValue(); return; } + Type runtimeType = value.GetType(); - string payload; - if (runtimeType == typeof(T)) - { - if (IsRegistered(runtimeType)) - { - payload = SerializeBasePayload(value, options); - } - else - { - WriteBaseObject(writer, 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)) + if (!TryGetPayload(writer, value, runtimeType, options, out string payload)) { return; } - else - { - JsonSerializer.Serialize(writer, value, options.GetTypeInfo(runtimeType)); - return; - } + using JsonDocument payloadDocument = JsonDocument.Parse(payload); - string discriminatorName = DiscriminatorPropertyName; - if (options.PropertyNamingPolicy != null) - { - discriminatorName = options.PropertyNamingPolicy.ConvertName(discriminatorName); - } + 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()) - { - if (!property.NameEquals(discriminatorName)) - { - property.WriteTo(writer); - } - } } - else + foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject()) { - foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject()) + if (!property.NameEquals(discriminatorName)) { - if (!property.NameEquals(discriminatorName)) - { - property.WriteTo(writer); - } + 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) @@ -929,8 +927,7 @@ namespace JsonSubTypes.Aot.Generated [global::System.CodeDom.Compiler.GeneratedCode("JsonSubTypes.Aot", "1.0.0")] public sealed class {{info.ConverterName}} : {{baseClass}} { - {{valueModeMembers}} - protected override Type SelectType(JsonElement root, JsonSerializerOptions options) + {{valueModeMembers}} protected override Type SelectType(JsonElement root, JsonSerializerOptions options) { {{EmitSelectTypeMethod(info)}} } @@ -1225,9 +1222,10 @@ private static string EmitDeserializeBase(BaseTypeInfo info) 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" + " {\n" + - EmitNestedCases(info) + "\n" + + (nestedCases.Length == 0 ? "" : nestedCases + "\n") + " return false;\n" + " }"; } From 849ee7afe315d070f08fb77d50b7938fa9ee24d7 Mon Sep 17 00:00:00 2001 From: manuc66 Date: Thu, 13 Aug 2026 23:25:24 +0200 Subject: [PATCH 4/4] Cut cognitive complexity and clear the remaining Sonar smells Value-mode SelectType was 18 (Cognitive Complexity limit 15): it now delegates the static discriminator table to a private ResolveStaticType helper (SelectType 6, helper under limit) with identical behavior, verified by the parity tests. The duplicated discriminator literal is emitted once as a private const DiscriminatorPropertyNameValue, reused by DiscriminatorPropertyName, SelectType and the nested chains that reference the converter's own discriminator. Shared base cleanup: DynamicSubtypes becomes a get-only property (S3887), the case-insensitive and discriminator-skip loops use Where (S3267), IndexOf('.') becomes Contains('.') (CA2249), and the bases source is a const (S3400). Generator internals stop repeating the ' {' / ' }' fragments (S1192). The driver now references System.Linq because the emitted bases use Where. Tests: JsonSerializerOptions instances are cached instead of rebuilt per operation, and the expected hints array is a static readonly field. Sonar's S2583 on JsonSubtypes.cs (the 'Count > 1' ambiguity check) is left untouched: it is a false positive - the throw is exercised by DemoKnownSubTypeWithPropertyTests and DemoKnownSubTypeWithMultiplePropertiesTests - and the concurrent refactor is behaviorally identical to the previous code. Verified: 22 generator tests, 73 Aot parity tests, 190 Text.Json tests, 147 Newtonsoft tests (net8 + net10), solution build, sample build. --- .../AnimalJsonSubTypesConverter.cs | 67 +++-- .../DottedGadgetJsonSubTypesConverter.cs | 55 ++-- .../GadgetJsonSubTypesConverter.cs | 67 +++-- .../GameJsonSubTypesConverter.cs | 67 +++-- .../JsonSubTypesAotConverterBases.cs | 21 +- .../PayloadJsonSubTypesConverter.cs | 71 +++-- .../PersonJsonSubTypesConverter.cs | 36 +-- .../CommittedGeneratedConverterTests.cs | 30 +- .../GeneratedCodeExecutionTests.cs | 29 +- .../GeneratorDriverRunner.cs | 1 + .../GeneratorDriverTests.cs | 9 +- JsonSubTypes.Aot/JsonSubTypesGenerator.cs | 257 +++++++++--------- 12 files changed, 384 insertions(+), 326 deletions(-) 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 3b6af6d..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 @@ -8,7 +8,8 @@ namespace JsonSubTypes.Aot.Generated /// Generated by JsonSubTypes.Aot. Compiled converter for global::JsonSubTypes.Aot.Generated.TestDomain.Animal. public sealed class AnimalJsonSubTypesConverter : JsonSubTypesAotValueConverterBase { - protected override string DiscriminatorPropertyName => "type"; + private const string DiscriminatorPropertyNameValue = "type"; + protected override string DiscriminatorPropertyName => DiscriminatorPropertyNameValue; protected override System.Collections.Generic.Dictionary> DiscriminatorWriters { get; } = new System.Collections.Generic.Dictionary> { @@ -18,38 +19,48 @@ public sealed class AnimalJsonSubTypesConverter : JsonSubTypesAotValueConverterB protected override Type SelectType(JsonElement root, JsonSerializerOptions options) { - if (TryGetValueInJson(root, "type", options, out JsonElement discriminator)) - { + if (!TryGetValueInJson(root, DiscriminatorPropertyNameValue, options, out JsonElement discriminator)) + { + return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Animal); + } + Type? staticType = ResolveStaticType(discriminator); + if (staticType != null) + { + return staticType; + } + 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(global::JsonSubTypes.Aot.Generated.TestDomain.Animal); + } - if (discriminator.ValueKind == JsonValueKind.String) - { - switch (discriminator.GetString()) + private static Type? ResolveStaticType(JsonElement discriminator) { + switch (discriminator.ValueKind) + { + 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; } protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Animal value, JsonSerializerOptions options) 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 ec82600..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 @@ -8,7 +8,8 @@ namespace JsonSubTypes.Aot.Generated /// Generated by JsonSubTypes.Aot. Compiled converter for global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget. public sealed class DottedGadgetJsonSubTypesConverter : JsonSubTypesAotValueConverterBase { - protected override string DiscriminatorPropertyName => "nested.type"; + private const string DiscriminatorPropertyNameValue = "nested.type"; + protected override string DiscriminatorPropertyName => DiscriminatorPropertyNameValue; protected override System.Collections.Generic.Dictionary> DiscriminatorWriters { get; } = new System.Collections.Generic.Dictionary> { @@ -17,32 +18,42 @@ public sealed class DottedGadgetJsonSubTypesConverter : JsonSubTypesAotValueConv protected override Type SelectType(JsonElement root, JsonSerializerOptions options) { - if (TryGetValueInJson(root, "nested.type", options, out JsonElement discriminator)) - { - - if (discriminator.ValueKind == JsonValueKind.String) - { - switch (discriminator.GetString()) - { - case "electronic": return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.DottedElectronic); + if (!TryGetValueInJson(root, DiscriminatorPropertyNameValue, options, out JsonElement discriminator)) + { + return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget); + } + Type? staticType = ResolveStaticType(discriminator); + if (staticType != null) + { + return staticType; + } + 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(global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget); } - } - if (TryGetDynamicType(discriminator, out Type? dynamicType)) + private static Type? ResolveStaticType(JsonElement discriminator) { - return dynamicType!; - } - if (CustomTypeNameResolver is not null) - { - Type? customType = CustomTypeNameResolver(GetDiscriminatorKey(discriminator)); - if (customType != null) + switch (discriminator.ValueKind) { - return customType; + case JsonValueKind.String: + switch (discriminator.GetString()) + { + case "electronic": return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.DottedElectronic); + } + break; } - } - return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget); - } - return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget); + return null; } protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget value, JsonSerializerOptions options) 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 826471c..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 @@ -8,7 +8,8 @@ namespace JsonSubTypes.Aot.Generated /// Generated by JsonSubTypes.Aot. Compiled converter for global::JsonSubTypes.Aot.Generated.TestDomain.Gadget. public sealed class GadgetJsonSubTypesConverter : JsonSubTypesAotValueConverterBase { - protected override string DiscriminatorPropertyName => "kind"; + private const string DiscriminatorPropertyNameValue = "kind"; + protected override string DiscriminatorPropertyName => DiscriminatorPropertyNameValue; protected override System.Collections.Generic.Dictionary> DiscriminatorWriters { get; } = new System.Collections.Generic.Dictionary> { @@ -17,38 +18,48 @@ public sealed class GadgetJsonSubTypesConverter : JsonSubTypesAotValueConverterB protected override Type SelectType(JsonElement root, JsonSerializerOptions options) { - if (TryGetValueInJson(root, "kind", options, out JsonElement discriminator)) - { + if (!TryGetValueInJson(root, DiscriminatorPropertyNameValue, options, out JsonElement discriminator)) + { + return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Gadget); + } + Type? staticType = ResolveStaticType(discriminator); + if (staticType != null) + { + return staticType; + } + 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(global::JsonSubTypes.Aot.Generated.TestDomain.Gadget); + } - if (discriminator.ValueKind == JsonValueKind.String) - { - switch (discriminator.GetString()) + private static Type? ResolveStaticType(JsonElement discriminator) { + switch (discriminator.ValueKind) + { + 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 typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Gadget); - } - return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Gadget); + return null; } protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Gadget value, JsonSerializerOptions options) 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 c191582..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 @@ -8,7 +8,8 @@ namespace JsonSubTypes.Aot.Generated /// Generated by JsonSubTypes.Aot. Compiled converter for global::JsonSubTypes.Aot.Generated.TestDomain.Game. public sealed class GameJsonSubTypesConverter : JsonSubTypesAotValueConverterBase { - protected override string DiscriminatorPropertyName => "$GameKind"; + private const string DiscriminatorPropertyNameValue = "$GameKind"; + protected override string DiscriminatorPropertyName => DiscriminatorPropertyNameValue; protected override System.Collections.Generic.Dictionary> DiscriminatorWriters { get; } = new System.Collections.Generic.Dictionary> { @@ -18,40 +19,50 @@ public sealed class GameJsonSubTypesConverter : JsonSubTypesAotValueConverterBas protected override Type SelectType(JsonElement root, JsonSerializerOptions options) { - if (TryGetValueInJson(root, "$GameKind", options, out JsonElement discriminator)) - { + if (!TryGetValueInJson(root, DiscriminatorPropertyNameValue, options, out JsonElement discriminator)) + { + return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Game); + } + Type? staticType = ResolveStaticType(discriminator); + if (staticType != null) + { + return staticType; + } + 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(global::JsonSubTypes.Aot.Generated.TestDomain.Game); + } - if (discriminator.ValueKind == JsonValueKind.String) - { - switch (discriminator.GetString()) + private static Type? ResolveStaticType(JsonElement discriminator) { + switch (discriminator.ValueKind) + { + 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 typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Game); - } - return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Game); + return null; } protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Game value, JsonSerializerOptions options) 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 index 4a397f9..4b65802 100644 --- a/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/JsonSubTypesAotConverterBases.cs +++ b/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/JsonSubTypesAotConverterBases.cs @@ -1,5 +1,6 @@ #nullable enable using System; +using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; @@ -59,7 +60,7 @@ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions // presence-mode converters. protected virtual string DiscriminatorPropertyName => ""; - public readonly System.Collections.Concurrent.ConcurrentDictionary DynamicSubtypes = new System.Collections.Concurrent.ConcurrentDictionary(); + 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) @@ -154,13 +155,10 @@ protected static bool TryGetProperty(JsonElement root, string name, JsonSerializ } if (options.PropertyNameCaseInsensitive) { - foreach (JsonProperty property in root.EnumerateObject()) + foreach (JsonProperty property in root.EnumerateObject().Where(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase))) { - if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase)) - { - value = property.Value; - return true; - } + value = property.Value; + return true; } } return false; @@ -172,7 +170,7 @@ protected static bool TryGetValueInJson(JsonElement root, string propertyName, J { return true; } - if (propertyName.IndexOf('.') >= 0) + if (propertyName.Contains('.')) { string[] segments = propertyName.Split('.'); JsonElement current = root; @@ -216,12 +214,9 @@ public sealed override void Write(Utf8JsonWriter writer, T value, JsonSerializer writer.WritePropertyName(discriminatorName); WriteDiscriminatorValue(writer, runtimeType, options); } - foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject()) + foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject().Where(p => !p.NameEquals(discriminatorName))) { - if (!property.NameEquals(discriminatorName)) - { - property.WriteTo(writer); - } + property.WriteTo(writer); } if (!AddDiscriminatorFirst) { 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 45ae513..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 @@ -8,7 +8,8 @@ namespace JsonSubTypes.Aot.Generated /// Generated by JsonSubTypes.Aot. Compiled converter for global::JsonSubTypes.Aot.Generated.TestDomain.Payload. public sealed class PayloadJsonSubTypesConverter : JsonSubTypesAotValueConverterBase { - protected override string DiscriminatorPropertyName => "$PayloadKind"; + private const string DiscriminatorPropertyNameValue = "$PayloadKind"; + protected override string DiscriminatorPropertyName => DiscriminatorPropertyNameValue; protected override System.Collections.Generic.Dictionary> DiscriminatorWriters { get; } = new System.Collections.Generic.Dictionary> { @@ -18,40 +19,50 @@ public sealed class PayloadJsonSubTypesConverter : JsonSubTypesAotValueConverter protected override Type SelectType(JsonElement root, JsonSerializerOptions options) { - if (TryGetValueInJson(root, "$PayloadKind", options, out JsonElement discriminator)) - { + if (!TryGetValueInJson(root, DiscriminatorPropertyNameValue, options, out JsonElement discriminator)) + { + return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Payload); + } + Type? staticType = ResolveStaticType(discriminator); + if (staticType != null) + { + return staticType; + } + 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(global::JsonSubTypes.Aot.Generated.TestDomain.Payload); + } - if (discriminator.ValueKind == JsonValueKind.String) - { - switch (discriminator.GetString()) + private static Type? ResolveStaticType(JsonElement discriminator) { + switch (discriminator.ValueKind) + { + 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 typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Payload); - } - return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Payload); + return null; } protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Payload value, JsonSerializerOptions options) @@ -71,7 +82,7 @@ protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::Json 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)))); @@ -87,7 +98,7 @@ protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::Json 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)))); 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 a910081..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 @@ -11,24 +11,24 @@ public sealed class PersonJsonSubTypesConverter : JsonSubTypesAotConverterBase 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); + 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); } protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Person value, JsonSerializerOptions options) 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 4c1e6ca..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", "JsonSubTypesAotConverterBases.g.cs", "JsonSubTypesAotConverters.g.cs" }, - hints); + CollectionAssert.AreEqual(SingleBaseExpectedHints, hints); } [Test] diff --git a/JsonSubTypes.Aot/JsonSubTypesGenerator.cs b/JsonSubTypes.Aot/JsonSubTypesGenerator.cs index 65aec69..11d3e19 100644 --- a/JsonSubTypes.Aot/JsonSubTypesGenerator.cs +++ b/JsonSubTypes.Aot/JsonSubTypesGenerator.cs @@ -73,7 +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(EmitConverterBases(), 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)); @@ -606,17 +606,19 @@ public static class JsonSubTypesAotConverters """; } - private static string EmitConverterBases() - { - // 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. - 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; + using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; @@ -677,7 +679,7 @@ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions // presence-mode converters. protected virtual string DiscriminatorPropertyName => ""; - public readonly System.Collections.Concurrent.ConcurrentDictionary DynamicSubtypes = new System.Collections.Concurrent.ConcurrentDictionary(); + 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) @@ -772,13 +774,10 @@ protected static bool TryGetProperty(JsonElement root, string name, JsonSerializ } if (options.PropertyNameCaseInsensitive) { - foreach (JsonProperty property in root.EnumerateObject()) + foreach (JsonProperty property in root.EnumerateObject().Where(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase))) { - if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase)) - { - value = property.Value; - return true; - } + value = property.Value; + return true; } } return false; @@ -790,7 +789,7 @@ protected static bool TryGetValueInJson(JsonElement root, string propertyName, J { return true; } - if (propertyName.IndexOf('.') >= 0) + if (propertyName.Contains('.')) { string[] segments = propertyName.Split('.'); JsonElement current = root; @@ -835,12 +834,9 @@ public sealed override void Write(Utf8JsonWriter writer, T value, JsonSerializer writer.WritePropertyName(discriminatorName); WriteDiscriminatorValue(writer, runtimeType, options); } - foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject()) + foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject().Where(p => !p.NameEquals(discriminatorName))) { - if (!property.NameEquals(discriminatorName)) - { - property.WriteTo(writer); - } + property.WriteTo(writer); } if (!AddDiscriminatorFirst) { @@ -904,7 +900,6 @@ private void WriteDiscriminatorValue(Utf8JsonWriter writer, Type runtimeType, Js } } """; - } private static string EmitConverter(BaseTypeInfo info) { @@ -927,10 +922,7 @@ namespace JsonSubTypes.Aot.Generated [global::System.CodeDom.Compiler.GeneratedCode("JsonSubTypes.Aot", "1.0.0")] public sealed class {{info.ConverterName}} : {{baseClass}} { - {{valueModeMembers}} protected override Type SelectType(JsonElement root, JsonSerializerOptions options) - { - {{EmitSelectTypeMethod(info)}} - } + {{valueModeMembers}}{{EmitSelectTypeMethod(info)}} {{EmitWriteBaseObject(info)}} {{EmitDeserializeBase(info)}} @@ -942,7 +934,9 @@ public sealed class {{info.ConverterName}} : {{baseClass}} private static string EmitValueModeMembers(BaseTypeInfo info) { - string discriminatorProperty = $" protected override string DiscriminatorPropertyName => {SymbolDisplay.FormatLiteral(info.DiscriminatorPropertyName!, quote: true)};\n\n"; + 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; @@ -955,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) @@ -1039,34 +1036,31 @@ 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 EmitDiscriminatorWriters(BaseTypeInfo info) @@ -1091,7 +1085,7 @@ private static string EmitDiscriminatorWriters(BaseTypeInfo info) } return " protected override System.Collections.Generic.Dictionary> DiscriminatorWriters { get; } = new System.Collections.Generic.Dictionary>\n" + - " {\n" + + MemberOpenBrace + string.Join(",\n", dictionaryEntries) + "\n" + " };"; } @@ -1109,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 = $$""" @@ -1170,11 +1167,11 @@ private static string EmitWriteBaseObject(BaseTypeInfo info) string body = string.Join("\n", writeProperties); return " protected override void WriteBaseObject(Utf8JsonWriter writer, " + info.FullyQualifiedName + " value, JsonSerializerOptions options)\n" + - " {\n" + + MemberOpenBrace + " writer.WriteStartObject();\n" + body + "\n" + " writer.WriteEndObject();\n" + - " }"; + MemberCloseBrace; } private static string EmitDeserializeBase(BaseTypeInfo info) @@ -1182,17 +1179,17 @@ private static string EmitDeserializeBase(BaseTypeInfo info) if (info.BaseIsAbstractOrInterface) { return " protected override " + info.FullyQualifiedName + " DeserializeBase(JsonElement root, JsonSerializerOptions options)\n" + - " {\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" + - " {\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 = []; @@ -1213,21 +1210,21 @@ private static string EmitDeserializeBase(BaseTypeInfo info) string body = string.Join("\n", readProperties); return " protected override " + info.FullyQualifiedName + " DeserializeBase(JsonElement root, JsonSerializerOptions options)\n" + - " {\n" + + MemberOpenBrace + " " + info.FullyQualifiedName + " instance = new " + info.FullyQualifiedName + "();\n" + body + "\n" + " return instance;\n" + - " }"; + MemberCloseBrace; } 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" + - " {\n" + + MemberOpenBrace + (nestedCases.Length == 0 ? "" : nestedCases + "\n") + " return false;\n" + - " }"; + MemberCloseBrace; } } }