From dcff4f45e76dce2c8b5bedc309ae8e2f3f4f6a2a Mon Sep 17 00:00:00 2001 From: manuc66 Date: Thu, 13 Aug 2026 23:46:10 +0200 Subject: [PATCH 1/8] Rename JsonSubTypes.Aot to JsonSubTypes.Text.Json.Aot The generator only targets the System.Text.Json variant, but the name JsonSubTypes.Aot did not say so and could be read as applying to the Newtonsoft JsonSubTypes port as well. Renamed the project, package, generator namespace, the generated-code namespace (JsonSubTypes.Aot.Generated -> JsonSubTypes.Text.Json.Aot.Generated), the test/sample projects, the GeneratedCode marker, the diagnostic category and the golden master. Breaking, deliberate before the 1.0.0 stable release (the package is still rc): consumers must update their using directives (JsonSubTypes.Aot.Generated -> JsonSubTypes.Text.Json.Aot.Generated) and the PackageReference name. Verified: solution build, 22 generator tests, 77 Aot tests (4 new deep-chain and interface-overlap tests), 190 Text.Json tests, 147 Newtonsoft tests, all on net8.0 and net10.0, and the sample build. --- .github/workflows/build.yml | 18 +- .github/workflows/publish.yml | 12 +- CHANGELOG.md | 6 +- .../PersonJsonSubTypesConverter.cs | 60 ------ .../JsonSubTypes.Benchmarks.csproj | 2 +- JsonSubTypes.Benchmarks/Program.cs | 2 +- .../AnimalJsonSubTypesConverter.cs | 48 ++--- .../DottedGadgetJsonSubTypesConverter.cs | 44 ++-- .../GadgetJsonSubTypesConverter.cs | 46 ++--- .../GameJsonSubTypesConverter.cs | 30 +-- .../JsonSubTypesAotConverterBases.cs | 8 +- .../JsonSubTypesAotConverters.cs | 4 +- .../PayloadJsonSubTypesConverter.cs | 42 ++-- .../PersonJsonSubTypesConverter.cs | 60 ++++++ ...sonSubTypes.Text.Json.Aot.Generated.csproj | 4 +- .../TestDomain.cs | 2 +- .../CommittedGeneratedConverterTests.cs | 8 +- .../GeneratedCodeExecutionTests.cs | 2 +- .../GeneratorDriverRunner.cs | 4 +- .../GeneratorDriverTests.cs | 2 +- .../GoldenMasterTests.cs | 8 +- ...Types.Text.Json.Aot.Generator.Tests.csproj | 4 +- .../JsonSubTypes.Text.Json.Aot.Sample.csproj | 2 +- .../Program.cs | 2 +- .../EngineParityTests.cs | 2 +- .../GeneratedConverterAdvancedTests.cs | 113 +++++++++- .../GeneratedConverterParityTests.cs | 4 +- .../GeneratedConverterTests.cs | 4 +- .../JsonSubTypes.Text.Json.Aot.Tests.csproj | 2 +- .../ParityDomain.cs | 2 +- .../ResolverParityTests.cs | 2 +- .../RuntimeConverterParityTests.cs | 2 +- .../AnalyzerReleases.Shipped.md | 4 +- .../AnalyzerReleases.Unshipped.md | 1 + .../JsonSubTypes.Key.snk | Bin .../JsonSubTypes.Text.Json.Aot.csproj | 0 .../JsonSubTypesGenerator.cs | 193 ++++++++++++------ JsonSubTypes.Text.Json/JsonSubtypes.cs | 2 +- JsonSubTypes.sln | 10 +- README.md | 12 +- codecov.yml | 6 +- 41 files changed, 484 insertions(+), 295 deletions(-) delete mode 100644 JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator/PersonJsonSubTypesConverter.cs rename {JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator => JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator}/AnimalJsonSubTypesConverter.cs (55%) rename {JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator => JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator}/DottedGadgetJsonSubTypesConverter.cs (54%) rename {JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator => JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator}/GadgetJsonSubTypesConverter.cs (54%) rename {JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator => JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator}/GameJsonSubTypesConverter.cs (57%) rename {JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator => JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator}/JsonSubTypesAotConverterBases.cs (95%) rename {JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator => JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator}/JsonSubTypesAotConverters.cs (79%) rename {JsonSubTypes.Aot.Generated/GoldenMaster/JsonSubTypes.Aot/JsonSubTypes.Aot.JsonSubTypesGenerator => JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator}/PayloadJsonSubTypesConverter.cs (58%) create mode 100644 JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/PersonJsonSubTypesConverter.cs rename JsonSubTypes.Aot.Generated/JsonSubTypes.Aot.Generated.csproj => JsonSubTypes.Text.Json.Aot.Generated/JsonSubTypes.Text.Json.Aot.Generated.csproj (81%) rename {JsonSubTypes.Aot.Generated => JsonSubTypes.Text.Json.Aot.Generated}/TestDomain.cs (98%) rename {JsonSubTypes.Aot.Generator.Tests => JsonSubTypes.Text.Json.Aot.Generator.Tests}/CommittedGeneratedConverterTests.cs (95%) rename {JsonSubTypes.Aot.Generator.Tests => JsonSubTypes.Text.Json.Aot.Generator.Tests}/GeneratedCodeExecutionTests.cs (98%) rename {JsonSubTypes.Aot.Generator.Tests => JsonSubTypes.Text.Json.Aot.Generator.Tests}/GeneratorDriverRunner.cs (97%) rename {JsonSubTypes.Aot.Generator.Tests => JsonSubTypes.Text.Json.Aot.Generator.Tests}/GeneratorDriverTests.cs (98%) rename {JsonSubTypes.Aot.Generator.Tests => JsonSubTypes.Text.Json.Aot.Generator.Tests}/GoldenMasterTests.cs (92%) rename JsonSubTypes.Aot.Generator.Tests/JsonSubTypes.Aot.Generator.Tests.csproj => JsonSubTypes.Text.Json.Aot.Generator.Tests/JsonSubTypes.Text.Json.Aot.Generator.Tests.csproj (83%) rename JsonSubTypes.Aot.Sample/JsonSubTypes.Aot.Sample.csproj => JsonSubTypes.Text.Json.Aot.Sample/JsonSubTypes.Text.Json.Aot.Sample.csproj (92%) rename {JsonSubTypes.Aot.Sample => JsonSubTypes.Text.Json.Aot.Sample}/Program.cs (99%) rename {JsonSubTypes.Aot.Tests => JsonSubTypes.Text.Json.Aot.Tests}/EngineParityTests.cs (99%) rename {JsonSubTypes.Aot.Tests => JsonSubTypes.Text.Json.Aot.Tests}/GeneratedConverterAdvancedTests.cs (80%) rename {JsonSubTypes.Aot.Tests => JsonSubTypes.Text.Json.Aot.Tests}/GeneratedConverterParityTests.cs (95%) rename {JsonSubTypes.Aot.Tests => JsonSubTypes.Text.Json.Aot.Tests}/GeneratedConverterTests.cs (98%) rename JsonSubTypes.Aot.Tests/JsonSubTypes.Aot.Tests.csproj => JsonSubTypes.Text.Json.Aot.Tests/JsonSubTypes.Text.Json.Aot.Tests.csproj (90%) rename {JsonSubTypes.Aot.Tests => JsonSubTypes.Text.Json.Aot.Tests}/ParityDomain.cs (99%) rename {JsonSubTypes.Aot.Tests => JsonSubTypes.Text.Json.Aot.Tests}/ResolverParityTests.cs (96%) rename {JsonSubTypes.Aot.Tests => JsonSubTypes.Text.Json.Aot.Tests}/RuntimeConverterParityTests.cs (98%) rename {JsonSubTypes.Aot => JsonSubTypes.Text.Json.Aot}/AnalyzerReleases.Shipped.md (56%) rename {JsonSubTypes.Aot => JsonSubTypes.Text.Json.Aot}/AnalyzerReleases.Unshipped.md (70%) rename {JsonSubTypes.Aot => JsonSubTypes.Text.Json.Aot}/JsonSubTypes.Key.snk (100%) rename JsonSubTypes.Aot/JsonSubTypes.Aot.csproj => JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.csproj (100%) rename {JsonSubTypes.Aot => JsonSubTypes.Text.Json.Aot}/JsonSubTypesGenerator.cs (86%) 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.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..a6a9476 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 && true) + { + 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.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..9702edc 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 && true) + { + 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.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..b6936b0 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 && true) + { + 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 95% 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..c341af4 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, @@ -189,7 +189,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 79% 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..f0bb7cd 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(); 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..92de50c --- /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 && true) + { + 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 98% rename from JsonSubTypes.Aot.Generated/TestDomain.cs rename to JsonSubTypes.Text.Json.Aot.Generated/TestDomain.cs index 50bf9c3..117378d 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")] diff --git a/JsonSubTypes.Aot.Generator.Tests/CommittedGeneratedConverterTests.cs b/JsonSubTypes.Text.Json.Aot.Generator.Tests/CommittedGeneratedConverterTests.cs similarity index 95% rename from JsonSubTypes.Aot.Generator.Tests/CommittedGeneratedConverterTests.cs rename to JsonSubTypes.Text.Json.Aot.Generator.Tests/CommittedGeneratedConverterTests.cs index 1a60d1d..9eabb33 100644 --- a/JsonSubTypes.Aot.Generator.Tests/CommittedGeneratedConverterTests.cs +++ b/JsonSubTypes.Text.Json.Aot.Generator.Tests/CommittedGeneratedConverterTests.cs @@ -1,14 +1,14 @@ #nullable enable using System.Text.Json; using System.Text.Json.Serialization; -using JsonSubTypes.Aot.Generated; -using JsonSubTypes.Aot.Generated.TestDomain; +using JsonSubTypes.Text.Json.Aot.Generated; +using JsonSubTypes.Text.Json.Aot.Generated.TestDomain; using NUnit.Framework; -namespace JsonSubTypes.Aot.Generator.Tests +namespace JsonSubTypes.Text.Json.Aot.Generator.Tests { // Exercises the committed golden-master converters (the Generated/ files in - // JsonSubTypes.Aot.Generated). Because those files are compiled as real sources, + // 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. 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.Aot.Generator.Tests/GeneratorDriverTests.cs b/JsonSubTypes.Text.Json.Aot.Generator.Tests/GeneratorDriverTests.cs similarity index 98% rename from JsonSubTypes.Aot.Generator.Tests/GeneratorDriverTests.cs rename to JsonSubTypes.Text.Json.Aot.Generator.Tests/GeneratorDriverTests.cs index 8fae5f9..dc94dbf 100644 --- a/JsonSubTypes.Aot.Generator.Tests/GeneratorDriverTests.cs +++ b/JsonSubTypes.Text.Json.Aot.Generator.Tests/GeneratorDriverTests.cs @@ -2,7 +2,7 @@ using System.Linq; using NUnit.Framework; -namespace JsonSubTypes.Aot.Generator.Tests +namespace JsonSubTypes.Text.Json.Aot.Generator.Tests { [TestFixture] public class GeneratorDriverTests 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..49fa349 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 @@ -452,3 +452,112 @@ public class DNLeaf : DNMid { public int Mark { 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 +{ +} 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 86% rename from JsonSubTypes.Aot/JsonSubTypesGenerator.cs rename to JsonSubTypes.Text.Json.Aot/JsonSubTypesGenerator.cs index 11d3e19..8663ff7 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().GetLocation(), + subtype.Name)); + return; } cancellationToken.ThrowIfCancellationRequested(); @@ -302,41 +334,64 @@ 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; } - bool ignored = false; + // 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. + string ignoreCondition = "Never"; string? jsonName = null; foreach (AttributeData attr in property.GetAttributes()) { - switch (attr.AttributeClass?.Name) + if (IsAttribute(attr, SystemTextJsonSerializationNamespace, "JsonIgnoreAttribute")) + { + ignoreCondition = "Always"; + foreach (KeyValuePair namedArg in attr.NamedArguments) + { + if (namedArg is { Key: "Condition", Value.Value: int condition }) + { + ignoreCondition = condition switch + { + 0 => "Never", + 1 => "Always", + 2 => "WhenWritingDefault", + 3 => "WhenWritingNull", + _ => "Always" + }; + } + } + } + else if (IsAttribute(attr, SystemTextJsonSerializationNamespace, "JsonPropertyNameAttribute") && + attr.ConstructorArguments.Length > 0 && + attr.ConstructorArguments[0].Value is string name) { - case "JsonIgnoreAttribute": - ignored = true; - break; - case "JsonPropertyNameAttribute" when - attr.ConstructorArguments.Length > 0 && - attr.ConstructorArguments[0].Value is string name: - jsonName = name; - break; + jsonName = name; } } - if (!ignored) + if (ignoreCondition == "Always") { - info.Properties.Add(new BaseProperty - { - 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 - }); + continue; } + + 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 + }); } } @@ -594,10 +649,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}} @@ -622,10 +677,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 +702,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, @@ -808,8 +863,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 +971,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 +1198,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 = []; @@ -1154,15 +1215,34 @@ private static string EmitWriteBaseObject(BaseTypeInfo info) } 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 (options.PropertyNamingPolicy != null && " + applyPolicy + ")\n" + + " {\n" + + " name" + prop.Name + " = options.PropertyNamingPolicy.ConvertName(name" + prop.Name + ");\n" + + " }\n" + + " 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 +1280,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" From e702b778f4c902197364ac9549ed02b78b2420ce Mon Sep 17 00:00:00 2001 From: manuc66 Date: Thu, 13 Aug 2026 23:49:51 +0200 Subject: [PATCH 2/8] Replace the first-match foreach by FirstOrDefault and silence the S2583 false positive The generated TryGetProperty used a foreach that returns on its first iteration, which static analysis flags (the loop never iterates more than once). FirstOrDefault expresses the lookup directly and keeps the case-insensitive fallback working. Sonar's S2583 on GetTypeByPropertyPresence ('typesFound.Count > 1' always false) is a false positive - the ambiguity throw is exercised by DemoKnownSubTypeWithPropertyTests and DemoKnownSubTypeWithMultiplePropertiesTests - but the analyzer cannot see it through HashSet growth. The ambiguity check reverts to the pre-refactor loop form (result + ambiguous flag), which is behavior-identical and not flagged. Verified by the 147 Newtonsoft tests. --- .../JsonSubTypesAotConverterBases.cs | 5 +++-- .../JsonSubTypesGenerator.cs | 5 +++-- JsonSubTypes/JsonSubtypes.cs | 17 +++++++++++++++-- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/JsonSubTypesAotConverterBases.cs b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/JsonSubTypesAotConverterBases.cs index c341af4..dc97606 100644 --- a/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/JsonSubTypesAotConverterBases.cs +++ b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/JsonSubTypesAotConverterBases.cs @@ -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; } } diff --git a/JsonSubTypes.Text.Json.Aot/JsonSubTypesGenerator.cs b/JsonSubTypes.Text.Json.Aot/JsonSubTypesGenerator.cs index 8663ff7..480ebe6 100644 --- a/JsonSubTypes.Text.Json.Aot/JsonSubTypesGenerator.cs +++ b/JsonSubTypes.Text.Json.Aot/JsonSubTypesGenerator.cs @@ -829,9 +829,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; } } diff --git a/JsonSubTypes/JsonSubtypes.cs b/JsonSubTypes/JsonSubtypes.cs index 0e25612..5651be5 100644 --- a/JsonSubTypes/JsonSubtypes.cs +++ b/JsonSubTypes/JsonSubtypes.cs @@ -339,7 +339,20 @@ private Type GetTypeByPropertyPresence(JObject jObject, Type parentType) typesFound.Add(matchingKnownType); } - if (typesFound.Count > 1) + Type result = null; + bool ambiguous = false; + foreach (Type matchingType in typesFound) + { + if (result != null) + { + ambiguous = true; + break; + } + + result = matchingType; + } + + if (ambiguous) { throw new JsonSerializationException( "Ambiguous type resolution, expected only one type but got: " + @@ -347,7 +360,7 @@ private Type GetTypeByPropertyPresence(JObject jObject, Type parentType) ); } - return typesFound.SingleOrDefault(); + return result; } internal virtual List GetTypesByPropertyPresence(Type parentType) From 98314856c68a2c290f835101fcae003738e29f9f Mon Sep 17 00:00:00 2001 From: manuc66 Date: Thu, 13 Aug 2026 23:54:37 +0200 Subject: [PATCH 3/8] Cut CollectBaseProperties' cognitive complexity by extracting the attribute parsing The JsonIgnore-condition + JsonPropertyName parsing grew the method to cognitive complexity 22 (limit 15). The per-property attribute reading moves to TryReadPropertyMetadata (with an early true for always-ignored properties) and ReadIgnoreCondition. Emitted code is unchanged, verified by the 22 golden-master driver tests. --- .../JsonSubTypesGenerator.cs | 80 ++++++++++++------- 1 file changed, 49 insertions(+), 31 deletions(-) diff --git a/JsonSubTypes.Text.Json.Aot/JsonSubTypesGenerator.cs b/JsonSubTypes.Text.Json.Aot/JsonSubTypesGenerator.cs index 480ebe6..15e9435 100644 --- a/JsonSubTypes.Text.Json.Aot/JsonSubTypesGenerator.cs +++ b/JsonSubTypes.Text.Json.Aot/JsonSubTypesGenerator.cs @@ -344,37 +344,7 @@ private static void CollectBaseProperties(INamedTypeSymbol baseType, BaseTypeInf // 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. - string ignoreCondition = "Never"; - string? jsonName = null; - foreach (AttributeData attr in property.GetAttributes()) - { - if (IsAttribute(attr, SystemTextJsonSerializationNamespace, "JsonIgnoreAttribute")) - { - ignoreCondition = "Always"; - foreach (KeyValuePair namedArg in attr.NamedArguments) - { - if (namedArg is { Key: "Condition", Value.Value: int condition }) - { - ignoreCondition = condition switch - { - 0 => "Never", - 1 => "Always", - 2 => "WhenWritingDefault", - 3 => "WhenWritingNull", - _ => "Always" - }; - } - } - } - else if (IsAttribute(attr, SystemTextJsonSerializationNamespace, "JsonPropertyNameAttribute") && - attr.ConstructorArguments.Length > 0 && - attr.ConstructorArguments[0].Value is string name) - { - jsonName = name; - } - } - - if (ignoreCondition == "Always") + if (TryReadPropertyMetadata(property, out string ignoreCondition, out string? jsonName)) { continue; } @@ -395,6 +365,54 @@ private static void CollectBaseProperties(INamedTypeSymbol baseType, BaseTypeInf } } + // 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")) + { + ignoreCondition = ReadIgnoreCondition(attr); + if (ignoreCondition == "Always") + { + return true; + } + } + else if (IsAttribute(attr, SystemTextJsonSerializationNamespace, "JsonPropertyNameAttribute") && + attr.ConstructorArguments.Length > 0 && + attr.ConstructorArguments[0].Value is string name) + { + jsonName = name; + } + } + + return false; + } + + private static string ReadIgnoreCondition(AttributeData attr) + { + foreach (KeyValuePair namedArg in attr.NamedArguments) + { + if (namedArg is { Key: "Condition", Value.Value: int condition }) + { + return condition switch + { + 0 => "Never", + 1 => "Always", + 2 => "WhenWritingDefault", + 3 => "WhenWritingNull", + _ => "Always" + }; + } + } + + return "Always"; // the JsonIgnoreAttribute default + } + private static bool TryGetDiscriminator(TypedConstant value, SubtypeRegistration registration) { if (value.Value == null) From 7cc896178095f07525ddf2cb9e11e4f867491b3b Mon Sep 17 00:00:00 2001 From: manuc66 Date: Fri, 14 Aug 2026 00:06:08 +0200 Subject: [PATCH 4/8] Raise new-code coverage past the gate and clear the remaining analyzer findings Sonar's quality gate on the follow-up PR failed two conditions: reliability rating (S8949) and 64.5% coverage on new code (< 80%). Generator: CollectBaseProperties was already split (previous commit); the remaining findings were S8949 (GetSyntax now receives the cancellation token), S1192 ('Always' is a const) and S3776 on the rewritten attribute parsing. JsonSubtypes.cs reverts to the Count > 1 form that matches master, so the S2583/S4158 false positives (Sonar cannot model HashSet growth; the ambiguity throw is covered by the DemoKnownSubTypeWith* tests) stop counting as new code on the PR. Coverage: the golden-master domain gains NullDiscriminatorAnimal (null discriminator + base registered with a discriminator, exercising SerializeBasePayload), DiscriminatorLast (AddDiscriminatorFirst=false), DynamicShape (get-only property and JsonIgnore(WhenWritingNull)) and Fox (an unregistered subtype for the dynamic-write path). CommittedGeneratedConverterTests grows from 22 to 54 tests covering the shared skeleton's edge paths (null serialize/deserialize, non-object throw, base-object write), the dynamic subtype and custom resolver paths, the nested chains of Payload/Game, and the conditional-ignore/get-only write rules. The test types are namespaced (CA1050). Measured locally from the cobertura reports: generator 86.3%, golden master (weighted) 65.6%, aggregate 80.4% line coverage. Verified: solution build, 54 generator tests, 77 Aot tests, 190 Text.Json tests, 147 Newtonsoft tests, net8.0 and net10.0. --- .../DiscriminatorLastJsonSubTypesConverter.cs | 87 ++++++ .../DynamicShapeJsonSubTypesConverter.cs | 106 +++++++ .../JsonSubTypesAotConverters.cs | 3 + ...iscriminatorAnimalJsonSubTypesConverter.cs | 88 ++++++ .../TestDomain.cs | 57 ++++ .../CommittedGeneratedConverterTests.cs | 292 ++++++++++++++++++ .../GeneratedConverterAdvancedTests.cs | 7 +- .../JsonSubTypesGenerator.cs | 11 +- JsonSubTypes/JsonSubtypes.cs | 17 +- 9 files changed, 646 insertions(+), 22 deletions(-) create mode 100644 JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/DiscriminatorLastJsonSubTypesConverter.cs create mode 100644 JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/DynamicShapeJsonSubTypesConverter.cs create mode 100644 JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/NullDiscriminatorAnimalJsonSubTypesConverter.cs 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..10c9e66 --- /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 && true) + { + 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.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..7b8f133 --- /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 && true) + { + nameAge = options.PropertyNamingPolicy.ConvertName(nameAge); + } + writer.WritePropertyName(nameAge); + JsonSerializer.Serialize(writer, value.Age, options.GetTypeInfo(typeof(int))); + string nameComputed = "Computed"; + if (options.PropertyNamingPolicy != null && true) + { + 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 && true) + { + 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.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/JsonSubTypesAotConverters.cs b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/JsonSubTypesAotConverters.cs index f0bb7cd..eefd2ec 100644 --- a/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/JsonSubTypesAotConverters.cs +++ b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/JsonSubTypesAotConverters.cs @@ -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..b20f33e --- /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 && true) + { + 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.Text.Json.Aot.Generated/TestDomain.cs b/JsonSubTypes.Text.Json.Aot.Generated/TestDomain.cs index 117378d..0317359 100644 --- a/JsonSubTypes.Text.Json.Aot.Generated/TestDomain.cs +++ b/JsonSubTypes.Text.Json.Aot.Generated/TestDomain.cs @@ -22,6 +22,11 @@ public class Dog : Animal public bool CanHunt { get; set; } } + public class Fox : Animal + { + public int Speed { get; set; } + } + [JsonSubTypesAotConverter] [KnownSubTypeWithProperty(typeof(Employee), "JobTitle")] [KnownSubTypeWithProperty(typeof(Artist), "Skill")] @@ -108,9 +113,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 +174,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 index 9eabb33..3899532 100644 --- a/JsonSubTypes.Text.Json.Aot.Generator.Tests/CommittedGeneratedConverterTests.cs +++ b/JsonSubTypes.Text.Json.Aot.Generator.Tests/CommittedGeneratedConverterTests.cs @@ -148,5 +148,297 @@ public void Payload_DeserializeCom_ReturnsCom() "{\"$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 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 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 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}")); + } + + // ---- 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}")); + } + + // ---- 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.Text.Json.Aot.Tests/GeneratedConverterAdvancedTests.cs b/JsonSubTypes.Text.Json.Aot.Tests/GeneratedConverterAdvancedTests.cs index 49fa349..ef53b29 100644 --- a/JsonSubTypes.Text.Json.Aot.Tests/GeneratedConverterAdvancedTests.cs +++ b/JsonSubTypes.Text.Json.Aot.Tests/GeneratedConverterAdvancedTests.cs @@ -453,8 +453,10 @@ public class DNLeaf : DNMid public int Mark { get; set; } } -[TestFixture] -public class GeneratedDeepHierarchyTests +namespace JsonSubTypes.Text.Json.Aot.Tests +{ + [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 @@ -561,3 +563,4 @@ public interface IShared : IRootA, IRootB public class Leaf : IShared { } +} diff --git a/JsonSubTypes.Text.Json.Aot/JsonSubTypesGenerator.cs b/JsonSubTypes.Text.Json.Aot/JsonSubTypesGenerator.cs index 15e9435..aa6079c 100644 --- a/JsonSubTypes.Text.Json.Aot/JsonSubTypesGenerator.cs +++ b/JsonSubTypes.Text.Json.Aot/JsonSubTypesGenerator.cs @@ -248,7 +248,7 @@ private static void ProcessKnownSubType(AttributeData attr, BaseTypeInfo info, S { // presence mode ignores value registrations; say so instead of staying silent spc.ReportDiagnostic(Diagnostic.Create(PresenceModeIgnoresValueRegistration, - attr.ApplicationSyntaxReference?.GetSyntax().GetLocation(), + attr.ApplicationSyntaxReference?.GetSyntax(cancellationToken).GetLocation(), subtype.Name)); return; } @@ -377,7 +377,7 @@ private static bool TryReadPropertyMetadata(IPropertySymbol property, out string if (IsAttribute(attr, SystemTextJsonSerializationNamespace, "JsonIgnoreAttribute")) { ignoreCondition = ReadIgnoreCondition(attr); - if (ignoreCondition == "Always") + if (ignoreCondition == AlwaysIgnore) { return true; } @@ -402,15 +402,15 @@ private static string ReadIgnoreCondition(AttributeData attr) return condition switch { 0 => "Never", - 1 => "Always", + 1 => AlwaysIgnore, 2 => "WhenWritingDefault", 3 => "WhenWritingNull", - _ => "Always" + _ => AlwaysIgnore }; } } - return "Always"; // the JsonIgnoreAttribute default + return AlwaysIgnore; // the JsonIgnoreAttribute default } private static bool TryGetDiscriminator(TypedConstant value, SubtypeRegistration registration) @@ -681,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 diff --git a/JsonSubTypes/JsonSubtypes.cs b/JsonSubTypes/JsonSubtypes.cs index 5651be5..0e25612 100644 --- a/JsonSubTypes/JsonSubtypes.cs +++ b/JsonSubTypes/JsonSubtypes.cs @@ -339,20 +339,7 @@ private Type GetTypeByPropertyPresence(JObject jObject, Type parentType) typesFound.Add(matchingKnownType); } - Type result = null; - bool ambiguous = false; - foreach (Type matchingType in typesFound) - { - if (result != null) - { - ambiguous = true; - break; - } - - result = matchingType; - } - - if (ambiguous) + if (typesFound.Count > 1) { throw new JsonSerializationException( "Ambiguous type resolution, expected only one type but got: " + @@ -360,7 +347,7 @@ private Type GetTypeByPropertyPresence(JObject jObject, Type parentType) ); } - return result; + return typesFound.SingleOrDefault(); } internal virtual List GetTypesByPropertyPresence(Type parentType) From ac2b0829896d7b6eb30d7020edef2c0db937bfcd Mon Sep 17 00:00:00 2001 From: manuc66 Date: Fri, 14 Aug 2026 00:12:31 +0200 Subject: [PATCH 5/8] Keep a single namespace per file in the Aot tests The deep-hierarchy and interface-overlap fixtures were appended in a second namespace block, which the single-namespace-per-file rule flags. They move into the file's existing namespace block instead; the pre-existing global-namespace domain types stay untouched. --- .../GeneratedConverterAdvancedTests.cs | 220 +++++++++--------- 1 file changed, 109 insertions(+), 111 deletions(-) diff --git a/JsonSubTypes.Text.Json.Aot.Tests/GeneratedConverterAdvancedTests.cs b/JsonSubTypes.Text.Json.Aot.Tests/GeneratedConverterAdvancedTests.cs index ef53b29..ba79777 100644 --- a/JsonSubTypes.Text.Json.Aot.Tests/GeneratedConverterAdvancedTests.cs +++ b/JsonSubTypes.Text.Json.Aot.Tests/GeneratedConverterAdvancedTests.cs @@ -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] @@ -453,114 +562,3 @@ public class DNLeaf : DNMid public int Mark { get; set; } } -namespace JsonSubTypes.Text.Json.Aot.Tests -{ - [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 -{ -} -} From 3466aa7429cf55db6b38e6ab5f087457af01b831 Mon Sep 17 00:00:00 2001 From: manuc66 Date: Fri, 14 Aug 2026 00:18:17 +0200 Subject: [PATCH 6/8] Drop the redundant && true in the emitted base write and cover the ignore-condition branches The WriteBaseObject emitted '&& true' / '&& false' next to the naming-policy check (S1125). The policy block is now emitted only for properties without a custom name. A driver test runs the generator over a base exercising all four JsonIgnoreCondition values plus a get-only property, which covers the ReadIgnoreCondition switch cases and the conditional-write branches. --- .../AnimalJsonSubTypesConverter.cs | 2 +- .../DiscriminatorLastJsonSubTypesConverter.cs | 2 +- .../DottedGadgetJsonSubTypesConverter.cs | 2 +- .../DynamicShapeJsonSubTypesConverter.cs | 6 ++-- .../GadgetJsonSubTypesConverter.cs | 2 +- ...iscriminatorAnimalJsonSubTypesConverter.cs | 2 +- .../PersonJsonSubTypesConverter.cs | 2 +- .../GeneratorDriverTests.cs | 35 +++++++++++++++++++ .../JsonSubTypesGenerator.cs | 18 +++++----- 9 files changed, 54 insertions(+), 17 deletions(-) diff --git a/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/AnimalJsonSubTypesConverter.cs b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/AnimalJsonSubTypesConverter.cs index a6a9476..b19bd65 100644 --- a/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/AnimalJsonSubTypesConverter.cs +++ b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/AnimalJsonSubTypesConverter.cs @@ -67,7 +67,7 @@ protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTy { writer.WriteStartObject(); string nameAge = "Age"; - if (options.PropertyNamingPolicy != null && true) + if (options.PropertyNamingPolicy != null) { nameAge = options.PropertyNamingPolicy.ConvertName(nameAge); } 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 index 10c9e66..85a7807 100644 --- 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 @@ -62,7 +62,7 @@ protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTy { writer.WriteStartObject(); string nameAge = "Age"; - if (options.PropertyNamingPolicy != null && true) + if (options.PropertyNamingPolicy != null) { nameAge = options.PropertyNamingPolicy.ConvertName(nameAge); } diff --git a/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/DottedGadgetJsonSubTypesConverter.cs b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/DottedGadgetJsonSubTypesConverter.cs index 9702edc..19876ca 100644 --- a/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/DottedGadgetJsonSubTypesConverter.cs +++ b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/DottedGadgetJsonSubTypesConverter.cs @@ -60,7 +60,7 @@ protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTy { writer.WriteStartObject(); string nameAge = "Age"; - if (options.PropertyNamingPolicy != null && true) + if (options.PropertyNamingPolicy != null) { nameAge = options.PropertyNamingPolicy.ConvertName(nameAge); } 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 index 7b8f133..5147cf1 100644 --- 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 @@ -60,14 +60,14 @@ protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTy { writer.WriteStartObject(); string nameAge = "Age"; - if (options.PropertyNamingPolicy != null && true) + 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 && true) + if (options.PropertyNamingPolicy != null) { nameComputed = options.PropertyNamingPolicy.ConvertName(nameComputed); } @@ -76,7 +76,7 @@ protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTy if (value.Nickname != null) { string nameNickname = "Nickname"; - if (options.PropertyNamingPolicy != null && true) + if (options.PropertyNamingPolicy != null) { nameNickname = options.PropertyNamingPolicy.ConvertName(nameNickname); } diff --git a/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/GadgetJsonSubTypesConverter.cs b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/GadgetJsonSubTypesConverter.cs index b6936b0..a879cc8 100644 --- a/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/GadgetJsonSubTypesConverter.cs +++ b/JsonSubTypes.Text.Json.Aot.Generated/GoldenMaster/JsonSubTypes.Text.Json.Aot/JsonSubTypes.Text.Json.Aot.JsonSubTypesGenerator/GadgetJsonSubTypesConverter.cs @@ -66,7 +66,7 @@ protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTy { writer.WriteStartObject(); string nameAge = "Age"; - if (options.PropertyNamingPolicy != null && true) + if (options.PropertyNamingPolicy != null) { nameAge = options.PropertyNamingPolicy.ConvertName(nameAge); } 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 index b20f33e..031013a 100644 --- 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 @@ -63,7 +63,7 @@ protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTy { writer.WriteStartObject(); string nameAge = "Age"; - if (options.PropertyNamingPolicy != null && true) + if (options.PropertyNamingPolicy != null) { nameAge = options.PropertyNamingPolicy.ConvertName(nameAge); } 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 index 92de50c..40b1f1b 100644 --- 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 @@ -35,7 +35,7 @@ protected override void WriteBaseObject(Utf8JsonWriter writer, global::JsonSubTy { writer.WriteStartObject(); string nameFirstName = "FirstName"; - if (options.PropertyNamingPolicy != null && true) + if (options.PropertyNamingPolicy != null) { nameFirstName = options.PropertyNamingPolicy.ConvertName(nameFirstName); } diff --git a/JsonSubTypes.Text.Json.Aot.Generator.Tests/GeneratorDriverTests.cs b/JsonSubTypes.Text.Json.Aot.Generator.Tests/GeneratorDriverTests.cs index dc94dbf..f4b129d 100644 --- a/JsonSubTypes.Text.Json.Aot.Generator.Tests/GeneratorDriverTests.cs +++ b/JsonSubTypes.Text.Json.Aot.Generator.Tests/GeneratorDriverTests.cs @@ -128,5 +128,40 @@ public class Dog : Animal { } 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"); + } } } diff --git a/JsonSubTypes.Text.Json.Aot/JsonSubTypesGenerator.cs b/JsonSubTypes.Text.Json.Aot/JsonSubTypesGenerator.cs index aa6079c..8cd45aa 100644 --- a/JsonSubTypes.Text.Json.Aot/JsonSubTypesGenerator.cs +++ b/JsonSubTypes.Text.Json.Aot/JsonSubTypesGenerator.cs @@ -1234,15 +1234,17 @@ private static string EmitWriteBaseObject(BaseTypeInfo info) continue; } - string applyPolicy = prop.HasCustomName ? "false" : "true"; string write = - " string name" + prop.Name + " = " + SymbolDisplay.FormatLiteral(prop.JsonName, quote: true) + ";\n" + - " if (options.PropertyNamingPolicy != null && " + applyPolicy + ")\n" + - " {\n" + - " name" + prop.Name + " = options.PropertyNamingPolicy.ConvertName(name" + prop.Name + ");\n" + - " }\n" + - " writer.WritePropertyName(name" + prop.Name + ");\n" + - " JsonSerializer.Serialize(writer, value." + prop.Name + ", options.GetTypeInfo(typeof(" + prop.PropertyTypeName + ")));"; + " 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 From 8ab17b36f631ed24789d0fc3243a653c82c41bf7 Mon Sep 17 00:00:00 2001 From: manuc66 Date: Fri, 14 Aug 2026 00:24:12 +0200 Subject: [PATCH 7/8] Cover the fallback and string-enum paths of the committed converters Adds tests that exercise each converter's SelectType fallbacks (missing or unknown discriminator, which also reach DeserializeBase and the ResolveStaticType null return), the string-form enum discriminators (RUN/WALK, GAME/COM, ElectronicCat), the unregistered-subtype write path in the shared base (TryWriteNestedObject/TryWriteDynamic fall-throughs) and the shared dynamic / custom-resolver paths. A separate unregistered Owl type keeps the unregistered test independent of the dynamic-registration test, which leaves a stale reverse entry in the private _dynamicReverse map. --- .../TestDomain.cs | 5 ++ .../CommittedGeneratedConverterTests.cs | 84 +++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/JsonSubTypes.Text.Json.Aot.Generated/TestDomain.cs b/JsonSubTypes.Text.Json.Aot.Generated/TestDomain.cs index 0317359..edb1e6d 100644 --- a/JsonSubTypes.Text.Json.Aot.Generated/TestDomain.cs +++ b/JsonSubTypes.Text.Json.Aot.Generated/TestDomain.cs @@ -27,6 +27,11 @@ 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")] diff --git a/JsonSubTypes.Text.Json.Aot.Generator.Tests/CommittedGeneratedConverterTests.cs b/JsonSubTypes.Text.Json.Aot.Generator.Tests/CommittedGeneratedConverterTests.cs index 3899532..7bd560d 100644 --- a/JsonSubTypes.Text.Json.Aot.Generator.Tests/CommittedGeneratedConverterTests.cs +++ b/JsonSubTypes.Text.Json.Aot.Generator.Tests/CommittedGeneratedConverterTests.cs @@ -173,6 +173,32 @@ public void Payload_SerializeCom_WritesDirectDiscriminator() 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() { @@ -205,6 +231,32 @@ public void Game_SerializeBase_WritesBaseObject() 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() { @@ -221,6 +273,14 @@ public void Gadget_DeserializeUnknown_FallsBackToBase() 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() { @@ -293,6 +353,14 @@ public void Animal_SerializeBase_WritesBaseObjectWithoutDiscriminator() 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] @@ -353,6 +421,22 @@ public void DiscriminatorLast_SerializeBase_WritesBaseObject() 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] From 4ef8091371dd2e502e866dde94cbfd51201e91d0 Mon Sep 17 00:00:00 2001 From: manuc66 Date: Fri, 14 Aug 2026 00:32:32 +0200 Subject: [PATCH 8/8] Exercise the generator's diagnostic and edge emission paths in driver tests Covers the branches the golden-master domain cannot reach: JSTAOT001 for unsupported discriminators (double, bool, enum without a matching member), JSTAOT002 for duplicates, JSTAOT003 for value registrations ignored in presence mode, JsonPropertyName on a base property, an explicit JsonIgnore(Always) condition, and the abstract-base / parameterized-base DeserializeBase emissions. Generator line coverage goes to 98.4%. --- .../GeneratorDriverTests.cs | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) diff --git a/JsonSubTypes.Text.Json.Aot.Generator.Tests/GeneratorDriverTests.cs b/JsonSubTypes.Text.Json.Aot.Generator.Tests/GeneratorDriverTests.cs index f4b129d..bc64956 100644 --- a/JsonSubTypes.Text.Json.Aot.Generator.Tests/GeneratorDriverTests.cs +++ b/JsonSubTypes.Text.Json.Aot.Generator.Tests/GeneratorDriverTests.cs @@ -163,5 +163,179 @@ public class Sub : Base { } 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!); + } } }