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