diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4d57c62..f950a3b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,7 +31,7 @@ jobs: run: dotnet test JsonSubTypes.Text.Json.Tests/JsonSubTypes.Text.Json.Tests.csproj -c Release - name: Test Aot (net8 + net10) - run: dotnet test JsonSubTypes.Aot.Tests/JsonSubTypes.Aot.Tests.csproj -c Release + run: dotnet test JsonSubTypes.Text.Json.Aot.Tests/JsonSubTypes.Text.Json.Aot.Tests.csproj -c Release # The Newtonsoft tests run on net10 too (the library targets netstandard1.3/2.0), # which lets coverlet measure the JsonSubTypes package on Linux. @@ -42,10 +42,10 @@ jobs: run: dotnet test JsonSubTypes.Text.Json.Tests/JsonSubTypes.Text.Json.Tests.csproj -c Release -f net10.0 --collect:"XPlat Code Coverage" --results-directory TestResults/textjson - name: Test Aot with coverage (net10) - run: dotnet test JsonSubTypes.Aot.Tests/JsonSubTypes.Aot.Tests.csproj -c Release -f net10.0 --collect:"XPlat Code Coverage" --results-directory TestResults/aot + run: dotnet test JsonSubTypes.Text.Json.Aot.Tests/JsonSubTypes.Text.Json.Aot.Tests.csproj -c Release -f net10.0 --collect:"XPlat Code Coverage" --results-directory TestResults/aot - name: Test Aot generator with coverage (net10) - run: dotnet test JsonSubTypes.Aot.Generator.Tests/JsonSubTypes.Aot.Generator.Tests.csproj -c Release -f net10.0 --collect:"XPlat Code Coverage" --results-directory TestResults/generator + run: dotnet test JsonSubTypes.Text.Json.Aot.Generator.Tests/JsonSubTypes.Text.Json.Aot.Generator.Tests.csproj -c Release -f net10.0 --collect:"XPlat Code Coverage" --results-directory TestResults/generator - name: Install gpg (required by codecov-action v5) run: sudo apt-get install -y gpg @@ -67,16 +67,16 @@ jobs: # building it catches generated-code conflicts that unit tests cannot (e.g. a # top-level `_` variable colliding with `out _` emitted by the generator). - name: Build Aot sample (top-level statements consumer) - run: dotnet build JsonSubTypes.Aot.Sample/JsonSubTypes.Aot.Sample.csproj -c Release -f net8.0 + run: dotnet build JsonSubTypes.Text.Json.Aot.Sample/JsonSubTypes.Text.Json.Aot.Sample.csproj -c Release -f net8.0 # Native AOT end-to-end check: publish the sample as a native binary and run # it. This is the only check that proves the generated converters work in a # trimmed/AOT binary, not just under the JIT. - name: Publish Aot sample as native binary - run: dotnet publish JsonSubTypes.Aot.Sample/JsonSubTypes.Aot.Sample.csproj -c Release -f net8.0 -r linux-x64 -p:StripSymbols=true + run: dotnet publish JsonSubTypes.Text.Json.Aot.Sample/JsonSubTypes.Text.Json.Aot.Sample.csproj -c Release -f net8.0 -r linux-x64 -p:StripSymbols=true - name: Run the native Aot sample - run: ./JsonSubTypes.Aot.Sample/bin/Release/net8.0/linux-x64/publish/JsonSubTypes.Aot.Sample + run: ./JsonSubTypes.Text.Json.Aot.Sample/bin/Release/net8.0/linux-x64/publish/JsonSubTypes.Text.Json.Aot.Sample framework: # The Newtonsoft library targets net35/40/45/46/47 (.NET Framework), which runs @@ -140,7 +140,7 @@ jobs: if [ "${{ github.event_name }}" = "pull_request" ]; then pr_args="/d:sonar.pullrequest.key=$PR_KEY /d:sonar.pullrequest.branch=$PR_BRANCH /d:sonar.pullrequest.base=$PR_BASE" fi - dotnet-sonarscanner begin /k:manuc66_JsonSubTypes /o:manuc66-github /d:sonar.token="$SONAR_TOKEN" /d:sonar.host.url=https://sonarcloud.io /d:sonar.cs.cobertura.reportsPaths=TestResults/**/coverage.cobertura.xml /d:sonar.exclusions=**/*Tests*/**/*,**/JsonSubTypes.Benchmarks/**/*,**/JsonSubTypes.Aot.Sample/**/*,**/JsonSubTypes.Aot.Generated/TestDomain.cs /d:sonar.cpd.exclusions=**/*.g.cs $pr_args + dotnet-sonarscanner begin /k:manuc66_JsonSubTypes /o:manuc66-github /d:sonar.token="$SONAR_TOKEN" /d:sonar.host.url=https://sonarcloud.io /d:sonar.cs.cobertura.reportsPaths=TestResults/**/coverage.cobertura.xml /d:sonar.exclusions=**/*Tests*/**/*,**/JsonSubTypes.Benchmarks/**/*,**/JsonSubTypes.Text.Json.Aot.Sample/**/*,**/JsonSubTypes.Text.Json.Aot.Generated/TestDomain.cs /d:sonar.cpd.exclusions=**/*.g.cs $pr_args - name: Build for SonarCloud analysis run: dotnet build JsonSubTypes.sln -c Release @@ -149,8 +149,8 @@ jobs: run: | dotnet test JsonSubTypes.Tests/JsonSubTypes.Tests.csproj -c Release -f net10.0 --collect:"XPlat Code Coverage" --results-directory TestResults/newtonsoft dotnet test JsonSubTypes.Text.Json.Tests/JsonSubTypes.Text.Json.Tests.csproj -c Release -f net10.0 --collect:"XPlat Code Coverage" --results-directory TestResults/textjson - dotnet test JsonSubTypes.Aot.Tests/JsonSubTypes.Aot.Tests.csproj -c Release -f net10.0 --collect:"XPlat Code Coverage" --results-directory TestResults/aot - dotnet test JsonSubTypes.Aot.Generator.Tests/JsonSubTypes.Aot.Generator.Tests.csproj -c Release -f net10.0 --collect:"XPlat Code Coverage" --results-directory TestResults/generator + dotnet test JsonSubTypes.Text.Json.Aot.Tests/JsonSubTypes.Text.Json.Aot.Tests.csproj -c Release -f net10.0 --collect:"XPlat Code Coverage" --results-directory TestResults/aot + dotnet test JsonSubTypes.Text.Json.Aot.Generator.Tests/JsonSubTypes.Text.Json.Aot.Generator.Tests.csproj -c Release -f net10.0 --collect:"XPlat Code Coverage" --results-directory TestResults/generator - name: End SonarCloud analysis run: dotnet-sonarscanner end /d:sonar.token="$SONAR_TOKEN" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ed02202..ab3f062 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -41,8 +41,8 @@ jobs: - name: Build + pack JsonSubTypes.Text.Json run: dotnet build JsonSubTypes.Text.Json/JsonSubTypes.Text.Json.csproj -c Release - - name: Build + pack JsonSubTypes.Aot - run: dotnet build JsonSubTypes.Aot/JsonSubTypes.Aot.csproj -c Release + - name: Build + pack JsonSubTypes.Text.Json.Aot + run: dotnet build JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.csproj -c Release # Trusted publishing: exchange the GitHub OIDC token for a short-lived # NuGet API key (no long-lived secret). Requires a trusted publishing @@ -78,12 +78,12 @@ jobs: dotnet nuget push "JsonSubTypes.Text.Json/bin/Release/*.nupkg" "JsonSubTypes.Text.Json/bin/Release/*.snupkg" --api-key ${{ steps.nuget-login.outputs.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate fi - - name: Publish JsonSubTypes.Aot + - name: Publish JsonSubTypes.Text.Json.Aot run: | id=jsonSubTypes.aot - version=$(dotnet msbuild JsonSubTypes.Aot/JsonSubTypes.Aot.csproj -getProperty:Version -p:Configuration=Release | tail -n 1) + version=$(dotnet msbuild JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.csproj -getProperty:Version -p:Configuration=Release | tail -n 1) if curl -sf -o /dev/null "https://api.nuget.org/v3-flatcontainer/$id/$version/$id.$version.nupkg"; then - echo "JsonSubTypes.Aot $version already on nuget.org, skipping" + echo "JsonSubTypes.Text.Json.Aot $version already on nuget.org, skipping" else - dotnet nuget push "JsonSubTypes.Aot/bin/Release/*.nupkg" --api-key ${{ steps.nuget-login.outputs.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate + dotnet nuget push "JsonSubTypes.Text.Json.Aot/bin/Release/*.nupkg" --api-key ${{ steps.nuget-login.outputs.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 83f9088..30ee87c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,11 +15,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### JsonSubTypes.Text.Json #### Added -- New `JsonSubTypesAotConverterAttribute` opting a base type into the `JsonSubTypes.Aot` source generator. +- New `JsonSubTypesAotConverterAttribute` opting a base type into the `JsonSubTypes.Text.Json.Aot` source generator. -### JsonSubTypes.Aot +### JsonSubTypes.Text.Json.Aot #### Added -- New package `JsonSubTypes.Aot` (1.0.0-rc.1): a Roslyn source generator emitting compiled subtype converters. Routing (property presence, fallback, enums, nested hierarchies, dynamic registration) is compiled, so it works in Native AOT / trimmed binaries without reflection. +- New package `JsonSubTypes.Text.Json.Aot` (1.0.0-rc.1): a Roslyn source generator emitting compiled subtype converters. Routing (property presence, fallback, enums, nested hierarchies, dynamic registration) is compiled, so it works in Native AOT / trimmed binaries without reflection. ## [1.0.0-rc.2] - 2026-08-10 ### Changed 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 deleted file mode 100644 index 902773a..0000000 --- a/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/PersonJsonSubTypesConverter.cs +++ /dev/null @@ -1,60 +0,0 @@ -#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.Person. - public sealed class PersonJsonSubTypesConverter : JsonSubTypesAotConverterBase - { - protected override Type SelectType(JsonElement root, JsonSerializerOptions options) - { - System.Collections.Generic.HashSet matches = new System.Collections.Generic.HashSet(); - if (root.TryGetProperty("JobTitle", out _)) - { - matches.Add(typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Employee)); - } - if (root.TryGetProperty("Skill", out _)) - { - matches.Add(typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Artist)); - } - if (matches.Count == 1) - { - return matches.First(); - } - if (matches.Count > 1) - { - throw new JsonException("Ambiguous type resolution, expected only one type but got: " + string.Join(", ", matches.Select(t => t.FullName))); - } - return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Person); - } - - protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Person value, JsonSerializerOptions options) - { - writer.WriteStartObject(); - string nameFirstName = "FirstName"; - if (options.PropertyNamingPolicy != null && true) - { - nameFirstName = options.PropertyNamingPolicy.ConvertName(nameFirstName); - } - writer.WritePropertyName(nameFirstName); - JsonSerializer.Serialize(writer, value.FirstName, options.GetTypeInfo(typeof(string))); - 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; - } - protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Person value, Type runtimeType, JsonSerializerOptions options) - { - return false; - } - } -} \ No newline at end of file diff --git a/JsonSubTypes.Aot.Generator.Tests/CommittedGeneratedConverterTests.cs b/JsonSubTypes.Aot.Generator.Tests/CommittedGeneratedConverterTests.cs deleted file mode 100644 index 1a60d1d..0000000 --- a/JsonSubTypes.Aot.Generator.Tests/CommittedGeneratedConverterTests.cs +++ /dev/null @@ -1,152 +0,0 @@ -#nullable enable -using System.Text.Json; -using System.Text.Json.Serialization; -using JsonSubTypes.Aot.Generated; -using JsonSubTypes.Aot.Generated.TestDomain; -using NUnit.Framework; - -namespace JsonSubTypes.Aot.Generator.Tests -{ - // Exercises the committed golden-master converters (the Generated/ files in - // JsonSubTypes.Aot.Generated). Because those files are compiled as real sources, - // coverlet measures them here and Sonar analyzes them. Every converter must be - // exercised on both serialize and deserialize so its Write/Read/SelectType paths - // are covered. - [TestFixture] - public class CommittedGeneratedConverterTests - { - private static JsonSerializerOptions Options() - { - return new JsonSerializerOptions { Converters = { GetConverter() } }; - } - - private static JsonConverter GetConverter() - { - // resolve the converter instance from the committed registry - return (JsonConverter)RegistryConverter(typeof(T)); - } - - private static object RegistryConverter(System.Type baseType) - { - var registry = typeof(JsonSubTypesAotConverters); - string memberName = baseType.Name; - return registry.GetField(memberName)?.GetValue(null) - ?? throw new System.InvalidOperationException("No committed converter for " + baseType.Name); - } - - // ---- Animal (string + int discriminators) ---- - - [Test] - public void Animal_SerializeCat_WritesDiscriminator() - { - string json = JsonSerializer.Serialize(new Cat { Age = 3, Lives = 9 }, Options()); - Assert.That(json, Does.Contain("\"type\":\"cat\"")); - } - - [Test] - public void Animal_DeserializeCatDiscriminator_ReturnsCat() - { - Animal? result = JsonSerializer.Deserialize("{\"type\":\"cat\",\"Lives\":9,\"Age\":3}", Options()); - Assert.That(result, Is.InstanceOf()); - } - - [Test] - public void Animal_DeserializeIntDogDiscriminator_ReturnsDog() - { - Animal? result = JsonSerializer.Deserialize("{\"type\":2,\"CanHunt\":true,\"Age\":3}", Options()); - Assert.That(result, Is.InstanceOf()); - } - - [Test] - public void Animal_DeserializeUnknownDiscriminator_FallsBackToBase() - { - Animal? result = JsonSerializer.Deserialize("{\"type\":\"fish\",\"Age\":3}", Options()); - Assert.That(result, Is.InstanceOf()); - Assert.That(result, Is.Not.InstanceOf()); - } - - // ---- Person (property presence + fallback) ---- - - [Test] - public void Person_SerializeArtist_NoDiscriminatorWritten() - { - string json = JsonSerializer.Serialize(new Artist { Skill = "Painter", FirstName = "A" }, Options()); - Assert.That(json, Does.Contain("\"Skill\"")); - } - - [Test] - public void Person_DeserializeBySkill_ReturnsArtist() - { - Person? result = JsonSerializer.Deserialize("{\"Skill\":\"Painter\",\"FirstName\":\"A\"}", Options()); - Assert.That(result, Is.InstanceOf()); - } - - [Test] - public void Person_DeserializeUnknown_FallsBackToBase() - { - Person? result = JsonSerializer.Deserialize("{\"FirstName\":\"A\"}", Options()); - Assert.That(result, Is.InstanceOf()); - Assert.That(result, Is.Not.InstanceOf()); - } - - // ---- Gadget (enum discriminator) ---- - - [Test] - public void Gadget_SerializeElectronicCat_WritesEnumDiscriminator() - { - string json = JsonSerializer.Serialize(new ElectronicCat { Age = 3, Lives = 9 }, Options()); - Assert.That(json, Does.Contain("\"kind\":")); - } - - [Test] - public void Gadget_DeserializeElectronicCat_ReturnsSubtype() - { - Gadget? result = JsonSerializer.Deserialize("{\"kind\":0,\"Lives\":9,\"Age\":3}", Options()); - Assert.That(result, Is.InstanceOf()); - } - - // ---- DottedGadget (nested discriminator path) ---- - - [Test] - public void DottedGadget_DeserializeNestedDiscriminator_ReturnsSubtype() - { - DottedGadget? result = JsonSerializer.Deserialize( - "{\"nested\":{\"type\":\"electronic\"},\"Lives\":9,\"Age\":3}", Options()); - Assert.That(result, Is.InstanceOf()); - } - - // ---- 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() - { - Payload? result = JsonSerializer.Deserialize( - "{\"$PayloadKind\":0,\"$GameKind\":0}", PayloadAndGameOptions); - Assert.That(result, Is.InstanceOf()); - } - - [Test] - public void Payload_DeserializeCom_ReturnsCom() - { - Payload? result = JsonSerializer.Deserialize( - "{\"$PayloadKind\":1}", PayloadOptions); - Assert.That(result, Is.InstanceOf()); - } - } -} diff --git a/JsonSubTypes.Aot.Generator.Tests/GeneratorDriverTests.cs b/JsonSubTypes.Aot.Generator.Tests/GeneratorDriverTests.cs deleted file mode 100644 index 8fae5f9..0000000 --- a/JsonSubTypes.Aot.Generator.Tests/GeneratorDriverTests.cs +++ /dev/null @@ -1,132 +0,0 @@ -#nullable enable -using System.Linq; -using NUnit.Framework; - -namespace JsonSubTypes.Aot.Generator.Tests -{ - [TestFixture] - public class GeneratorDriverTests - { - private const string SimpleHierarchy = @" -using System.Text.Json.Serialization; -using JsonSubTypes.Text.Json; - -[JsonSubTypesAotConverter(""type"")] -[KnownSubType(typeof(Cat), ""cat"")] -[KnownSubType(typeof(Dog), ""dog"")] -public class Animal -{ - public int Age { get; set; } -} - -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() - { - GeneratorRun run = GeneratorDriverRunner.GetRun(SimpleHierarchy); - - string[] hints = run.DriverResults.Results - .SelectMany(r => r.GeneratedSources) - .Select(s => s.HintName).OrderBy(h => h).ToArray(); - CollectionAssert.AreEqual(SingleBaseExpectedHints, hints); - } - - [Test] - public void Generate_SingleBase_RegistryExposesConverterInstance() - { - GeneratorRun run = GeneratorDriverRunner.GetRun(SimpleHierarchy); - - string? text = GeneratorDriverRunner.GetGeneratedSource(run, "JsonSubTypesAotConverters.g.cs"); - Assert.That(text, Is.Not.Null); - StringAssert.Contains("public static readonly AnimalJsonSubTypesConverter Animal", text!); - } - - [Test] - public void Generate_SingleBase_ConverterRoutesSubtypes() - { - GeneratorRun run = GeneratorDriverRunner.GetRun(SimpleHierarchy); - - string? text = GeneratorDriverRunner.GetGeneratedSource(run, "AnimalJsonSubTypesConverter.g.cs"); - Assert.That(text, Is.Not.Null); - StringAssert.Contains("class AnimalJsonSubTypesConverter : JsonSubTypesAotValueConverterBase<", text!); - StringAssert.Contains("\"cat\"", text!); - StringAssert.Contains("\"dog\"", text!); - } - - [Test] - public void Generate_NoAttribute_EmitsNothing() - { - GeneratorRun run = GeneratorDriverRunner.GetRun("public class Plain { public int X { get; set; } }"); - - Assert.That(run.DriverResults.Results.SelectMany(r => r.GeneratedSources), Is.Empty); - } - - [Test] - public void Generate_PropertyPresence_EmitsPresenceRouting() - { - const string presenceHierarchy = @" -using System.Text.Json.Serialization; -using JsonSubTypes.Text.Json; - -[JsonSubTypesAotConverter] -[KnownSubTypeWithProperty(typeof(Employee), ""JobTitle"")] -[KnownSubTypeWithProperty(typeof(Artist), ""Skill"")] -public class Person { public string? FirstName { get; set; } } - -public class Employee : Person { public string? JobTitle { get; set; } } -public class Artist : Person { public string? Skill { get; set; } } -"; - GeneratorRun run = GeneratorDriverRunner.GetRun(presenceHierarchy); - - string? text = GeneratorDriverRunner.GetGeneratedSource(run, "PersonJsonSubTypesConverter.g.cs"); - Assert.That(text, Is.Not.Null); - StringAssert.Contains("TryGetProperty(\"JobTitle\"", text!); - StringAssert.Contains("TryGetProperty(\"Skill\"", text!); - } - - [Test] - public void Generate_TwoBasesWithSameNameInDifferentNamespaces_ProducesDistinctConverters() - { - const string twoAnimals = @" -using System.Text.Json.Serialization; -using JsonSubTypes.Text.Json; - -namespace A -{ - [JsonSubTypesAotConverter(""type"")] - [KnownSubType(typeof(Cat), ""cat"")] - public class Animal { public int Age { get; set; } } - public class Cat : Animal { } -} - -namespace B -{ - [JsonSubTypesAotConverter(""type"")] - [KnownSubType(typeof(Dog), ""dog"")] - public class Animal { public int Age { get; set; } } - public class Dog : Animal { } -} -"; - GeneratorRun run = GeneratorDriverRunner.GetRun(twoAnimals); - - string[] hints = run.DriverResults.Results - .SelectMany(r => r.GeneratedSources) - .Select(s => s.HintName).OrderBy(h => h).ToArray(); - - // Both converters are emitted under distinct qualified names, not one - // overwriting the other. - Assert.That(hints, Does.Contain("global__A_AnimalJsonSubTypesConverter.g.cs")); - Assert.That(hints, Does.Contain("global__B_AnimalJsonSubTypesConverter.g.cs")); - Assert.That(hints.Count(h => h.StartsWith("Animal")), Is.EqualTo(0), - "unqualified Animal converter should not exist when names collide"); - } - } -} diff --git a/JsonSubTypes.Benchmarks/JsonSubTypes.Benchmarks.csproj b/JsonSubTypes.Benchmarks/JsonSubTypes.Benchmarks.csproj index e849751..a55dbb4 100644 --- a/JsonSubTypes.Benchmarks/JsonSubTypes.Benchmarks.csproj +++ b/JsonSubTypes.Benchmarks/JsonSubTypes.Benchmarks.csproj @@ -15,7 +15,7 @@ - diff --git a/JsonSubTypes.Benchmarks/Program.cs b/JsonSubTypes.Benchmarks/Program.cs index 0ba3318..2e488ad 100644 --- a/JsonSubTypes.Benchmarks/Program.cs +++ b/JsonSubTypes.Benchmarks/Program.cs @@ -5,7 +5,7 @@ using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Running; using BenchmarkDotNet.Toolchains.NativeAot; -using JsonSubTypes.Aot.Generated; +using JsonSubTypes.Text.Json.Aot.Generated; using JsonSubTypes.Text.Json; namespace JsonSubTypes.Benchmarks diff --git a/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/AnimalJsonSubTypesConverter.cs b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/AnimalJsonSubTypesConverter.cs similarity index 55% rename from JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/AnimalJsonSubTypesConverter.cs rename to JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/AnimalJsonSubTypesConverter.cs index 1d806e5..b19bd65 100644 --- a/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/AnimalJsonSubTypesConverter.cs +++ b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/AnimalJsonSubTypesConverter.cs @@ -3,25 +3,25 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace JsonSubTypes.Aot.Generated +namespace JsonSubTypes.Text.Json.Aot.Generated { - /// Generated by JsonSubTypes.Aot. Compiled converter for global::JsonSubTypes.Aot.Generated.TestDomain.Animal. - public sealed class AnimalJsonSubTypesConverter : JsonSubTypesAotValueConverterBase + /// Generated by JsonSubTypes.Text.Json.Aot. Compiled converter for global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Animal. + public sealed class AnimalJsonSubTypesConverter : JsonSubTypesAotValueConverterBase { private const string DiscriminatorPropertyNameValue = "type"; protected override string DiscriminatorPropertyName => DiscriminatorPropertyNameValue; protected override System.Collections.Generic.Dictionary> DiscriminatorWriters { get; } = 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) + [typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Cat)] = static (writer, options) => writer.WriteStringValue("cat"), + [typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Dog)] = static (writer, options) => writer.WriteNumberValue(2) }; protected override Type SelectType(JsonElement root, JsonSerializerOptions options) { if (!TryGetValueInJson(root, DiscriminatorPropertyNameValue, options, out JsonElement discriminator)) { - return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Animal); + return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Animal); } Type? staticType = ResolveStaticType(discriminator); if (staticType != null) @@ -40,7 +40,7 @@ protected override Type SelectType(JsonElement root, JsonSerializerOptions optio return customType; } } - return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Animal); + return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Animal); } private static Type? ResolveStaticType(JsonElement discriminator) @@ -50,41 +50,41 @@ protected override Type SelectType(JsonElement root, JsonSerializerOptions optio case JsonValueKind.String: switch (discriminator.GetString()) { - case "cat": return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Cat); + case "cat": return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Cat); } break; case JsonValueKind.Number: switch (discriminator.GetRawText()) { - case "2": return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Dog); + case "2": return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Dog); } break; } return null; } - protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Animal value, JsonSerializerOptions options) + protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Animal value, JsonSerializerOptions options) { writer.WriteStartObject(); - string nameAge = "Age"; - if (options.PropertyNamingPolicy != null && true) - { - nameAge = options.PropertyNamingPolicy.ConvertName(nameAge); - } - writer.WritePropertyName(nameAge); - JsonSerializer.Serialize(writer, value.Age, options.GetTypeInfo(typeof(int))); + string nameAge = "Age"; + if (options.PropertyNamingPolicy != null) + { + nameAge = options.PropertyNamingPolicy.ConvertName(nameAge); + } + writer.WritePropertyName(nameAge); + JsonSerializer.Serialize(writer, value.Age, options.GetTypeInfo(typeof(int))); writer.WriteEndObject(); } - protected override global::JsonSubTypes.Aot.Generated.TestDomain.Animal DeserializeBase(JsonElement root, JsonSerializerOptions options) + protected override global::JsonSubTypes.Text.Json.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)))!; - } + global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Animal instance = new global::JsonSubTypes.Text.Json.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; } - protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Animal value, Type runtimeType, JsonSerializerOptions options) + protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Animal value, Type runtimeType, JsonSerializerOptions options) { return false; } diff --git a/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/DiscriminatorLastJsonSubTypesConverter.cs b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/DiscriminatorLastJsonSubTypesConverter.cs new file mode 100644 index 0000000..85a7807 --- /dev/null +++ b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/DiscriminatorLastJsonSubTypesConverter.cs @@ -0,0 +1,87 @@ +#nullable enable +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace JsonSubTypes.Text.Json.Aot.Generated +{ + /// Generated by JsonSubTypes.Text.Json.Aot. Compiled converter for global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.DiscriminatorLast. + public sealed class DiscriminatorLastJsonSubTypesConverter : JsonSubTypesAotValueConverterBase + { + private const string DiscriminatorPropertyNameValue = "type"; + protected override string DiscriminatorPropertyName => DiscriminatorPropertyNameValue; + + protected override bool AddDiscriminatorFirst => false; + + protected override System.Collections.Generic.Dictionary> DiscriminatorWriters { get; } = new System.Collections.Generic.Dictionary> + { + [typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Mammoth)] = static (writer, options) => writer.WriteStringValue("mammoth") + }; + + protected override Type SelectType(JsonElement root, JsonSerializerOptions options) + { + if (!TryGetValueInJson(root, DiscriminatorPropertyNameValue, options, out JsonElement discriminator)) + { + return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.DiscriminatorLast); + } + 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.Text.Json.Aot.Generated.TestDomain.DiscriminatorLast); + } + + private static Type? ResolveStaticType(JsonElement discriminator) + { + switch (discriminator.ValueKind) + { + case JsonValueKind.String: + switch (discriminator.GetString()) + { + case "mammoth": return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Mammoth); + } + break; + } + return null; + } + + protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.DiscriminatorLast value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + string nameAge = "Age"; + if (options.PropertyNamingPolicy != null) + { + nameAge = options.PropertyNamingPolicy.ConvertName(nameAge); + } + writer.WritePropertyName(nameAge); + JsonSerializer.Serialize(writer, value.Age, options.GetTypeInfo(typeof(int))); + writer.WriteEndObject(); + } + protected override global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.DiscriminatorLast DeserializeBase(JsonElement root, JsonSerializerOptions options) + { + global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.DiscriminatorLast instance = new global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.DiscriminatorLast(); + if (TryGetProperty(root, "Age", options, out JsonElement AgeValue)) + { + instance.Age = (int)JsonSerializer.Deserialize(AgeValue.GetRawText(), options.GetTypeInfo(typeof(int)))!; + } + return instance; + } + protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.DiscriminatorLast value, Type runtimeType, JsonSerializerOptions options) + { + return false; + } + } +} \ No newline at end of file diff --git a/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/DottedGadgetJsonSubTypesConverter.cs b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/DottedGadgetJsonSubTypesConverter.cs similarity index 54% rename from JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/DottedGadgetJsonSubTypesConverter.cs rename to JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/DottedGadgetJsonSubTypesConverter.cs index 842aa3a..19876ca 100644 --- a/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/DottedGadgetJsonSubTypesConverter.cs +++ b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/DottedGadgetJsonSubTypesConverter.cs @@ -3,24 +3,24 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace JsonSubTypes.Aot.Generated +namespace JsonSubTypes.Text.Json.Aot.Generated { - /// Generated by JsonSubTypes.Aot. Compiled converter for global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget. - public sealed class DottedGadgetJsonSubTypesConverter : JsonSubTypesAotValueConverterBase + /// Generated by JsonSubTypes.Text.Json.Aot. Compiled converter for global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.DottedGadget. + public sealed class DottedGadgetJsonSubTypesConverter : JsonSubTypesAotValueConverterBase { private const string DiscriminatorPropertyNameValue = "nested.type"; protected override string DiscriminatorPropertyName => DiscriminatorPropertyNameValue; protected override System.Collections.Generic.Dictionary> DiscriminatorWriters { get; } = new System.Collections.Generic.Dictionary> { - [typeof(global::JsonSubTypes.Aot.Generated.TestDomain.DottedElectronic)] = static (writer, options) => writer.WriteStringValue("electronic") + [typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.DottedElectronic)] = static (writer, options) => writer.WriteStringValue("electronic") }; protected override Type SelectType(JsonElement root, JsonSerializerOptions options) { if (!TryGetValueInJson(root, DiscriminatorPropertyNameValue, options, out JsonElement discriminator)) { - return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget); + return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.DottedGadget); } Type? staticType = ResolveStaticType(discriminator); if (staticType != null) @@ -39,7 +39,7 @@ protected override Type SelectType(JsonElement root, JsonSerializerOptions optio return customType; } } - return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget); + return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.DottedGadget); } private static Type? ResolveStaticType(JsonElement discriminator) @@ -49,35 +49,35 @@ protected override Type SelectType(JsonElement root, JsonSerializerOptions optio case JsonValueKind.String: switch (discriminator.GetString()) { - case "electronic": return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.DottedElectronic); + case "electronic": return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.DottedElectronic); } break; } return null; } - protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget value, JsonSerializerOptions options) + protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.DottedGadget value, JsonSerializerOptions options) { writer.WriteStartObject(); - string nameAge = "Age"; - if (options.PropertyNamingPolicy != null && true) - { - nameAge = options.PropertyNamingPolicy.ConvertName(nameAge); - } - writer.WritePropertyName(nameAge); - JsonSerializer.Serialize(writer, value.Age, options.GetTypeInfo(typeof(int))); + string nameAge = "Age"; + if (options.PropertyNamingPolicy != null) + { + nameAge = options.PropertyNamingPolicy.ConvertName(nameAge); + } + writer.WritePropertyName(nameAge); + JsonSerializer.Serialize(writer, value.Age, options.GetTypeInfo(typeof(int))); writer.WriteEndObject(); } - protected override global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget DeserializeBase(JsonElement root, JsonSerializerOptions options) + protected override global::JsonSubTypes.Text.Json.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)))!; - } + global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.DottedGadget instance = new global::JsonSubTypes.Text.Json.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; } - protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.DottedGadget value, Type runtimeType, JsonSerializerOptions options) + protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.DottedGadget value, Type runtimeType, JsonSerializerOptions options) { return false; } diff --git a/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/DynamicShapeJsonSubTypesConverter.cs b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/DynamicShapeJsonSubTypesConverter.cs new file mode 100644 index 0000000..5147cf1 --- /dev/null +++ b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/DynamicShapeJsonSubTypesConverter.cs @@ -0,0 +1,106 @@ +#nullable enable +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace JsonSubTypes.Text.Json.Aot.Generated +{ + /// Generated by JsonSubTypes.Text.Json.Aot. Compiled converter for global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.DynamicShape. + public sealed class DynamicShapeJsonSubTypesConverter : JsonSubTypesAotValueConverterBase + { + private const string DiscriminatorPropertyNameValue = "kind"; + protected override string DiscriminatorPropertyName => DiscriminatorPropertyNameValue; + + protected override System.Collections.Generic.Dictionary> DiscriminatorWriters { get; } = new System.Collections.Generic.Dictionary> + { + [typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.DynamicCat)] = static (writer, options) => writer.WriteStringValue("cat") + }; + + protected override Type SelectType(JsonElement root, JsonSerializerOptions options) + { + if (!TryGetValueInJson(root, DiscriminatorPropertyNameValue, options, out JsonElement discriminator)) + { + return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.DynamicShape); + } + 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.Text.Json.Aot.Generated.TestDomain.DynamicShape); + } + + private static Type? ResolveStaticType(JsonElement discriminator) + { + switch (discriminator.ValueKind) + { + case JsonValueKind.String: + switch (discriminator.GetString()) + { + case "cat": return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.DynamicCat); + } + break; + } + return null; + } + + protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.DynamicShape value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + string nameAge = "Age"; + if (options.PropertyNamingPolicy != null) + { + nameAge = options.PropertyNamingPolicy.ConvertName(nameAge); + } + writer.WritePropertyName(nameAge); + JsonSerializer.Serialize(writer, value.Age, options.GetTypeInfo(typeof(int))); + string nameComputed = "Computed"; + if (options.PropertyNamingPolicy != null) + { + nameComputed = options.PropertyNamingPolicy.ConvertName(nameComputed); + } + writer.WritePropertyName(nameComputed); + JsonSerializer.Serialize(writer, value.Computed, options.GetTypeInfo(typeof(string))); + if (value.Nickname != null) + { + string nameNickname = "Nickname"; + if (options.PropertyNamingPolicy != null) + { + nameNickname = options.PropertyNamingPolicy.ConvertName(nameNickname); + } + writer.WritePropertyName(nameNickname); + JsonSerializer.Serialize(writer, value.Nickname, options.GetTypeInfo(typeof(string))); + } + writer.WriteEndObject(); + } + protected override global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.DynamicShape DeserializeBase(JsonElement root, JsonSerializerOptions options) + { + global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.DynamicShape instance = new global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.DynamicShape(); + if (TryGetProperty(root, "Age", options, out JsonElement AgeValue)) + { + instance.Age = (int)JsonSerializer.Deserialize(AgeValue.GetRawText(), options.GetTypeInfo(typeof(int)))!; + } + if (TryGetProperty(root, "Nickname", options, out JsonElement NicknameValue)) + { + instance.Nickname = (string)JsonSerializer.Deserialize(NicknameValue.GetRawText(), options.GetTypeInfo(typeof(string)))!; + } + return instance; + } + protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.DynamicShape value, Type runtimeType, JsonSerializerOptions options) + { + return false; + } + } +} \ No newline at end of file diff --git a/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/GadgetJsonSubTypesConverter.cs b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/GadgetJsonSubTypesConverter.cs similarity index 54% rename from JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/GadgetJsonSubTypesConverter.cs rename to JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/GadgetJsonSubTypesConverter.cs index 5de618f..a879cc8 100644 --- a/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/GadgetJsonSubTypesConverter.cs +++ b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/GadgetJsonSubTypesConverter.cs @@ -3,24 +3,24 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace JsonSubTypes.Aot.Generated +namespace JsonSubTypes.Text.Json.Aot.Generated { - /// Generated by JsonSubTypes.Aot. Compiled converter for global::JsonSubTypes.Aot.Generated.TestDomain.Gadget. - public sealed class GadgetJsonSubTypesConverter : JsonSubTypesAotValueConverterBase + /// Generated by JsonSubTypes.Text.Json.Aot. Compiled converter for global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Gadget. + public sealed class GadgetJsonSubTypesConverter : JsonSubTypesAotValueConverterBase { private const string DiscriminatorPropertyNameValue = "kind"; protected override string DiscriminatorPropertyName => DiscriminatorPropertyNameValue; protected override System.Collections.Generic.Dictionary> DiscriminatorWriters { get; } = 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)))) + [typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.ElectronicCat)] = static (writer, options) => writer.WriteRawValue(JsonSerializer.Serialize(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.GadgetKind.ElectronicCat, options.GetTypeInfo(typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.GadgetKind)))) }; protected override Type SelectType(JsonElement root, JsonSerializerOptions options) { if (!TryGetValueInJson(root, DiscriminatorPropertyNameValue, options, out JsonElement discriminator)) { - return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Gadget); + return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Gadget); } Type? staticType = ResolveStaticType(discriminator); if (staticType != null) @@ -39,7 +39,7 @@ protected override Type SelectType(JsonElement root, JsonSerializerOptions optio return customType; } } - return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Gadget); + return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Gadget); } private static Type? ResolveStaticType(JsonElement discriminator) @@ -49,41 +49,41 @@ protected override Type SelectType(JsonElement root, JsonSerializerOptions optio case JsonValueKind.String: switch (discriminator.GetString()) { - case "ElectronicCat": return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.ElectronicCat); + case "ElectronicCat": return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.ElectronicCat); } break; case JsonValueKind.Number: switch (discriminator.GetRawText()) { - case "0": return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.ElectronicCat); + case "0": return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.ElectronicCat); } break; } return null; } - protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Gadget value, JsonSerializerOptions options) + protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Gadget value, JsonSerializerOptions options) { writer.WriteStartObject(); - string nameAge = "Age"; - if (options.PropertyNamingPolicy != null && true) - { - nameAge = options.PropertyNamingPolicy.ConvertName(nameAge); - } - writer.WritePropertyName(nameAge); - JsonSerializer.Serialize(writer, value.Age, options.GetTypeInfo(typeof(int))); + string nameAge = "Age"; + if (options.PropertyNamingPolicy != null) + { + nameAge = options.PropertyNamingPolicy.ConvertName(nameAge); + } + writer.WritePropertyName(nameAge); + JsonSerializer.Serialize(writer, value.Age, options.GetTypeInfo(typeof(int))); writer.WriteEndObject(); } - protected override global::JsonSubTypes.Aot.Generated.TestDomain.Gadget DeserializeBase(JsonElement root, JsonSerializerOptions options) + protected override global::JsonSubTypes.Text.Json.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)))!; - } + global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Gadget instance = new global::JsonSubTypes.Text.Json.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; } - protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Gadget value, Type runtimeType, JsonSerializerOptions options) + protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Text.Json.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.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/GameJsonSubTypesConverter.cs similarity index 57% rename from JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/GameJsonSubTypesConverter.cs rename to JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/GameJsonSubTypesConverter.cs index 48171e5..d374f15 100644 --- a/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/GameJsonSubTypesConverter.cs +++ b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/GameJsonSubTypesConverter.cs @@ -3,25 +3,25 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace JsonSubTypes.Aot.Generated +namespace JsonSubTypes.Text.Json.Aot.Generated { - /// Generated by JsonSubTypes.Aot. Compiled converter for global::JsonSubTypes.Aot.Generated.TestDomain.Game. - public sealed class GameJsonSubTypesConverter : JsonSubTypesAotValueConverterBase + /// Generated by JsonSubTypes.Text.Json.Aot. Compiled converter for global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Game. + public sealed class GameJsonSubTypesConverter : JsonSubTypesAotValueConverterBase { private const string DiscriminatorPropertyNameValue = "$GameKind"; protected override string DiscriminatorPropertyName => DiscriminatorPropertyNameValue; protected override System.Collections.Generic.Dictionary> DiscriminatorWriters { get; } = 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)))) + [typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Run)] = static (writer, options) => writer.WriteRawValue(JsonSerializer.Serialize(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.GameDiscriminator.RUN, options.GetTypeInfo(typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.GameDiscriminator)))), + [typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Walk)] = static (writer, options) => writer.WriteRawValue(JsonSerializer.Serialize(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.GameDiscriminator.WALK, options.GetTypeInfo(typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.GameDiscriminator)))) }; protected override Type SelectType(JsonElement root, JsonSerializerOptions options) { if (!TryGetValueInJson(root, DiscriminatorPropertyNameValue, options, out JsonElement discriminator)) { - return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Game); + return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Game); } Type? staticType = ResolveStaticType(discriminator); if (staticType != null) @@ -40,7 +40,7 @@ protected override Type SelectType(JsonElement root, JsonSerializerOptions optio return customType; } } - return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Game); + return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Game); } private static Type? ResolveStaticType(JsonElement discriminator) @@ -50,34 +50,34 @@ protected override Type SelectType(JsonElement root, JsonSerializerOptions optio 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); + case "RUN": return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Run); + case "WALK": return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Walk); } 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); + case "0": return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Run); + case "1": return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Walk); } break; } return null; } - protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Game value, JsonSerializerOptions options) + protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Game value, JsonSerializerOptions options) { writer.WriteStartObject(); writer.WriteEndObject(); } - protected override global::JsonSubTypes.Aot.Generated.TestDomain.Game DeserializeBase(JsonElement root, JsonSerializerOptions options) + protected override global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Game DeserializeBase(JsonElement root, JsonSerializerOptions options) { - global::JsonSubTypes.Aot.Generated.TestDomain.Game instance = new global::JsonSubTypes.Aot.Generated.TestDomain.Game(); + global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Game instance = new global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Game(); return instance; } - protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Game value, Type runtimeType, JsonSerializerOptions options) + protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Text.Json.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.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/JsonSubTypesAotConverterBases.cs similarity index 93% rename from JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/JsonSubTypesAotConverterBases.cs rename to JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/JsonSubTypesAotConverterBases.cs index 4b65802..dc97606 100644 --- a/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/JsonSubTypesAotConverterBases.cs +++ b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/JsonSubTypesAotConverterBases.cs @@ -4,9 +4,9 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace JsonSubTypes.Aot.Generated +namespace JsonSubTypes.Text.Json.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. + /// Generated by JsonSubTypes.Text.Json.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); @@ -28,7 +28,7 @@ public abstract class JsonSubTypesAotConverterBase : JsonConverter where T { return DeserializeBase(root, options); } - return (T?)JsonSerializer.Deserialize(root.GetRawText(), options.GetTypeInfo(target)); + return (T?)JsonSerializer.Deserialize(root, options.GetTypeInfo(target)); } // Presence-mode write: the base object is written through WriteBaseObject, @@ -155,9 +155,10 @@ protected static bool TryGetProperty(JsonElement root, string name, JsonSerializ } if (options.PropertyNameCaseInsensitive) { - foreach (JsonProperty property in root.EnumerateObject().Where(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase))) + JsonProperty match = root.EnumerateObject().FirstOrDefault(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase)); + if (match.Value.ValueKind != JsonValueKind.Undefined) { - value = property.Value; + value = match.Value; return true; } } @@ -189,7 +190,7 @@ protected static bool TryGetValueInJson(JsonElement root, string propertyName, J } } - /// Generated by JsonSubTypes.Aot. Value-mode converter skeleton: adds the discriminator-injection Write on top of JsonSubTypesAotConverterBase. + /// Generated by JsonSubTypes.Text.Json.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) diff --git a/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/JsonSubTypesAotConverters.cs b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/JsonSubTypesAotConverters.cs similarity index 54% rename from JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/JsonSubTypesAotConverters.cs rename to JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/JsonSubTypesAotConverters.cs index ddfa7f3..eefd2ec 100644 --- a/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/JsonSubTypesAotConverters.cs +++ b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/JsonSubTypesAotConverters.cs @@ -1,8 +1,8 @@ #nullable enable -namespace JsonSubTypes.Aot.Generated +namespace JsonSubTypes.Text.Json.Aot.Generated { - /// Generated by JsonSubTypes.Aot. Shared converter instances to add to JsonSerializerOptions.Converters. + /// Generated by JsonSubTypes.Text.Json.Aot. Shared converter instances to add to JsonSerializerOptions.Converters. public static class JsonSubTypesAotConverters { public static readonly AnimalJsonSubTypesConverter Animal = new AnimalJsonSubTypesConverter(); @@ -11,5 +11,8 @@ public static class JsonSubTypesAotConverters public static readonly DottedGadgetJsonSubTypesConverter DottedGadget = new DottedGadgetJsonSubTypesConverter(); public static readonly PayloadJsonSubTypesConverter Payload = new PayloadJsonSubTypesConverter(); public static readonly GameJsonSubTypesConverter Game = new GameJsonSubTypesConverter(); + public static readonly NullDiscriminatorAnimalJsonSubTypesConverter NullDiscriminatorAnimal = new NullDiscriminatorAnimalJsonSubTypesConverter(); + public static readonly DiscriminatorLastJsonSubTypesConverter DiscriminatorLast = new DiscriminatorLastJsonSubTypesConverter(); + public static readonly DynamicShapeJsonSubTypesConverter DynamicShape = new DynamicShapeJsonSubTypesConverter(); } } \ No newline at end of file diff --git a/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/NullDiscriminatorAnimalJsonSubTypesConverter.cs b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/NullDiscriminatorAnimalJsonSubTypesConverter.cs new file mode 100644 index 0000000..031013a --- /dev/null +++ b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/NullDiscriminatorAnimalJsonSubTypesConverter.cs @@ -0,0 +1,88 @@ +#nullable enable +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace JsonSubTypes.Text.Json.Aot.Generated +{ + /// Generated by JsonSubTypes.Text.Json.Aot. Compiled converter for global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.NullDiscriminatorAnimal. + public sealed class NullDiscriminatorAnimalJsonSubTypesConverter : JsonSubTypesAotValueConverterBase + { + private const string DiscriminatorPropertyNameValue = "type"; + protected override string DiscriminatorPropertyName => DiscriminatorPropertyNameValue; + + protected override System.Collections.Generic.Dictionary> DiscriminatorWriters { get; } = new System.Collections.Generic.Dictionary> + { + [typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.NullDiscriminatorAnimal)] = static (writer, options) => writer.WriteNullValue(), + [typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Deer)] = static (writer, options) => writer.WriteStringValue("deer") + }; + + protected override Type SelectType(JsonElement root, JsonSerializerOptions options) + { + if (!TryGetValueInJson(root, DiscriminatorPropertyNameValue, options, out JsonElement discriminator)) + { + return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.NullDiscriminatorAnimal); + } + 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.Text.Json.Aot.Generated.TestDomain.NullDiscriminatorAnimal); + } + + private static Type? ResolveStaticType(JsonElement discriminator) + { + switch (discriminator.ValueKind) + { + case JsonValueKind.Null: + return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.NullDiscriminatorAnimal); + case JsonValueKind.String: + switch (discriminator.GetString()) + { + case "deer": return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Deer); + } + break; + } + return null; + } + + protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.NullDiscriminatorAnimal value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + string nameAge = "Age"; + if (options.PropertyNamingPolicy != null) + { + nameAge = options.PropertyNamingPolicy.ConvertName(nameAge); + } + writer.WritePropertyName(nameAge); + JsonSerializer.Serialize(writer, value.Age, options.GetTypeInfo(typeof(int))); + writer.WriteEndObject(); + } + protected override global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.NullDiscriminatorAnimal DeserializeBase(JsonElement root, JsonSerializerOptions options) + { + global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.NullDiscriminatorAnimal instance = new global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.NullDiscriminatorAnimal(); + if (TryGetProperty(root, "Age", options, out JsonElement AgeValue)) + { + instance.Age = (int)JsonSerializer.Deserialize(AgeValue.GetRawText(), options.GetTypeInfo(typeof(int)))!; + } + return instance; + } + protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.NullDiscriminatorAnimal value, Type runtimeType, JsonSerializerOptions options) + { + return false; + } + } +} \ No newline at end of file diff --git a/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/PayloadJsonSubTypesConverter.cs b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/PayloadJsonSubTypesConverter.cs similarity index 58% rename from JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/PayloadJsonSubTypesConverter.cs rename to JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/PayloadJsonSubTypesConverter.cs index a6a3f99..b2d5759 100644 --- a/JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/PayloadJsonSubTypesConverter.cs +++ b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/PayloadJsonSubTypesConverter.cs @@ -3,25 +3,25 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace JsonSubTypes.Aot.Generated +namespace JsonSubTypes.Text.Json.Aot.Generated { - /// Generated by JsonSubTypes.Aot. Compiled converter for global::JsonSubTypes.Aot.Generated.TestDomain.Payload. - public sealed class PayloadJsonSubTypesConverter : JsonSubTypesAotValueConverterBase + /// Generated by JsonSubTypes.Text.Json.Aot. Compiled converter for global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Payload. + public sealed class PayloadJsonSubTypesConverter : JsonSubTypesAotValueConverterBase { private const string DiscriminatorPropertyNameValue = "$PayloadKind"; protected override string DiscriminatorPropertyName => DiscriminatorPropertyNameValue; protected override System.Collections.Generic.Dictionary> DiscriminatorWriters { get; } = 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)))) + [typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Game)] = static (writer, options) => writer.WriteRawValue(JsonSerializer.Serialize(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.PayloadDiscriminator.GAME, options.GetTypeInfo(typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.PayloadDiscriminator)))), + [typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Com)] = static (writer, options) => writer.WriteRawValue(JsonSerializer.Serialize(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.PayloadDiscriminator.COM, options.GetTypeInfo(typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.PayloadDiscriminator)))) }; protected override Type SelectType(JsonElement root, JsonSerializerOptions options) { if (!TryGetValueInJson(root, DiscriminatorPropertyNameValue, options, out JsonElement discriminator)) { - return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Payload); + return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Payload); } Type? staticType = ResolveStaticType(discriminator); if (staticType != null) @@ -40,7 +40,7 @@ protected override Type SelectType(JsonElement root, JsonSerializerOptions optio return customType; } } - return typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Payload); + return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Payload); } private static Type? ResolveStaticType(JsonElement discriminator) @@ -50,42 +50,42 @@ protected override Type SelectType(JsonElement root, JsonSerializerOptions optio 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); + case "GAME": return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Game); + case "COM": return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Com); } 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); + case "0": return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Game); + case "1": return typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Com); } break; } return null; } - protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Payload value, JsonSerializerOptions options) + protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Payload value, JsonSerializerOptions options) { writer.WriteStartObject(); writer.WriteEndObject(); } - protected override global::JsonSubTypes.Aot.Generated.TestDomain.Payload DeserializeBase(JsonElement root, JsonSerializerOptions options) + protected override global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Payload DeserializeBase(JsonElement root, JsonSerializerOptions options) { - global::JsonSubTypes.Aot.Generated.TestDomain.Payload instance = new global::JsonSubTypes.Aot.Generated.TestDomain.Payload(); + global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Payload instance = new global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Payload(); return instance; } - protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Aot.Generated.TestDomain.Payload value, Type runtimeType, JsonSerializerOptions options) + protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Payload value, Type runtimeType, JsonSerializerOptions options) { - if (runtimeType == typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Run)) + if (runtimeType == typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Run)) { writer.WriteStartObject(); writer.WritePropertyName(DiscriminatorPropertyNameValue); - writer.WriteRawValue(JsonSerializer.Serialize(global::JsonSubTypes.Aot.Generated.TestDomain.PayloadDiscriminator.GAME, options.GetTypeInfo(typeof(global::JsonSubTypes.Aot.Generated.TestDomain.PayloadDiscriminator)))); + writer.WriteRawValue(JsonSerializer.Serialize(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.PayloadDiscriminator.GAME, options.GetTypeInfo(typeof(global::JsonSubTypes.Text.Json.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)))); + writer.WriteRawValue(JsonSerializer.Serialize(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.GameDiscriminator.RUN, options.GetTypeInfo(typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.GameDiscriminator)))); string payload = JsonSerializer.Serialize(value, options.GetTypeInfo(runtimeType)); using JsonDocument payloadDocument = JsonDocument.Parse(payload); foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject()) @@ -95,13 +95,13 @@ protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::Json writer.WriteEndObject(); return true; } - if (runtimeType == typeof(global::JsonSubTypes.Aot.Generated.TestDomain.Walk)) + if (runtimeType == typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Walk)) { writer.WriteStartObject(); writer.WritePropertyName(DiscriminatorPropertyNameValue); - writer.WriteRawValue(JsonSerializer.Serialize(global::JsonSubTypes.Aot.Generated.TestDomain.PayloadDiscriminator.GAME, options.GetTypeInfo(typeof(global::JsonSubTypes.Aot.Generated.TestDomain.PayloadDiscriminator)))); + writer.WriteRawValue(JsonSerializer.Serialize(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.PayloadDiscriminator.GAME, options.GetTypeInfo(typeof(global::JsonSubTypes.Text.Json.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)))); + writer.WriteRawValue(JsonSerializer.Serialize(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.GameDiscriminator.WALK, options.GetTypeInfo(typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.GameDiscriminator)))); string payload = JsonSerializer.Serialize(value, options.GetTypeInfo(runtimeType)); using JsonDocument payloadDocument = JsonDocument.Parse(payload); foreach (JsonProperty property in payloadDocument.RootElement.EnumerateObject()) diff --git a/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/PersonJsonSubTypesConverter.cs b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/PersonJsonSubTypesConverter.cs new file mode 100644 index 0000000..40b1f1b --- /dev/null +++ b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/PersonJsonSubTypesConverter.cs @@ -0,0 +1,60 @@ +#nullable enable +using System; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace JsonSubTypes.Text.Json.Aot.Generated +{ + /// Generated by JsonSubTypes.Text.Json.Aot. Compiled converter for global::JsonSubTypes.Text.Json.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(); + if (root.TryGetProperty("JobTitle", out _)) + { + matches.Add(typeof(global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Employee)); + } + if (root.TryGetProperty("Skill", out _)) + { + matches.Add(typeof(global::JsonSubTypes.Text.Json.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.Text.Json.Aot.Generated.TestDomain.Person); + } + + protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Person value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + string nameFirstName = "FirstName"; + if (options.PropertyNamingPolicy != null) + { + nameFirstName = options.PropertyNamingPolicy.ConvertName(nameFirstName); + } + writer.WritePropertyName(nameFirstName); + JsonSerializer.Serialize(writer, value.FirstName, options.GetTypeInfo(typeof(string))); + writer.WriteEndObject(); + } + protected override global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Person DeserializeBase(JsonElement root, JsonSerializerOptions options) + { + global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Person instance = new global::JsonSubTypes.Text.Json.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; + } + protected override bool TryWriteNestedObject(Utf8JsonWriter writer, global::JsonSubTypes.Text.Json.Aot.Generated.TestDomain.Person value, Type runtimeType, JsonSerializerOptions options) + { + return false; + } + } +} \ No newline at end of file diff --git a/JsonSubTypes.Aot.Generated/JsonSubTypes.Aot.Generated.csproj b/JsonSubTypes.Text.Json.Aot.Generated/JsonSubTypes.Text.Json.Aot.Generated.csproj similarity index 81% rename from JsonSubTypes.Aot.Generated/JsonSubTypes.Aot.Generated.csproj rename to JsonSubTypes.Text.Json.Aot.Generated/JsonSubTypes.Text.Json.Aot.Generated.csproj index 5c0133b..2a28c0c 100644 --- a/JsonSubTypes.Aot.Generated/JsonSubTypes.Aot.Generated.csproj +++ b/JsonSubTypes.Text.Json.Aot.Generated/JsonSubTypes.Text.Json.Aot.Generated.csproj @@ -6,13 +6,13 @@ latest false + JsonSubTypes.Text.Json.Aot.Generator.Tests verify the committed files still match. --> diff --git a/JsonSubTypes.Aot.Generated/TestDomain.cs b/JsonSubTypes.Text.Json.Aot.Generated/TestDomain.cs similarity index 62% rename from JsonSubTypes.Aot.Generated/TestDomain.cs rename to JsonSubTypes.Text.Json.Aot.Generated/TestDomain.cs index 50bf9c3..edb1e6d 100644 --- a/JsonSubTypes.Aot.Generated/TestDomain.cs +++ b/JsonSubTypes.Text.Json.Aot.Generated/TestDomain.cs @@ -2,7 +2,7 @@ using System.Text.Json.Serialization; using JsonSubTypes.Text.Json; -namespace JsonSubTypes.Aot.Generated.TestDomain +namespace JsonSubTypes.Text.Json.Aot.Generated.TestDomain { [JsonSubTypesAotConverter("type")] [KnownSubType(typeof(Cat), "cat")] @@ -22,6 +22,16 @@ public class Dog : Animal public bool CanHunt { get; set; } } + public class Fox : Animal + { + public int Speed { get; set; } + } + + public class Owl : Animal + { + public int Wingspan { get; set; } + } + [JsonSubTypesAotConverter] [KnownSubTypeWithProperty(typeof(Employee), "JobTitle")] [KnownSubTypeWithProperty(typeof(Artist), "Skill")] @@ -108,9 +118,55 @@ public class Walk : Game { } + [JsonSubTypesAotConverter("type")] + [KnownSubType(typeof(NullDiscriminatorAnimal), null)] + [KnownSubType(typeof(Deer), "deer")] + public class NullDiscriminatorAnimal + { + public int Age { get; set; } + } + + public class Deer : NullDiscriminatorAnimal + { + public int AntlerSize { get; set; } + } + + [JsonSubTypesAotConverter("type", AddDiscriminatorFirst = false)] + [KnownSubType(typeof(Mammoth), "mammoth")] + public class DiscriminatorLast + { + public int Age { get; set; } + } + + public class Mammoth : DiscriminatorLast + { + public int Tusks { get; set; } + } + + [JsonSubTypesAotConverter("kind")] + [KnownSubType(typeof(DynamicCat), "cat")] + public class DynamicShape + { + public int Age { get; set; } + + public string Computed { get; } = "computed"; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Nickname { get; set; } + } + + public class DynamicCat : DynamicShape + { + public int Lives { get; set; } + } + [JsonSerializable(typeof(Animal))] [JsonSerializable(typeof(Cat))] [JsonSerializable(typeof(Dog))] + [JsonSerializable(typeof(Fox))] + [JsonSerializable(typeof(GadgetKind))] + [JsonSerializable(typeof(PayloadDiscriminator))] + [JsonSerializable(typeof(GameDiscriminator))] [JsonSerializable(typeof(Person))] [JsonSerializable(typeof(Employee))] [JsonSerializable(typeof(Artist))] @@ -123,6 +179,12 @@ public class Walk : Game [JsonSerializable(typeof(Game))] [JsonSerializable(typeof(Run))] [JsonSerializable(typeof(Walk))] + [JsonSerializable(typeof(NullDiscriminatorAnimal))] + [JsonSerializable(typeof(Deer))] + [JsonSerializable(typeof(DiscriminatorLast))] + [JsonSerializable(typeof(Mammoth))] + [JsonSerializable(typeof(DynamicShape))] + [JsonSerializable(typeof(DynamicCat))] public partial class TestDomainJsonContext : JsonSerializerContext { } diff --git a/JsonSubTypes.Text.Json.Aot.Generator.Tests/CommittedGeneratedConverterTests.cs b/JsonSubTypes.Text.Json.Aot.Generator.Tests/CommittedGeneratedConverterTests.cs new file mode 100644 index 0000000..7bd560d --- /dev/null +++ b/JsonSubTypes.Text.Json.Aot.Generator.Tests/CommittedGeneratedConverterTests.cs @@ -0,0 +1,528 @@ +#nullable enable +using System.Text.Json; +using System.Text.Json.Serialization; +using JsonSubTypes.Text.Json.Aot.Generated; +using JsonSubTypes.Text.Json.Aot.Generated.TestDomain; +using NUnit.Framework; + +namespace JsonSubTypes.Text.Json.Aot.Generator.Tests +{ + // Exercises the committed golden-master converters (the Generated/ files in + // JsonSubTypes.Text.Json.Aot.Generated). Because those files are compiled as real sources, + // coverlet measures them here and Sonar analyzes them. Every converter must be + // exercised on both serialize and deserialize so its Write/Read/SelectType paths + // are covered. + [TestFixture] + public class CommittedGeneratedConverterTests + { + private static JsonSerializerOptions Options() + { + return new JsonSerializerOptions { Converters = { GetConverter() } }; + } + + private static JsonConverter GetConverter() + { + // resolve the converter instance from the committed registry + return (JsonConverter)RegistryConverter(typeof(T)); + } + + private static object RegistryConverter(System.Type baseType) + { + var registry = typeof(JsonSubTypesAotConverters); + string memberName = baseType.Name; + return registry.GetField(memberName)?.GetValue(null) + ?? throw new System.InvalidOperationException("No committed converter for " + baseType.Name); + } + + // ---- Animal (string + int discriminators) ---- + + [Test] + public void Animal_SerializeCat_WritesDiscriminator() + { + string json = JsonSerializer.Serialize(new Cat { Age = 3, Lives = 9 }, Options()); + Assert.That(json, Does.Contain("\"type\":\"cat\"")); + } + + [Test] + public void Animal_DeserializeCatDiscriminator_ReturnsCat() + { + Animal? result = JsonSerializer.Deserialize("{\"type\":\"cat\",\"Lives\":9,\"Age\":3}", Options()); + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public void Animal_DeserializeIntDogDiscriminator_ReturnsDog() + { + Animal? result = JsonSerializer.Deserialize("{\"type\":2,\"CanHunt\":true,\"Age\":3}", Options()); + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public void Animal_DeserializeUnknownDiscriminator_FallsBackToBase() + { + Animal? result = JsonSerializer.Deserialize("{\"type\":\"fish\",\"Age\":3}", Options()); + Assert.That(result, Is.InstanceOf()); + Assert.That(result, Is.Not.InstanceOf()); + } + + // ---- Person (property presence + fallback) ---- + + [Test] + public void Person_SerializeArtist_NoDiscriminatorWritten() + { + string json = JsonSerializer.Serialize(new Artist { Skill = "Painter", FirstName = "A" }, Options()); + Assert.That(json, Does.Contain("\"Skill\"")); + } + + [Test] + public void Person_DeserializeBySkill_ReturnsArtist() + { + Person? result = JsonSerializer.Deserialize("{\"Skill\":\"Painter\",\"FirstName\":\"A\"}", Options()); + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public void Person_DeserializeUnknown_FallsBackToBase() + { + Person? result = JsonSerializer.Deserialize("{\"FirstName\":\"A\"}", Options()); + Assert.That(result, Is.InstanceOf()); + Assert.That(result, Is.Not.InstanceOf()); + } + + // ---- Gadget (enum discriminator) ---- + + [Test] + public void Gadget_SerializeElectronicCat_WritesEnumDiscriminator() + { + string json = JsonSerializer.Serialize(new ElectronicCat { Age = 3, Lives = 9 }, Options()); + Assert.That(json, Does.Contain("\"kind\":")); + } + + [Test] + public void Gadget_DeserializeElectronicCat_ReturnsSubtype() + { + Gadget? result = JsonSerializer.Deserialize("{\"kind\":0,\"Lives\":9,\"Age\":3}", Options()); + Assert.That(result, Is.InstanceOf()); + } + + // ---- DottedGadget (nested discriminator path) ---- + + [Test] + public void DottedGadget_DeserializeNestedDiscriminator_ReturnsSubtype() + { + DottedGadget? result = JsonSerializer.Deserialize( + "{\"nested\":{\"type\":\"electronic\"},\"Lives\":9,\"Age\":3}", Options()); + Assert.That(result, Is.InstanceOf()); + } + + // ---- 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() + { + Payload? result = JsonSerializer.Deserialize( + "{\"$PayloadKind\":0,\"$GameKind\":0}", PayloadAndGameOptions); + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public void Payload_DeserializeCom_ReturnsCom() + { + Payload? result = JsonSerializer.Deserialize( + "{\"$PayloadKind\":1}", PayloadOptions); + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public void Payload_SerializeRun_WritesNestedChain() + { + string json = JsonSerializer.Serialize(new Run(), PayloadAndGameOptions); + + Assert.That(json, Is.EqualTo("{\"$PayloadKind\":0,\"$GameKind\":0}")); + } + + [Test] + public void Payload_SerializeWalk_WritesNestedChain() + { + string json = JsonSerializer.Serialize(new Walk(), PayloadAndGameOptions); + + Assert.That(json, Is.EqualTo("{\"$PayloadKind\":0,\"$GameKind\":1}")); + } + + [Test] + public void Payload_SerializeCom_WritesDirectDiscriminator() + { + string json = JsonSerializer.Serialize(new Com(), PayloadOptions); + + Assert.That(json, Is.EqualTo("{\"$PayloadKind\":1}")); + } + + [Test] + public void Payload_DeserializeMissingDiscriminator_FallsBackToBase() + { + Payload? result = JsonSerializer.Deserialize("{}", PayloadOptions); + + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public void Payload_DeserializeUnknownValue_FallsBackToBase() + { + Payload? result = JsonSerializer.Deserialize("{\"$PayloadKind\":\"nope\"}", PayloadOptions); + + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public void Payload_DeserializeStringEnumNames_ReturnsSubtype() + { + Payload? game = JsonSerializer.Deserialize("{\"$PayloadKind\":\"GAME\"}", PayloadOptions); + Payload? com = JsonSerializer.Deserialize("{\"$PayloadKind\":\"COM\"}", PayloadOptions); + + Assert.That(game, Is.InstanceOf()); + Assert.That(com, Is.InstanceOf()); + } + + [Test] + public void Game_SerializeRun_WritesDiscriminator() + { + string json = JsonSerializer.Serialize(new Run(), PayloadAndGameOptions); + + Assert.That(json, Is.EqualTo("{\"$GameKind\":0}")); + } + + [Test] + public void Game_DeserializeRun_ReturnsRun() + { + Game? result = JsonSerializer.Deserialize("{\"$GameKind\":0}", PayloadAndGameOptions); + + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public void Game_DeserializeWalk_ReturnsWalk() + { + Game? result = JsonSerializer.Deserialize("{\"$GameKind\":1}", PayloadAndGameOptions); + + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public void Game_SerializeBase_WritesBaseObject() + { + string json = JsonSerializer.Serialize(new Game(), PayloadAndGameOptions); + + Assert.That(json, Is.EqualTo("{}")); + } + + [Test] + public void Game_DeserializeMissingDiscriminator_FallsBackToBase() + { + Game? result = JsonSerializer.Deserialize("{}", PayloadAndGameOptions); + + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public void Game_DeserializeUnknownValue_FallsBackToBase() + { + Game? result = JsonSerializer.Deserialize("{\"$GameKind\":\"nope\"}", PayloadAndGameOptions); + + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public void Game_DeserializeStringEnumNames_ReturnsSubtype() + { + Game? run = JsonSerializer.Deserialize("{\"$GameKind\":\"RUN\"}", PayloadAndGameOptions); + Game? walk = JsonSerializer.Deserialize("{\"$GameKind\":\"WALK\"}", PayloadAndGameOptions); + + Assert.That(run, Is.InstanceOf()); + Assert.That(walk, Is.InstanceOf()); + } + + [Test] + public void Gadget_SerializeBase_WritesBaseObject() + { + string json = JsonSerializer.Serialize(new Gadget { Age = 3 }, Options()); + + Assert.That(json, Is.EqualTo("{\"Age\":3}")); + } + + [Test] + public void Gadget_DeserializeUnknown_FallsBackToBase() + { + Gadget? result = JsonSerializer.Deserialize("{\"kind\":\"fish\",\"Age\":3}", Options()); + + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public void Gadget_DeserializeStringEnumName_ReturnsSubtype() + { + Gadget? result = JsonSerializer.Deserialize("{\"kind\":\"ElectronicCat\",\"Lives\":9,\"Age\":3}", Options()); + + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public void DottedGadget_SerializeBase_WritesBaseObject() + { + string json = JsonSerializer.Serialize(new DottedGadget { Age = 3 }, Options()); + + Assert.That(json, Is.EqualTo("{\"Age\":3}")); + } + + [Test] + public void DottedGadget_DeserializeUnknown_FallsBackToBase() + { + DottedGadget? result = JsonSerializer.Deserialize("{\"Age\":3}", Options()); + + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public void DottedGadget_SerializeElectronic_WritesDiscriminator() + { + string json = JsonSerializer.Serialize(new DottedElectronic { Age = 3, Lives = 9 }, Options()); + + Assert.That(json, Is.EqualTo("{\"nested.type\":\"electronic\",\"Lives\":9,\"Age\":3}")); + } + + [Test] + public void Person_SerializeBase_WritesBaseObject() + { + string json = JsonSerializer.Serialize(new Person { FirstName = "A" }, Options()); + + Assert.That(json, Is.EqualTo("{\"FirstName\":\"A\"}")); + } + + [Test] + public void DynamicShape_DeserializeCat_ReturnsCat() + { + DynamicShape? result = JsonSerializer.Deserialize("{\"kind\":\"cat\",\"Lives\":9,\"Age\":3}", Options()); + + Assert.That(result, Is.InstanceOf()); + } + + // ---- edge paths on the shared skeleton ---- + + [Test] + public void Animal_SerializeNull_WritesNull() + { + string json = JsonSerializer.Serialize(null, Options()); + + Assert.That(json, Is.EqualTo("null")); + } + + [Test] + public void Animal_DeserializeNull_ReturnsNull() + { + Animal? result = JsonSerializer.Deserialize("null", Options()); + + Assert.That(result, Is.Null); + } + + [Test] + public void Animal_DeserializeNonObject_Throws() + { + Assert.Throws(() => JsonSerializer.Deserialize("[1,2]", Options())); + } + + [Test] + public void Animal_SerializeBase_WritesBaseObjectWithoutDiscriminator() + { + string json = JsonSerializer.Serialize(new Animal { Age = 3 }, Options()); + + Assert.That(json, Is.EqualTo("{\"Age\":3}")); + } + + [Test] + public void Animal_SerializeUnregisteredSubtype_WritesThroughResolver() + { + string json = JsonSerializer.Serialize(new Owl { Wingspan = 40, Age = 3 }, Options()); + + Assert.That(json, Is.EqualTo("{\"Wingspan\":40,\"Age\":3}")); + } + + // ---- null discriminator ---- + + [Test] + public void NullDiscriminatorAnimal_SerializeBase_WritesNullDiscriminator() + { + string json = JsonSerializer.Serialize(new NullDiscriminatorAnimal { Age = 3 }, Options()); + + Assert.That(json, Is.EqualTo("{\"type\":null,\"Age\":3}")); + } + + [Test] + public void NullDiscriminatorAnimal_DeserializeNullDiscriminator_ReturnsBase() + { + NullDiscriminatorAnimal? result = JsonSerializer.Deserialize("{\"type\":null,\"Age\":3}", Options()); + + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public void NullDiscriminatorAnimal_SerializeDeer_WritesDiscriminator() + { + string json = JsonSerializer.Serialize(new Deer { Age = 3, AntlerSize = 5 }, Options()); + + Assert.That(json, Is.EqualTo("{\"type\":\"deer\",\"AntlerSize\":5,\"Age\":3}")); + } + + [Test] + public void NullDiscriminatorAnimal_DeserializeDeer_ReturnsDeer() + { + NullDiscriminatorAnimal? result = JsonSerializer.Deserialize("{\"type\":\"deer\",\"AntlerSize\":5,\"Age\":3}", Options()); + + Assert.That(result, Is.InstanceOf()); + } + + // ---- AddDiscriminatorFirst = false ---- + + [Test] + public void DiscriminatorLast_SerializeWritesDiscriminatorLast() + { + string json = JsonSerializer.Serialize(new Mammoth { Age = 3, Tusks = 2 }, Options()); + + Assert.That(json, Is.EqualTo("{\"Tusks\":2,\"Age\":3,\"type\":\"mammoth\"}")); + } + + [Test] + public void DiscriminatorLast_DeserializeStillWorks() + { + DiscriminatorLast? result = JsonSerializer.Deserialize("{\"type\":\"mammoth\",\"Tusks\":2,\"Age\":3}", Options()); + + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public void DiscriminatorLast_SerializeBase_WritesBaseObject() + { + string json = JsonSerializer.Serialize(new DiscriminatorLast { Age = 3 }, Options()); + + Assert.That(json, Is.EqualTo("{\"Age\":3}")); + } + + [Test] + public void DiscriminatorLast_DeserializeMissingDiscriminator_FallsBackToBase() + { + DiscriminatorLast? result = JsonSerializer.Deserialize("{}", Options()); + + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public void DiscriminatorLast_DeserializeUnknownValue_FallsBackToBase() + { + DiscriminatorLast? result = JsonSerializer.Deserialize("{\"type\":\"nope\"}", Options()); + + Assert.That(result, Is.InstanceOf()); + } + + // ---- get-only property and conditional JsonIgnore ---- + + [Test] + public void DynamicShape_SerializeBase_WritesGetterOnlyAndSkipsNullNickname() + { + string json = JsonSerializer.Serialize(new DynamicShape { Age = 3 }, Options()); + + Assert.That(json, Is.EqualTo("{\"Age\":3,\"Computed\":\"computed\"}")); + } + + [Test] + public void DynamicShape_SerializeBase_WritesNicknameWhenNotNull() + { + string json = JsonSerializer.Serialize(new DynamicShape { Age = 3, Nickname = "N" }, Options()); + + Assert.That(json, Is.EqualTo("{\"Age\":3,\"Computed\":\"computed\",\"Nickname\":\"N\"}")); + } + + [Test] + public void DynamicShape_DeserializeBase_PopulatesSettablePropertiesOnly() + { + DynamicShape? result = JsonSerializer.Deserialize("{\"Age\":3,\"Computed\":\"other\",\"Nickname\":\"N\"}", Options()); + + Assert.That(result, Is.Not.Null); + Assert.That(result!.Age, Is.EqualTo(3)); + Assert.That(result.Nickname, Is.EqualTo("N")); + Assert.That(result.Computed, Is.EqualTo("computed"), "get-only property must not be overwritten on read"); + } + + [Test] + public void DynamicShape_SerializeCat_WritesDiscriminator() + { + string json = JsonSerializer.Serialize(new DynamicCat { Age = 3, Lives = 9 }, Options()); + + Assert.That(json, Is.EqualTo("{\"kind\":\"cat\",\"Lives\":9,\"Age\":3,\"Computed\":\"computed\"}")); + } + + // ---- dynamic subtype and custom resolver on the committed Animal converter ---- + + [Test] + public void Animal_SerializeDynamicSubtype_WritesDiscriminator() + { + JsonSubTypesAotConverters.Animal.RegisterDynamicSubtype("fish", typeof(Fox)); + + try + { + string json = JsonSerializer.Serialize(new Fox { Speed = 20, Age = 3 }, Options()); + Assert.That(json, Is.EqualTo("{\"type\":\"fish\",\"Speed\":20,\"Age\":3}")); + } + finally + { + JsonSubTypesAotConverters.Animal.DynamicSubtypes.TryRemove("fish", out _); + } + } + + [Test] + public void Animal_DeserializeDynamicDiscriminator_ReturnsDynamicType() + { + JsonSubTypesAotConverters.Animal.DynamicSubtypes["fish"] = typeof(Fox); + + try + { + Animal? result = JsonSerializer.Deserialize("{\"type\":\"fish\",\"Speed\":20,\"Age\":3}", Options()); + Assert.That(result, Is.InstanceOf()); + } + finally + { + JsonSubTypesAotConverters.Animal.DynamicSubtypes.TryRemove("fish", out _); + } + } + + [Test] + public void Animal_DeserializeCustomResolver_ResolvesArbitraryName() + { + JsonSubTypesAotConverters.Animal.CustomTypeNameResolver = name => + name as string == "bird" ? typeof(Fox) : null; + + try + { + Animal? result = JsonSerializer.Deserialize("{\"type\":\"bird\",\"Speed\":20,\"Age\":3}", Options()); + Assert.That(result, Is.InstanceOf()); + } + finally + { + JsonSubTypesAotConverters.Animal.CustomTypeNameResolver = null; + } + } + } +} diff --git a/JsonSubTypes.Aot.Generator.Tests/GeneratedCodeExecutionTests.cs b/JsonSubTypes.Text.Json.Aot.Generator.Tests/GeneratedCodeExecutionTests.cs similarity index 98% rename from JsonSubTypes.Aot.Generator.Tests/GeneratedCodeExecutionTests.cs rename to JsonSubTypes.Text.Json.Aot.Generator.Tests/GeneratedCodeExecutionTests.cs index b3dcaa3..a371d83 100644 --- a/JsonSubTypes.Aot.Generator.Tests/GeneratedCodeExecutionTests.cs +++ b/JsonSubTypes.Text.Json.Aot.Generator.Tests/GeneratedCodeExecutionTests.cs @@ -7,7 +7,7 @@ using System.Text.Json.Serialization; using NUnit.Framework; -namespace JsonSubTypes.Aot.Generator.Tests +namespace JsonSubTypes.Text.Json.Aot.Generator.Tests { // Compiles the driver output (source + generated converters) into a real // assembly and runs the generated converters through reflection. This is what diff --git a/JsonSubTypes.Aot.Generator.Tests/GeneratorDriverRunner.cs b/JsonSubTypes.Text.Json.Aot.Generator.Tests/GeneratorDriverRunner.cs similarity index 97% rename from JsonSubTypes.Aot.Generator.Tests/GeneratorDriverRunner.cs rename to JsonSubTypes.Text.Json.Aot.Generator.Tests/GeneratorDriverRunner.cs index fc54151..08c3165 100644 --- a/JsonSubTypes.Aot.Generator.Tests/GeneratorDriverRunner.cs +++ b/JsonSubTypes.Text.Json.Aot.Generator.Tests/GeneratorDriverRunner.cs @@ -6,10 +6,10 @@ using System.Text.Json.Serialization; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using JsonSubTypes.Aot; +using JsonSubTypes.Text.Json.Aot; using JsonSubTypes.Text.Json; -namespace JsonSubTypes.Aot.Generator.Tests +namespace JsonSubTypes.Text.Json.Aot.Generator.Tests { internal static class GeneratorDriverRunner { diff --git a/JsonSubTypes.Text.Json.Aot.Generator.Tests/GeneratorDriverTests.cs b/JsonSubTypes.Text.Json.Aot.Generator.Tests/GeneratorDriverTests.cs new file mode 100644 index 0000000..bc64956 --- /dev/null +++ b/JsonSubTypes.Text.Json.Aot.Generator.Tests/GeneratorDriverTests.cs @@ -0,0 +1,341 @@ +#nullable enable +using System.Linq; +using NUnit.Framework; + +namespace JsonSubTypes.Text.Json.Aot.Generator.Tests +{ + [TestFixture] + public class GeneratorDriverTests + { + private const string SimpleHierarchy = @" +using System.Text.Json.Serialization; +using JsonSubTypes.Text.Json; + +[JsonSubTypesAotConverter(""type"")] +[KnownSubType(typeof(Cat), ""cat"")] +[KnownSubType(typeof(Dog), ""dog"")] +public class Animal +{ + public int Age { get; set; } +} + +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() + { + GeneratorRun run = GeneratorDriverRunner.GetRun(SimpleHierarchy); + + string[] hints = run.DriverResults.Results + .SelectMany(r => r.GeneratedSources) + .Select(s => s.HintName).OrderBy(h => h).ToArray(); + CollectionAssert.AreEqual(SingleBaseExpectedHints, hints); + } + + [Test] + public void Generate_SingleBase_RegistryExposesConverterInstance() + { + GeneratorRun run = GeneratorDriverRunner.GetRun(SimpleHierarchy); + + string? text = GeneratorDriverRunner.GetGeneratedSource(run, "JsonSubTypesAotConverters.g.cs"); + Assert.That(text, Is.Not.Null); + StringAssert.Contains("public static readonly AnimalJsonSubTypesConverter Animal", text!); + } + + [Test] + public void Generate_SingleBase_ConverterRoutesSubtypes() + { + GeneratorRun run = GeneratorDriverRunner.GetRun(SimpleHierarchy); + + string? text = GeneratorDriverRunner.GetGeneratedSource(run, "AnimalJsonSubTypesConverter.g.cs"); + Assert.That(text, Is.Not.Null); + StringAssert.Contains("class AnimalJsonSubTypesConverter : JsonSubTypesAotValueConverterBase<", text!); + StringAssert.Contains("\"cat\"", text!); + StringAssert.Contains("\"dog\"", text!); + } + + [Test] + public void Generate_NoAttribute_EmitsNothing() + { + GeneratorRun run = GeneratorDriverRunner.GetRun("public class Plain { public int X { get; set; } }"); + + Assert.That(run.DriverResults.Results.SelectMany(r => r.GeneratedSources), Is.Empty); + } + + [Test] + public void Generate_PropertyPresence_EmitsPresenceRouting() + { + const string presenceHierarchy = @" +using System.Text.Json.Serialization; +using JsonSubTypes.Text.Json; + +[JsonSubTypesAotConverter] +[KnownSubTypeWithProperty(typeof(Employee), ""JobTitle"")] +[KnownSubTypeWithProperty(typeof(Artist), ""Skill"")] +public class Person { public string? FirstName { get; set; } } + +public class Employee : Person { public string? JobTitle { get; set; } } +public class Artist : Person { public string? Skill { get; set; } } +"; + GeneratorRun run = GeneratorDriverRunner.GetRun(presenceHierarchy); + + string? text = GeneratorDriverRunner.GetGeneratedSource(run, "PersonJsonSubTypesConverter.g.cs"); + Assert.That(text, Is.Not.Null); + StringAssert.Contains("TryGetProperty(\"JobTitle\"", text!); + StringAssert.Contains("TryGetProperty(\"Skill\"", text!); + } + + [Test] + public void Generate_TwoBasesWithSameNameInDifferentNamespaces_ProducesDistinctConverters() + { + const string twoAnimals = @" +using System.Text.Json.Serialization; +using JsonSubTypes.Text.Json; + +namespace A +{ + [JsonSubTypesAotConverter(""type"")] + [KnownSubType(typeof(Cat), ""cat"")] + public class Animal { public int Age { get; set; } } + public class Cat : Animal { } +} + +namespace B +{ + [JsonSubTypesAotConverter(""type"")] + [KnownSubType(typeof(Dog), ""dog"")] + public class Animal { public int Age { get; set; } } + public class Dog : Animal { } +} +"; + GeneratorRun run = GeneratorDriverRunner.GetRun(twoAnimals); + + string[] hints = run.DriverResults.Results + .SelectMany(r => r.GeneratedSources) + .Select(s => s.HintName).OrderBy(h => h).ToArray(); + + // Both converters are emitted under distinct qualified names, not one + // overwriting the other. + Assert.That(hints, Does.Contain("global__A_AnimalJsonSubTypesConverter.g.cs")); + Assert.That(hints, Does.Contain("global__B_AnimalJsonSubTypesConverter.g.cs")); + Assert.That(hints.Count(h => h.StartsWith("Animal")), Is.EqualTo(0), + "unqualified Animal converter should not exist when names collide"); + } + + [Test] + public void Generate_BaseProperties_HonorIgnoreConditionsAndGetterOnly() + { + const string domain = @" +using System.Text.Json.Serialization; +using JsonSubTypes.Text.Json; + +[JsonSubTypesAotConverter(""kind"")] +[KnownSubType(typeof(Sub), ""sub"")] +public class Base +{ + public int Age { get; set; } + public string Computed { get { return ""x""; } } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Nickname { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int Serial { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public string? Note { get; set; } + [JsonIgnore] + public string? Secret { get; set; } +} +public class Sub : Base { } +"; + GeneratorRun run = GeneratorDriverRunner.GetRun(domain); + + string? text = GeneratorDriverRunner.GetGeneratedSource(run, "BaseJsonSubTypesConverter.g.cs"); + Assert.That(text, Is.Not.Null); + StringAssert.Contains("value.Computed", text!, "get-only property must be written"); + StringAssert.Contains("value.Nickname != null", text!, "WhenWritingNull must guard the write"); + StringAssert.Contains("EqualityComparer.Default.Equals(value.Serial, default)", text!, "WhenWritingDefault must guard the write"); + StringAssert.Contains("value.Note", text!, "Never must write the property"); + StringAssert.DoesNotContain("value.Secret", text!, "Always must drop the property"); + } + + [Test] + public void Generate_ExplicitAlwaysIgnore_DropsProperty() + { + const string domain = @" +using System.Text.Json.Serialization; +using JsonSubTypes.Text.Json; + +[JsonSubTypesAotConverter(""kind"")] +[KnownSubType(typeof(Sub), ""sub"")] +public class Base +{ + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + public string? Secret { get; set; } +} +public class Sub : Base { } +"; + GeneratorRun run = GeneratorDriverRunner.GetRun(domain); + + string? text = GeneratorDriverRunner.GetGeneratedSource(run, "BaseJsonSubTypesConverter.g.cs"); + Assert.That(text, Is.Not.Null); + StringAssert.DoesNotContain("value.Secret", text!); + } + + [Test] + public void Generate_JsonPropertyName_EmitsCustomName() + { + const string domain = @" +using System.Text.Json.Serialization; +using JsonSubTypes.Text.Json; + +[JsonSubTypesAotConverter(""kind"")] +[KnownSubType(typeof(Sub), ""sub"")] +public class Base +{ + [JsonPropertyName(""age"")] + public int Age { get; set; } +} +public class Sub : Base { } +"; + GeneratorRun run = GeneratorDriverRunner.GetRun(domain); + + string? text = GeneratorDriverRunner.GetGeneratedSource(run, "BaseJsonSubTypesConverter.g.cs"); + Assert.That(text, Is.Not.Null); + StringAssert.Contains("= \"age\";", text!); + } + + [Test] + public void Generate_UnsupportedDiscriminator_ReportsJSTAOT001() + { + const string domain = @" +using JsonSubTypes.Text.Json; + +[JsonSubTypesAotConverter(""type"")] +[KnownSubType(typeof(Sub), 1.5)] +public class Base { } +public class Sub : Base { } +"; + GeneratorRun run = GeneratorDriverRunner.GetRun(domain); + + Assert.That(run.DriverResults.Diagnostics.Select(d => d.Id), Does.Contain("JSTAOT001")); + } + + [Test] + public void Generate_BoolDiscriminator_ReportsJSTAOT001() + { + const string domain = @" +using JsonSubTypes.Text.Json; + +[JsonSubTypesAotConverter(""type"")] +[KnownSubType(typeof(Sub), true)] +public class Base { } +public class Sub : Base { } +"; + GeneratorRun run = GeneratorDriverRunner.GetRun(domain); + + Assert.That(run.DriverResults.Diagnostics.Select(d => d.Id), Does.Contain("JSTAOT001")); + } + + [Test] + public void Generate_EnumWithoutMatchingMember_ReportsJSTAOT001() + { + const string domain = @" +using JsonSubTypes.Text.Json; + +public enum Kind { A, B } + +[JsonSubTypesAotConverter(""type"")] +[KnownSubType(typeof(Sub), (Kind)99)] +public class Base { } +public class Sub : Base { } +"; + GeneratorRun run = GeneratorDriverRunner.GetRun(domain); + + Assert.That(run.DriverResults.Diagnostics.Select(d => d.Id), Does.Contain("JSTAOT001")); + } + + [Test] + public void Generate_DuplicateDiscriminators_ReportsJSTAOT002() + { + const string domain = @" +#pragma warning disable JSTAOT002 +using JsonSubTypes.Text.Json; + +[JsonSubTypesAotConverter(""type"")] +[KnownSubType(typeof(Sub), ""a"")] +[KnownSubType(typeof(Sub), ""b"")] +public class Base { } +public class Sub : Base { } +#pragma warning restore JSTAOT002 +"; + GeneratorRun run = GeneratorDriverRunner.GetRun(domain); + + Assert.That(run.DriverResults.Diagnostics.Select(d => d.Id), Does.Contain("JSTAOT002")); + } + + [Test] + public void Generate_PresenceModeValueRegistration_ReportsJSTAOT003() + { + const string domain = @" +using JsonSubTypes.Text.Json; + +[JsonSubTypesAotConverter] +[KnownSubType(typeof(Sub), ""a"")] +[KnownSubTypeWithProperty(typeof(Sub), ""Marker"")] +public class Base { } +public class Sub : Base { public int Marker { get; set; } } +"; + GeneratorRun run = GeneratorDriverRunner.GetRun(domain); + + Assert.That(run.DriverResults.Diagnostics.Select(d => d.Id), Does.Contain("JSTAOT003")); + } + + [Test] + public void Generate_AbstractBase_EmitsThrowingDeserializeBase() + { + const string domain = @" +using JsonSubTypes.Text.Json; + +[JsonSubTypesAotConverter(""type"")] +[KnownSubType(typeof(Sub), ""sub"")] +public abstract class Base { } +public class Sub : Base { } +"; + GeneratorRun run = GeneratorDriverRunner.GetRun(domain); + + string? text = GeneratorDriverRunner.GetGeneratedSource(run, "BaseJsonSubTypesConverter.g.cs"); + Assert.That(text, Is.Not.Null); + StringAssert.Contains("interface or abstract class", text!); + } + + [Test] + public void Generate_ParameterizedBase_EmitsRequiringParameterlessCtor() + { + const string domain = @" +using JsonSubTypes.Text.Json; + +[JsonSubTypesAotConverter(""type"")] +[KnownSubType(typeof(Sub), ""sub"")] +public class Base +{ + public Base(string name) { } +} +public class Sub : Base +{ + public Sub() : base(""x"") { } +} +"; + GeneratorRun run = GeneratorDriverRunner.GetRun(domain); + + string? text = GeneratorDriverRunner.GetGeneratedSource(run, "BaseJsonSubTypesConverter.g.cs"); + Assert.That(text, Is.Not.Null); + StringAssert.Contains("parameterless constructor", text!); + } + } +} diff --git a/JsonSubTypes.Aot.Generator.Tests/GoldenMasterTests.cs b/JsonSubTypes.Text.Json.Aot.Generator.Tests/GoldenMasterTests.cs similarity index 92% rename from JsonSubTypes.Aot.Generator.Tests/GoldenMasterTests.cs rename to JsonSubTypes.Text.Json.Aot.Generator.Tests/GoldenMasterTests.cs index eb3dae6..8a17635 100644 --- a/JsonSubTypes.Aot.Generator.Tests/GoldenMasterTests.cs +++ b/JsonSubTypes.Text.Json.Aot.Generator.Tests/GoldenMasterTests.cs @@ -4,7 +4,7 @@ using System.Linq; using NUnit.Framework; -namespace JsonSubTypes.Aot.Generator.Tests +namespace JsonSubTypes.Text.Json.Aot.Generator.Tests { // Golden-master test: runs the generator over the committed test domain and // compares every produced file against the committed golden-master files. Any change @@ -12,8 +12,8 @@ namespace JsonSubTypes.Aot.Generator.Tests [TestFixture] public class GoldenMasterTests { - private const string DomainPath = "JsonSubTypes.Aot.Generated/TestDomain.cs"; - private const string GeneratedDir = "JsonSubTypes.Aot.Generated/GoldenMaster"; + private const string DomainPath = "JsonSubTypes.Text.Json.Aot.Generated/TestDomain.cs"; + private const string GeneratedDir = "JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster"; private static string FindRepoRoot() { @@ -60,7 +60,7 @@ public void GeneratedFiles_MatchCurrentGeneratorOutput() string producedText = NormalizeGeneratedCode(produced[hintName]); Assert.That(producedText, Is.EqualTo(committedText), "Generator output differs from committed " + committedName + - ".\nRegenerate with: dotnet build JsonSubTypes.Aot.Generated -p:EmitCompilerGeneratedFiles=true"); + ".\nRegenerate with: dotnet build JsonSubTypes.Text.Json.Aot.Generated -p:EmitCompilerGeneratedFiles=true"); } // No extra files produced that are not committed. diff --git a/JsonSubTypes.Aot.Generator.Tests/JsonSubTypes.Aot.Generator.Tests.csproj b/JsonSubTypes.Text.Json.Aot.Generator.Tests/JsonSubTypes.Text.Json.Aot.Generator.Tests.csproj similarity index 83% rename from JsonSubTypes.Aot.Generator.Tests/JsonSubTypes.Aot.Generator.Tests.csproj rename to JsonSubTypes.Text.Json.Aot.Generator.Tests/JsonSubTypes.Text.Json.Aot.Generator.Tests.csproj index 1e75a0c..8208ae8 100644 --- a/JsonSubTypes.Aot.Generator.Tests/JsonSubTypes.Aot.Generator.Tests.csproj +++ b/JsonSubTypes.Text.Json.Aot.Generator.Tests/JsonSubTypes.Text.Json.Aot.Generator.Tests.csproj @@ -21,9 +21,9 @@ instantiate and run it in-process via the Roslyn driver, which lets coverlet measure the generator code itself. --> - + - + diff --git a/JsonSubTypes.Aot.Sample/JsonSubTypes.Aot.Sample.csproj b/JsonSubTypes.Text.Json.Aot.Sample/JsonSubTypes.Text.Json.Aot.Sample.csproj similarity index 92% rename from JsonSubTypes.Aot.Sample/JsonSubTypes.Aot.Sample.csproj rename to JsonSubTypes.Text.Json.Aot.Sample/JsonSubTypes.Text.Json.Aot.Sample.csproj index 0a22223..9eb951e 100644 --- a/JsonSubTypes.Aot.Sample/JsonSubTypes.Aot.Sample.csproj +++ b/JsonSubTypes.Text.Json.Aot.Sample/JsonSubTypes.Text.Json.Aot.Sample.csproj @@ -19,7 +19,7 @@ - diff --git a/JsonSubTypes.Aot.Sample/Program.cs b/JsonSubTypes.Text.Json.Aot.Sample/Program.cs similarity index 99% rename from JsonSubTypes.Aot.Sample/Program.cs rename to JsonSubTypes.Text.Json.Aot.Sample/Program.cs index 8796ef1..7568561 100644 --- a/JsonSubTypes.Aot.Sample/Program.cs +++ b/JsonSubTypes.Text.Json.Aot.Sample/Program.cs @@ -3,7 +3,7 @@ #pragma warning disable IL2026, IL3050 using System.Text.Json; using System.Text.Json.Serialization; -using JsonSubTypes.Aot.Generated; +using JsonSubTypes.Text.Json.Aot.Generated; using JsonSubTypes.Text.Json; var options = new JsonSerializerOptions diff --git a/JsonSubTypes.Aot.Tests/EngineParityTests.cs b/JsonSubTypes.Text.Json.Aot.Tests/EngineParityTests.cs similarity index 99% rename from JsonSubTypes.Aot.Tests/EngineParityTests.cs rename to JsonSubTypes.Text.Json.Aot.Tests/EngineParityTests.cs index 8508f20..566f4bc 100644 --- a/JsonSubTypes.Aot.Tests/EngineParityTests.cs +++ b/JsonSubTypes.Text.Json.Aot.Tests/EngineParityTests.cs @@ -4,7 +4,7 @@ using System.Text.Json; using NUnit.Framework; -namespace JsonSubTypes.Aot.Tests +namespace JsonSubTypes.Text.Json.Aot.Tests { [Flags] public enum ParityCapabilities diff --git a/JsonSubTypes.Aot.Tests/GeneratedConverterAdvancedTests.cs b/JsonSubTypes.Text.Json.Aot.Tests/GeneratedConverterAdvancedTests.cs similarity index 80% rename from JsonSubTypes.Aot.Tests/GeneratedConverterAdvancedTests.cs rename to JsonSubTypes.Text.Json.Aot.Tests/GeneratedConverterAdvancedTests.cs index 242a37a..ba79777 100644 --- a/JsonSubTypes.Aot.Tests/GeneratedConverterAdvancedTests.cs +++ b/JsonSubTypes.Text.Json.Aot.Tests/GeneratedConverterAdvancedTests.cs @@ -1,11 +1,11 @@ #nullable enable using System.Text.Json; using System.Text.Json.Serialization; -using JsonSubTypes.Aot.Generated; +using JsonSubTypes.Text.Json.Aot.Generated; using JsonSubTypes.Text.Json; using NUnit.Framework; -namespace JsonSubTypes.Aot.Tests +namespace JsonSubTypes.Text.Json.Aot.Tests { [TestFixture] public class GeneratedEnumDiscriminatorTests @@ -302,6 +302,115 @@ public class DPCat : DPAnimal { public int Lives { get; set; } } + + [TestFixture] + public class GeneratedDeepHierarchyTests +{ + // A four-level hierarchy where every intermediate is a registered base itself and + // none of them is a direct subtype of the root: the leaf is only reachable through + // the whole chain, which stresses the ancestor BFS and the outer-first chain build. + private static JsonSerializerOptions Options() + { + return new JsonSerializerOptions { Converters = { JsonSubTypesAotConverters.RootDeep } }; + } + + [Test] + public void RootDeep_SerializeLeaf_WritesFullChain() + { + string json = JsonSerializer.Serialize(new DeepLeaf { Mark = 5 }, Options()); + + Assert.AreEqual("{\"kind\":\"mid1\",\"kind\":\"mid2\",\"kind\":\"leaf\",\"Mark\":5}", json); + } + + [Test] + public void RootDeep_DeserializeFirstDiscriminator_ReturnsDeepMid1() + { + // TryGetProperty reads the first discriminator occurrence; with a single "kind" + // the chain resolves to the registered intermediate. + var result = JsonSerializer.Deserialize("{\"kind\":\"mid1\",\"Mark\":5}", Options()); + + Assert.IsInstanceOf(result); + } +} + +[JsonSubTypesAotConverter("kind")] +[KnownSubType(typeof(DeepMid1), "mid1")] +public class RootDeep +{ +} + +[JsonSubTypesAotConverter("kind")] +[KnownSubType(typeof(DeepMid2), "mid2")] +public class DeepMid1 : RootDeep +{ +} + +[JsonSubTypesAotConverter("kind")] +[KnownSubType(typeof(DeepLeaf), "leaf")] +public class DeepMid2 : DeepMid1 +{ +} + +public class DeepLeaf : DeepMid2 +{ + public int Mark { get; set; } +} + +[TestFixture] +public class GeneratedOverlappingHierarchyTests +{ + // IShared implements two registered roots (IRootA and IRootB), which is only + // possible with interfaces: the same type sits in two overlapping hierarchies. + // Each root must serialize a leaf with its OWN discriminator for IShared, not a + // mixed chain. + private static JsonSerializerOptions RootAOptions() + { + return new JsonSerializerOptions { Converters = { JsonSubTypesAotConverters.IRootA } }; + } + + private static JsonSerializerOptions RootBOptions() + { + return new JsonSerializerOptions { Converters = { JsonSubTypesAotConverters.IRootB } }; + } + + [Test] + public void RootA_SerializeLeaf_WritesOwnChain() + { + string json = JsonSerializer.Serialize(new Leaf(), RootAOptions()); + + Assert.AreEqual("{\"kind\":\"shared\",\"kind\":\"leaf\"}", json); + } + + [Test] + public void RootB_SerializeLeaf_WritesOwnChain() + { + string json = JsonSerializer.Serialize(new Leaf(), RootBOptions()); + + Assert.AreEqual("{\"kind\":\"other\",\"kind\":\"leaf\"}", json); + } +} + +[JsonSubTypesAotConverter("kind")] +[KnownSubType(typeof(IShared), "shared")] +public interface IRootA +{ +} + +[JsonSubTypesAotConverter("kind")] +[KnownSubType(typeof(IShared), "other")] +public interface IRootB +{ +} + +[JsonSubTypesAotConverter("kind")] +[KnownSubType(typeof(Leaf), "leaf")] +public interface IShared : IRootA, IRootB +{ +} + +public class Leaf : IShared +{ +} } [TestFixture] @@ -452,3 +561,4 @@ public class DNLeaf : DNMid { public int Mark { get; set; } } + diff --git a/JsonSubTypes.Aot.Tests/GeneratedConverterParityTests.cs b/JsonSubTypes.Text.Json.Aot.Tests/GeneratedConverterParityTests.cs similarity index 95% rename from JsonSubTypes.Aot.Tests/GeneratedConverterParityTests.cs rename to JsonSubTypes.Text.Json.Aot.Tests/GeneratedConverterParityTests.cs index 79afd8e..18999bd 100644 --- a/JsonSubTypes.Aot.Tests/GeneratedConverterParityTests.cs +++ b/JsonSubTypes.Text.Json.Aot.Tests/GeneratedConverterParityTests.cs @@ -1,9 +1,9 @@ #nullable enable using System.Text.Json; -using JsonSubTypes.Aot.Generated; +using JsonSubTypes.Text.Json.Aot.Generated; using NUnit.Framework; -namespace JsonSubTypes.Aot.Tests +namespace JsonSubTypes.Text.Json.Aot.Tests { /// /// Runs the shared scenarios against the generated converters, diff --git a/JsonSubTypes.Aot.Tests/GeneratedConverterTests.cs b/JsonSubTypes.Text.Json.Aot.Tests/GeneratedConverterTests.cs similarity index 98% rename from JsonSubTypes.Aot.Tests/GeneratedConverterTests.cs rename to JsonSubTypes.Text.Json.Aot.Tests/GeneratedConverterTests.cs index e521979..62ea97b 100644 --- a/JsonSubTypes.Aot.Tests/GeneratedConverterTests.cs +++ b/JsonSubTypes.Text.Json.Aot.Tests/GeneratedConverterTests.cs @@ -1,10 +1,10 @@ #nullable enable using System.Text.Json; -using JsonSubTypes.Aot.Generated; +using JsonSubTypes.Text.Json.Aot.Generated; using JsonSubTypes.Text.Json; using NUnit.Framework; -namespace JsonSubTypes.Aot.Tests +namespace JsonSubTypes.Text.Json.Aot.Tests { [TestFixture] public class GeneratedValueModeTests diff --git a/JsonSubTypes.Aot.Tests/JsonSubTypes.Aot.Tests.csproj b/JsonSubTypes.Text.Json.Aot.Tests/JsonSubTypes.Text.Json.Aot.Tests.csproj similarity index 90% rename from JsonSubTypes.Aot.Tests/JsonSubTypes.Aot.Tests.csproj rename to JsonSubTypes.Text.Json.Aot.Tests/JsonSubTypes.Text.Json.Aot.Tests.csproj index 3f27ce7..cd8858e 100644 --- a/JsonSubTypes.Aot.Tests/JsonSubTypes.Aot.Tests.csproj +++ b/JsonSubTypes.Text.Json.Aot.Tests/JsonSubTypes.Text.Json.Aot.Tests.csproj @@ -18,7 +18,7 @@ - diff --git a/JsonSubTypes.Aot.Tests/ParityDomain.cs b/JsonSubTypes.Text.Json.Aot.Tests/ParityDomain.cs similarity index 99% rename from JsonSubTypes.Aot.Tests/ParityDomain.cs rename to JsonSubTypes.Text.Json.Aot.Tests/ParityDomain.cs index 24d72ab..5a81af2 100644 --- a/JsonSubTypes.Aot.Tests/ParityDomain.cs +++ b/JsonSubTypes.Text.Json.Aot.Tests/ParityDomain.cs @@ -4,7 +4,7 @@ using System.Text.Json.Serialization; using JsonSubTypes.Text.Json; -namespace JsonSubTypes.Aot.Tests +namespace JsonSubTypes.Text.Json.Aot.Tests { // Domain types shared by the runtime-converter and generated-converter parity fixtures. // The marker + KnownSubType attributes drive the generator; the runtime fixture mirrors the diff --git a/JsonSubTypes.Aot.Tests/ResolverParityTests.cs b/JsonSubTypes.Text.Json.Aot.Tests/ResolverParityTests.cs similarity index 96% rename from JsonSubTypes.Aot.Tests/ResolverParityTests.cs rename to JsonSubTypes.Text.Json.Aot.Tests/ResolverParityTests.cs index 054e654..259a27b 100644 --- a/JsonSubTypes.Aot.Tests/ResolverParityTests.cs +++ b/JsonSubTypes.Text.Json.Aot.Tests/ResolverParityTests.cs @@ -3,7 +3,7 @@ using JsonSubTypes.Text.Json; using NUnit.Framework; -namespace JsonSubTypes.Aot.Tests +namespace JsonSubTypes.Text.Json.Aot.Tests { /// /// Runs the shared scenarios against the native resolver diff --git a/JsonSubTypes.Aot.Tests/RuntimeConverterParityTests.cs b/JsonSubTypes.Text.Json.Aot.Tests/RuntimeConverterParityTests.cs similarity index 98% rename from JsonSubTypes.Aot.Tests/RuntimeConverterParityTests.cs rename to JsonSubTypes.Text.Json.Aot.Tests/RuntimeConverterParityTests.cs index 67b3aec..934f426 100644 --- a/JsonSubTypes.Aot.Tests/RuntimeConverterParityTests.cs +++ b/JsonSubTypes.Text.Json.Aot.Tests/RuntimeConverterParityTests.cs @@ -3,7 +3,7 @@ using JsonSubTypes.Text.Json; using NUnit.Framework; -namespace JsonSubTypes.Aot.Tests +namespace JsonSubTypes.Text.Json.Aot.Tests { /// /// Runs the shared scenarios against the runtime converter, diff --git a/JsonSubTypes.Aot/AnalyzerReleases.Shipped.md b/JsonSubTypes.Text.Json.Aot/AnalyzerReleases.Shipped.md similarity index 56% rename from JsonSubTypes.Aot/AnalyzerReleases.Shipped.md rename to JsonSubTypes.Text.Json.Aot/AnalyzerReleases.Shipped.md index 0c6092a..d18bec6 100644 --- a/JsonSubTypes.Aot/AnalyzerReleases.Shipped.md +++ b/JsonSubTypes.Text.Json.Aot/AnalyzerReleases.Shipped.md @@ -7,5 +7,5 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------- -JSTAOT001 | JsonSubTypes.Aot | Warning | Discriminator value not supported by JsonSubTypes.Aot -JSTAOT002 | JsonSubTypes.Aot | Warning | Multiple discriminators on one type +JSTAOT001 | JsonSubTypes.Text.Json.Aot | Warning | Discriminator value not supported by JsonSubTypes.Text.Json.Aot +JSTAOT002 | JsonSubTypes.Text.Json.Aot | Warning | Multiple discriminators on one type diff --git a/JsonSubTypes.Aot/AnalyzerReleases.Unshipped.md b/JsonSubTypes.Text.Json.Aot/AnalyzerReleases.Unshipped.md similarity index 70% rename from JsonSubTypes.Aot/AnalyzerReleases.Unshipped.md rename to JsonSubTypes.Text.Json.Aot/AnalyzerReleases.Unshipped.md index d6b137d..d79043a 100644 --- a/JsonSubTypes.Aot/AnalyzerReleases.Unshipped.md +++ b/JsonSubTypes.Text.Json.Aot/AnalyzerReleases.Unshipped.md @@ -5,3 +5,4 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------- +JSTAOT003 | JsonSubTypes.Text.Json.Aot | Warning | Value registration ignored in property-presence mode diff --git a/JsonSubTypes.Aot/JsonSubTypes.Key.snk b/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Key.snk similarity index 100% rename from JsonSubTypes.Aot/JsonSubTypes.Key.snk rename to JsonSubTypes.Text.Json.Aot/JsonSubTypes.Key.snk diff --git a/JsonSubTypes.Aot/JsonSubTypes.Aot.csproj b/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.csproj similarity index 100% rename from JsonSubTypes.Aot/JsonSubTypes.Aot.csproj rename to JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.csproj diff --git a/JsonSubTypes.Aot/JsonSubTypesGenerator.cs b/JsonSubTypes.Text.Json.Aot/JsonSubTypesGenerator.cs similarity index 85% rename from JsonSubTypes.Aot/JsonSubTypesGenerator.cs rename to JsonSubTypes.Text.Json.Aot/JsonSubTypesGenerator.cs index 11d3e19..8cd45aa 100644 --- a/JsonSubTypes.Aot/JsonSubTypesGenerator.cs +++ b/JsonSubTypes.Text.Json.Aot/JsonSubTypesGenerator.cs @@ -6,7 +6,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Text; -namespace JsonSubTypes.Aot +namespace JsonSubTypes.Text.Json.Aot { [Generator] public sealed class JsonSubTypesGenerator : IIncrementalGenerator @@ -16,24 +16,46 @@ public sealed class JsonSubTypesGenerator : IIncrementalGenerator private const string KnownSubTypeAttributeName = "KnownSubTypeAttribute"; private const string KnownSubTypeWithPropertyAttributeName = "KnownSubTypeWithPropertyAttribute"; private const string FallBackSubTypeAttributeName = "FallBackSubTypeAttribute"; + private const string SystemTextJsonSerializationNamespace = "System.Text.Json.Serialization"; private const string DiagnosticId = "JSTAOT001"; private const string DuplicateDiscriminatorDiagnosticId = "JSTAOT002"; + private const string PresenceModeIgnoresValueDiagnosticId = "JSTAOT003"; + + // Matches an attribute by its declaring namespace and short name, so a + // homonymous attribute from another library is never mistaken for the one + // the generator understands. + private static bool IsAttribute(AttributeData attribute, string containingNamespace, string name) + { + INamedTypeSymbol? attributeClass = attribute.AttributeClass; + return attributeClass != null + && attributeClass.Name == name + && attributeClass.ContainingNamespace?.ToDisplayString() == containingNamespace; + } private static readonly DiagnosticDescriptor UnsupportedDiscriminator = new( DiagnosticId, - "Discriminator value not supported by JsonSubTypes.Aot", - "The discriminator value of type '{0}' on subtype '{1}' is not supported by JsonSubTypes.Aot. The subtype is not generated. Use the runtime converter for this hierarchy.", - "JsonSubTypes.Aot", + "Discriminator value not supported by JsonSubTypes.Text.Json.Aot", + "The discriminator value of type '{0}' on subtype '{1}' is not supported by JsonSubTypes.Text.Json.Aot. The subtype is not generated. Use the runtime converter for this hierarchy.", + "JsonSubTypes.Text.Json.Aot", DiagnosticSeverity.Warning, isEnabledByDefault: true); private static readonly DiagnosticDescriptor DuplicateDiscriminators = new( DuplicateDiscriminatorDiagnosticId, - "Multiple discriminators on one type are not supported by JsonSubTypes.Aot", + "Multiple discriminators on one type are not supported by JsonSubTypes.Text.Json.Aot", "Type '{0}' is registered with several discriminator values; only the last one is used for writing. The runtime converter's Build() rejects this configuration.", - "JsonSubTypes.Aot", + "JsonSubTypes.Text.Json.Aot", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor PresenceModeIgnoresValueRegistration = + new( + PresenceModeIgnoresValueDiagnosticId, + "Value registration ignored in property-presence mode", + "Type '{0}' is registered with a discriminator value but the hierarchy uses property-presence mode ([JsonSubTypesAotConverter] without a discriminator name). The value registration is ignored.", + "JsonSubTypes.Text.Json.Aot", DiagnosticSeverity.Warning, isEnabledByDefault: true); @@ -141,6 +163,8 @@ private sealed class BaseProperty public bool HasCustomName { get; set; } public bool HasGetter { get; set; } public bool HasSetter { get; set; } + public string IgnoreCondition { get; set; } = "Never"; // Never | Always | WhenWritingNull | WhenWritingDefault + public bool IsReferenceType { get; set; } } private static BaseTypeInfo BuildBaseTypeInfo(INamedTypeSymbol baseType, SourceProductionContext spc) @@ -166,7 +190,7 @@ private static void ReadMarkerAttribute(INamedTypeSymbol baseType, BaseTypeInfo { foreach (AttributeData attr in baseType.GetAttributes()) { - if (attr.AttributeClass?.Name != JsonSubTypesAotConverterAttributeName) + if (!IsAttribute(attr, AttributeNamespace, JsonSubTypesAotConverterAttributeName)) { continue; } @@ -192,6 +216,10 @@ private static void ProcessRegistrationAttributes(INamedTypeSymbol baseType, Bas foreach (AttributeData attr in baseType.GetAttributes()) { spc.CancellationToken.ThrowIfCancellationRequested(); + if (!IsAttribute(attr, AttributeNamespace, attr.AttributeClass?.Name ?? "")) + { + continue; + } switch (attr.AttributeClass?.Name) { case KnownSubTypeAttributeName: @@ -218,7 +246,11 @@ private static void ProcessKnownSubType(AttributeData attr, BaseTypeInfo info, S if (info.DiscriminatorPropertyName == null) { - return; // presence mode ignores value registrations + // presence mode ignores value registrations; say so instead of staying silent + spc.ReportDiagnostic(Diagnostic.Create(PresenceModeIgnoresValueRegistration, + attr.ApplicationSyntaxReference?.GetSyntax(cancellationToken).GetLocation(), + subtype.Name)); + return; } cancellationToken.ThrowIfCancellationRequested(); @@ -302,42 +334,83 @@ private static void CollectBaseProperties(INamedTypeSymbol baseType, BaseTypeInf if (member is not IPropertySymbol property || property.IsStatic || property.DeclaredAccessibility != Accessibility.Public || - property.GetMethod == null || - property.SetMethod == null) + (property.GetMethod == null && property.SetMethod == null)) + { + continue; + } + + // The base object is written with the same rules System.Text.Json uses + // for any other type: an always-ignored property is dropped from both + // WriteBaseObject and DeserializeBase, while a conditionally-ignored one + // (WhenWritingNull / WhenWritingDefault) is still read and written unless + // the condition holds. Getters drive the write, setters the read. + if (TryReadPropertyMetadata(property, out string ignoreCondition, out string? jsonName)) { continue; } - bool ignored = false; - string? jsonName = null; - foreach (AttributeData attr in property.GetAttributes()) + info.Properties.Add(new BaseProperty + { + Name = property.Name, + JsonName = jsonName ?? property.Name, + PropertyTypeName = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + HasCustomName = jsonName != null, + HasGetter = property.GetMethod != null && property.GetMethod.DeclaredAccessibility == Accessibility.Public, + HasSetter = property.SetMethod != null && + property.SetMethod.DeclaredAccessibility == Accessibility.Public && + !property.SetMethod.IsInitOnly, + IgnoreCondition = ignoreCondition, + IsReferenceType = !property.Type.IsValueType + }); + } + } + + // Returns true when the property must be excluded entirely (JsonIgnore with the + // default Always condition). Otherwise it reports the ignore condition and the + // JsonPropertyName, if any. + private static bool TryReadPropertyMetadata(IPropertySymbol property, out string ignoreCondition, out string? jsonName) + { + ignoreCondition = "Never"; + jsonName = null; + foreach (AttributeData attr in property.GetAttributes()) + { + if (IsAttribute(attr, SystemTextJsonSerializationNamespace, "JsonIgnoreAttribute")) { - switch (attr.AttributeClass?.Name) + ignoreCondition = ReadIgnoreCondition(attr); + if (ignoreCondition == AlwaysIgnore) { - case "JsonIgnoreAttribute": - ignored = true; - break; - case "JsonPropertyNameAttribute" when - attr.ConstructorArguments.Length > 0 && - attr.ConstructorArguments[0].Value is string name: - jsonName = name; - break; + return true; } } + else if (IsAttribute(attr, SystemTextJsonSerializationNamespace, "JsonPropertyNameAttribute") && + attr.ConstructorArguments.Length > 0 && + attr.ConstructorArguments[0].Value is string name) + { + jsonName = name; + } + } - if (!ignored) + return false; + } + + private static string ReadIgnoreCondition(AttributeData attr) + { + foreach (KeyValuePair namedArg in attr.NamedArguments) + { + if (namedArg is { Key: "Condition", Value.Value: int condition }) { - info.Properties.Add(new BaseProperty + return condition switch { - Name = property.Name, - JsonName = jsonName ?? property.Name, - PropertyTypeName = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), - HasCustomName = jsonName != null, - HasGetter = property.GetMethod.DeclaredAccessibility == Accessibility.Public, - HasSetter = property.SetMethod.DeclaredAccessibility == Accessibility.Public - }); + 0 => "Never", + 1 => AlwaysIgnore, + 2 => "WhenWritingDefault", + 3 => "WhenWritingNull", + _ => AlwaysIgnore + }; } } + + return AlwaysIgnore; // the JsonIgnoreAttribute default } private static bool TryGetDiscriminator(TypedConstant value, SubtypeRegistration registration) @@ -594,10 +667,10 @@ private static string EmitRegistry(List bases) // #nullable enable - namespace JsonSubTypes.Aot.Generated + namespace JsonSubTypes.Text.Json.Aot.Generated { - /// Generated by JsonSubTypes.Aot. Shared converter instances to add to JsonSerializerOptions.Converters. - [global::System.CodeDom.Compiler.GeneratedCode("JsonSubTypes.Aot", "1.0.0")] + /// Generated by JsonSubTypes.Text.Json.Aot. Shared converter instances to add to JsonSerializerOptions.Converters. + [global::System.CodeDom.Compiler.GeneratedCode("JsonSubTypes.Text.Json.Aot", "1.0.0")] public static class JsonSubTypesAotConverters { {{converters}} @@ -608,6 +681,7 @@ public static class JsonSubTypesAotConverters private const string MemberOpenBrace = " {\n"; private const string MemberCloseBrace = " }"; + private const string AlwaysIgnore = "Always"; // The shared skeleton: emitted once, exercised by every converter test. // Presence-mode converters inherit JsonSubTypesAotConverterBase (its Write @@ -622,10 +696,10 @@ public static class JsonSubTypesAotConverters using System.Text.Json; using System.Text.Json.Serialization; - namespace JsonSubTypes.Aot.Generated + namespace JsonSubTypes.Text.Json.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. - [global::System.CodeDom.Compiler.GeneratedCode("JsonSubTypes.Aot", "1.0.0")] + /// Generated by JsonSubTypes.Text.Json.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.Text.Json.Aot", "1.0.0")] public abstract class JsonSubTypesAotConverterBase : JsonConverter where T : class { public sealed override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(T); @@ -647,7 +721,7 @@ public abstract class JsonSubTypesAotConverterBase : JsonConverter where T { return DeserializeBase(root, options); } - return (T?)JsonSerializer.Deserialize(root.GetRawText(), options.GetTypeInfo(target)); + return (T?)JsonSerializer.Deserialize(root, options.GetTypeInfo(target)); } // Presence-mode write: the base object is written through WriteBaseObject, @@ -774,9 +848,10 @@ protected static bool TryGetProperty(JsonElement root, string name, JsonSerializ } if (options.PropertyNameCaseInsensitive) { - foreach (JsonProperty property in root.EnumerateObject().Where(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase))) + JsonProperty match = root.EnumerateObject().FirstOrDefault(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase)); + if (match.Value.ValueKind != JsonValueKind.Undefined) { - value = property.Value; + value = match.Value; return true; } } @@ -808,8 +883,8 @@ protected static bool TryGetValueInJson(JsonElement root, string propertyName, J } } - /// 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")] + /// Generated by JsonSubTypes.Text.Json.Aot. Value-mode converter skeleton: adds the discriminator-injection Write on top of JsonSubTypesAotConverterBase. + [global::System.CodeDom.Compiler.GeneratedCode("JsonSubTypes.Text.Json.Aot", "1.0.0")] public abstract class JsonSubTypesAotValueConverterBase : JsonSubTypesAotConverterBase where T : class { public sealed override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) @@ -916,10 +991,10 @@ private static string EmitConverter(BaseTypeInfo info) {{usingLinq}}using System.Text.Json; using System.Text.Json.Serialization; - namespace JsonSubTypes.Aot.Generated + namespace JsonSubTypes.Text.Json.Aot.Generated { - /// Generated by JsonSubTypes.Aot. Compiled converter for {{info.FullyQualifiedName}}. - [global::System.CodeDom.Compiler.GeneratedCode("JsonSubTypes.Aot", "1.0.0")] + /// Generated by JsonSubTypes.Text.Json.Aot. Compiled converter for {{info.FullyQualifiedName}}. + [global::System.CodeDom.Compiler.GeneratedCode("JsonSubTypes.Text.Json.Aot", "1.0.0")] public sealed class {{info.ConverterName}} : {{baseClass}} { {{valueModeMembers}}{{EmitSelectTypeMethod(info)}} @@ -1143,6 +1218,12 @@ private static string EmitDiscriminatorValueStatement(SubtypeRegistration reg) }; } + private static string Indent(string text, int spaces) + { + string padding = new string(' ', spaces); + return padding + text.Replace("\n", "\n" + padding); + } + private static string EmitWriteBaseObject(BaseTypeInfo info) { List writeProperties = []; @@ -1153,16 +1234,37 @@ private static string EmitWriteBaseObject(BaseTypeInfo info) continue; } - string applyPolicy = prop.HasCustomName ? "false" : "true"; - writeProperties.Add($$""" - string name{{prop.Name}} = {{SymbolDisplay.FormatLiteral(prop.JsonName, quote: true)}}; - if (options.PropertyNamingPolicy != null && {{applyPolicy}}) - { - name{{prop.Name}} = options.PropertyNamingPolicy.ConvertName(name{{prop.Name}}); - } - writer.WritePropertyName(name{{prop.Name}}); - JsonSerializer.Serialize(writer, value.{{prop.Name}}, options.GetTypeInfo(typeof({{prop.PropertyTypeName}}))); - """); + string write = + " string name" + prop.Name + " = " + SymbolDisplay.FormatLiteral(prop.JsonName, quote: true) + ";\n"; + if (!prop.HasCustomName) + { + write += " if (options.PropertyNamingPolicy != null)\n" + + " {\n" + + " name" + prop.Name + " = options.PropertyNamingPolicy.ConvertName(name" + prop.Name + ");\n" + + " }\n"; + } + write += " writer.WritePropertyName(name" + prop.Name + ");\n" + + " JsonSerializer.Serialize(writer, value." + prop.Name + ", options.GetTypeInfo(typeof(" + prop.PropertyTypeName + ")));"; + + // JsonIgnoreCondition.WhenWritingNull/WhenWritingDefault only skip the + // write when the condition holds; the property is still read on + // deserialization. WhenWritingNull never applies to value types. + if (prop.IgnoreCondition == "WhenWritingNull" && prop.IsReferenceType) + { + write = " if (value." + prop.Name + " != null)\n" + + " {\n" + + Indent(write, 4) + "\n" + + " }"; + } + else if (prop.IgnoreCondition == "WhenWritingDefault") + { + write = " if (!global::System.Collections.Generic.EqualityComparer<" + prop.PropertyTypeName + ">.Default.Equals(value." + prop.Name + ", default))\n" + + " {\n" + + Indent(write, 4) + "\n" + + " }"; + } + + writeProperties.Add(write); } string body = string.Join("\n", writeProperties); @@ -1200,12 +1302,11 @@ private static string EmitDeserializeBase(BaseTypeInfo info) continue; } - readProperties.Add($$""" - if (TryGetProperty(root, {{SymbolDisplay.FormatLiteral(prop.JsonName, quote: true)}}, options, out JsonElement {{prop.Name}}Value)) - { - instance.{{prop.Name}} = ({{prop.PropertyTypeName}})JsonSerializer.Deserialize({{prop.Name}}Value.GetRawText(), options.GetTypeInfo(typeof({{prop.PropertyTypeName}})))!; - } - """); + readProperties.Add( + " if (TryGetProperty(root, " + SymbolDisplay.FormatLiteral(prop.JsonName, quote: true) + ", options, out JsonElement " + prop.Name + "Value))\n" + + " {\n" + + " instance." + prop.Name + " = (" + prop.PropertyTypeName + ")JsonSerializer.Deserialize(" + prop.Name + "Value.GetRawText(), options.GetTypeInfo(typeof(" + prop.PropertyTypeName + ")))!;\n" + + " }"); } string body = string.Join("\n", readProperties); diff --git a/JsonSubTypes.Text.Json/JsonSubtypes.cs b/JsonSubTypes.Text.Json/JsonSubtypes.cs index 476c0cd..97b50c6 100644 --- a/JsonSubTypes.Text.Json/JsonSubtypes.cs +++ b/JsonSubTypes.Text.Json/JsonSubtypes.cs @@ -54,7 +54,7 @@ public class FallBackSubTypeAttribute(Type subType) : Attribute } /// -/// Opts a base type into the JsonSubTypes.Aot source generator, which emits a compiled +/// Opts a base type into the JsonSubTypes.Text.Json.Aot source generator, which emits a compiled /// that routes subtypes without reflection (Native AOT friendly). /// Unlike , this attribute is not a /// and does not interfere with the System.Text.Json source diff --git a/JsonSubTypes.sln b/JsonSubTypes.sln index 127fbde..58b7254 100644 --- a/JsonSubTypes.sln +++ b/JsonSubTypes.sln @@ -26,17 +26,17 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonSubTypes.Text.Json.Test EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonSubTypes.Text.Json.Tests.Plugin", "JsonSubTypes.Text.Json.Tests.Plugin\JsonSubTypes.Text.Json.Tests.Plugin.csproj", "{3503C568-B5F3-472F-BED4-52FA7EEE98A7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonSubTypes.Aot", "JsonSubTypes.Aot\JsonSubTypes.Aot.csproj", "{FCC887C5-B1FA-4E98-A934-5605F1670E8B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonSubTypes.Text.Json.Aot", "JsonSubTypes.Text.Json.Aot\JsonSubTypes.Text.Json.Aot.csproj", "{FCC887C5-B1FA-4E98-A934-5605F1670E8B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonSubTypes.Aot.Tests", "JsonSubTypes.Aot.Tests\JsonSubTypes.Aot.Tests.csproj", "{3C6AF029-C3E3-40CE-8AB9-3442CFBA7FE3}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonSubTypes.Text.Json.Aot.Tests", "JsonSubTypes.Text.Json.Aot.Tests\JsonSubTypes.Text.Json.Aot.Tests.csproj", "{3C6AF029-C3E3-40CE-8AB9-3442CFBA7FE3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonSubTypes.Aot.Sample", "JsonSubTypes.Aot.Sample\JsonSubTypes.Aot.Sample.csproj", "{7FF3FF99-AA68-4294-B94A-BE54E6D79648}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonSubTypes.Text.Json.Aot.Sample", "JsonSubTypes.Text.Json.Aot.Sample\JsonSubTypes.Text.Json.Aot.Sample.csproj", "{7FF3FF99-AA68-4294-B94A-BE54E6D79648}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonSubTypes.Benchmarks", "JsonSubTypes.Benchmarks\JsonSubTypes.Benchmarks.csproj", "{43447A7A-0C1D-4613-A7FD-0D4A8C7A11AD}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonSubTypes.Aot.Generator.Tests", "JsonSubTypes.Aot.Generator.Tests\JsonSubTypes.Aot.Generator.Tests.csproj", "{50112231-C47C-4FAD-B2E2-7A61A9486C04}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonSubTypes.Text.Json.Aot.Generator.Tests", "JsonSubTypes.Text.Json.Aot.Generator.Tests\JsonSubTypes.Text.Json.Aot.Generator.Tests.csproj", "{50112231-C47C-4FAD-B2E2-7A61A9486C04}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonSubTypes.Aot.Generated", "JsonSubTypes.Aot.Generated\JsonSubTypes.Aot.Generated.csproj", "{5A0027A7-3617-4CD7-9E87-5F84F38B58A4}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonSubTypes.Text.Json.Aot.Generated", "JsonSubTypes.Text.Json.Aot.Generated\JsonSubTypes.Text.Json.Aot.Generated.csproj", "{5A0027A7-3617-4CD7-9E87-5F84F38B58A4}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/README.md b/README.md index a6b089a..0924267 100644 --- a/README.md +++ b/README.md @@ -370,7 +370,7 @@ public interface IExpression { } `JsonSubTypes.Text.Json` ships three engines that share the same configuration layer (the attributes and `JsonSubtypesConverterBuilder`), and a parity test battery keeps them aligned: -| Feature / Capability | Native STJ (`[JsonDerivedType]`) | Resolver (`BuildResolver()`) | Converter (`Build()`) | Generator (`JsonSubTypes.Aot`) | +| Feature / Capability | Native STJ (`[JsonDerivedType]`) | Resolver (`BuildResolver()`) | Converter (`Build()`) | Generator (`JsonSubTypes.Text.Json.Aot`) | | :--- | :---: | :---: | :---: | :---: | | Type discriminator mapping (string/int) | ✅ | ✅ | ✅ | ✅ | | Enum / `null` discriminator values | ❌ | ❌ | ✅ | ✅ | @@ -390,7 +390,7 @@ public interface IExpression { } 1. **Converter (`Build()`)** — the full-featured runtime engine and the right default for non-AOT applications. 2. **Resolver (`BuildResolver()`)** — the thin native bridge: simplest and fastest, but limited to the subset the native contract model can express. -3. **Generator (`JsonSubTypes.Aot`)** — a Roslyn source generator emitting compiled converters: the Native AOT answer, with routing compiled instead of reflected. +3. **Generator (`JsonSubTypes.Text.Json.Aot`)** — a Roslyn source generator emitting compiled converters: the Native AOT answer, with routing compiled instead of reflected. ### Converter known scope & fallback path @@ -404,7 +404,7 @@ To preserve full compatibility with advanced features while delegating object se Measured with BenchmarkDotNet (`JsonSubTypes.Benchmarks`, DefaultJob, .NET 8.0, one machine; serializing/deserializing a `Cat` declared as its `Animal` base). Numbers are machine-specific but reproducible by running that project. -| Benchmark | Converter (`Build()`) | Resolver (`BuildResolver()`) | Generator (`JsonSubTypes.Aot`) | +| Benchmark | Converter (`Build()`) | Resolver (`BuildResolver()`) | Generator (`JsonSubTypes.Text.Json.Aot`) | | :--- | ---: | ---: | ---: | | Serialize | 1.65–1.70 µs / 648 B | 0.40–0.41 µs / 400 B | 1.43–1.47 µs / 440 B | | Deserialize | 2.63–2.71 µs / 1264 B | 0.51–0.55 µs / 56 B | 1.50–1.56 µs / 216 B | @@ -423,7 +423,7 @@ In steady state, Native AOT is comparable to (slightly slower than) JIT for this ### Decision matrix | Use case | Recommended | |---|---| -| Native AOT / trimming, hierarchy known at compile time | `JsonSubTypes.Aot` generator | +| Native AOT / trimming, hierarchy known at compile time | `JsonSubTypes.Text.Json.Aot` generator | | Non-AOT, full feature set with minimal setup | Converter (`Build()`) | | Non-AOT, string/int discriminators only, fastest and simplest | Resolver (`BuildResolver()`) | | Discriminator by property presence (no discriminator field in the JSON) | Converter or Generator | @@ -437,10 +437,10 @@ In steady state, Native AOT is comparable to (slightly slower than) JIT for this The resolver and the converter rely on reflection and are therefore **not compatible with trimming or Native AOT**. The polymorphic metadata that the resolver configures must be declared at compile time for AOT: `System.Text.Json` freezes it at build time, and a source-generated `JsonTypeInfo` is read-only at runtime. Assigning `PolymorphismOptions` to a source-generated `JsonTypeInfo` throws `InvalidOperationException` on both .NET 8 and .NET 10. -For Native AOT, the `JsonSubTypes.Aot` generator compiles the routing into the converter (verified to run as a native binary with `dotnet publish -r linux-x64 -p:PublishAot=true`). The generator is referenced as an analyzer and reads its attributes (`[JsonSubTypesAotConverter]`, `[KnownSubType]`, …) from the `JsonSubTypes.Text.Json` package, so reference **both** `JsonSubTypes.Aot` and `JsonSubTypes.Text.Json`: +For Native AOT, the `JsonSubTypes.Text.Json.Aot` generator compiles the routing into the converter (verified to run as a native binary with `dotnet publish -r linux-x64 -p:PublishAot=true`). The generator is referenced as an analyzer and reads its attributes (`[JsonSubTypesAotConverter]`, `[KnownSubType]`, …) from the `JsonSubTypes.Text.Json` package, so reference **both** `JsonSubTypes.Text.Json.Aot` and `JsonSubTypes.Text.Json`: ```bash -dotnet add package JsonSubTypes.Aot +dotnet add package JsonSubTypes.Text.Json.Aot dotnet add package JsonSubTypes.Text.Json ``` diff --git a/codecov.yml b/codecov.yml index 491a8f7..b9b5434 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,13 +1,13 @@ # Codecov configuration. # -# The JsonSubTypes.Aot.Generated project is the golden master: committed output of the +# The JsonSubTypes.Text.Json.Aot.Generated project is the golden master: committed output of the # source generator, used to test the generator and to let Sonar analyze generated code. # Its coverage is not a meaningful project-level metric (it is exercised only partially # by the generator tests), so it is excluded from the Codecov project and patch checks. ignore: - - "JsonSubTypes.Aot.Generated/**" - - "JsonSubTypes.Aot.Generator.Tests/**" + - "JsonSubTypes.Text.Json.Aot.Generated/**" + - "JsonSubTypes.Text.Json.Aot.Generator.Tests/**" - "**/*Tests*/**" - "**/*.g.cs"