diff --git a/Directory.Packages.props b/Directory.Packages.props
index 94d8c431d..531bffd25 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -14,9 +14,12 @@
+
+
+
diff --git a/src/NJsonSchema.Demo/NJsonSchema.Demo.csproj b/src/NJsonSchema.Demo/NJsonSchema.Demo.csproj
index 7ee582559..2313820dd 100644
--- a/src/NJsonSchema.Demo/NJsonSchema.Demo.csproj
+++ b/src/NJsonSchema.Demo/NJsonSchema.Demo.csproj
@@ -1,4 +1,5 @@
+
net8.0
@@ -9,14 +10,26 @@
disable
-
+
+
+
+
+
+
diff --git a/src/NJsonSchema.Demo/schema.json b/src/NJsonSchema.Demo/schema.json
new file mode 100644
index 000000000..c0634b07f
--- /dev/null
+++ b/src/NJsonSchema.Demo/schema.json
@@ -0,0 +1,21 @@
+{
+ "$id": "https://example.com/person.schema.json",
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "title": "Person",
+ "type": "object",
+ "properties": {
+ "firstName": {
+ "type": "string",
+ "description": "The person's first name."
+ },
+ "lastName": {
+ "type": "string",
+ "description": "The person's last name."
+ },
+ "age": {
+ "description": "Age in years which must be equal to or greater than zero.",
+ "type": "integer",
+ "optional": true
+ }
+ }
+}
diff --git a/src/NJsonSchema.SourceGenerators.CSharp.Tests/JsonSchemaSourceGeneratorTests.cs b/src/NJsonSchema.SourceGenerators.CSharp.Tests/JsonSchemaSourceGeneratorTests.cs
new file mode 100644
index 000000000..fe29e843b
--- /dev/null
+++ b/src/NJsonSchema.SourceGenerators.CSharp.Tests/JsonSchemaSourceGeneratorTests.cs
@@ -0,0 +1,269 @@
+using Microsoft.CodeAnalysis;
+using System.Collections.Generic;
+using System.Linq;
+using Xunit.Abstractions;
+using static NJsonSchema.SourceGenerators.CSharp.GeneratorConfigurationKeys;
+
+namespace NJsonSchema.SourceGenerators.CSharp.Tests
+{
+ public class JsonSchemaSourceGeneratorTests : TestsBase
+ {
+ public JsonSchemaSourceGeneratorTests(ITestOutputHelper output) : base(output)
+ {
+ }
+
+ [Fact]
+ public void When_no_additional_files_specified_then_no_source_is_generated()
+ {
+ var (compilation, outputDiagnostics) = GetGeneratedOutput(null, []);
+
+ Assert.Empty(outputDiagnostics);
+ Assert.Empty(compilation.SyntaxTrees);
+ }
+
+ [Fact]
+ public void When_invalid_path_specified_then_nothing_is_generated()
+ {
+ var (compilation, outputDiagnostics) = GetGeneratedOutput(null, [new AdditionalTextStub("not_existing.json")]);
+
+ Assert.NotEmpty(outputDiagnostics);
+ Assert.Single(outputDiagnostics);
+ var outputDiagnostic = outputDiagnostics[0];
+ Assert.Equal("NJSG001", outputDiagnostic.Id);
+ Assert.Equal(DiagnosticSeverity.Error, outputDiagnostic.Severity);
+
+ Assert.Empty(compilation.SyntaxTrees);
+ }
+
+ [Fact]
+ public void When_without_config_then_generated_with_default_values()
+ {
+ var firstName = "Alex";
+ var defaultNamespace = "MyNamespace";
+
+ string source = $@"
+namespace Example
+{{
+ class Test
+ {{
+ public static string RunTest()
+ {{
+ var json = new {defaultNamespace}.Person()
+ {{
+ FirstName = ""{firstName}""
+ }};
+ return json.FirstName;
+ }}
+ }}
+}}";
+ var (compilation, outputDiagnostics) = GetGeneratedOutput(source, [new AdditionalTextStub("References/schema.json")]);
+
+ Assert.Empty(outputDiagnostics);
+
+ Assert.Equal(2, compilation.SyntaxTrees.Count());
+
+ Assert.Equal(firstName, RunTest(compilation));
+ }
+
+ [Theory]
+ [InlineData(null, false)]
+ [InlineData("false", false)]
+ [InlineData("False", false)]
+ [InlineData("true", true)]
+ [InlineData("True", true)]
+ public void When_GenerateOptionalPropertiesAsNullable_in_global_options_then_generate_according_to_config(
+ string generateOptionalPropertiesAsNullable,
+ bool shouldBeNullable)
+ {
+ string source = $@"
+namespace Example
+{{
+ class Test
+ {{
+ public static string RunTest()
+ {{
+ var json = new MyNamespace.Person();
+ return System.Convert.ToString(json.Age);
+ }}
+ }}
+}}";
+ var globalOptions = new Dictionary
+ {
+ { GenerateOptionalPropertiesAsNullable, generateOptionalPropertiesAsNullable }
+ };
+ var (compilation, outputDiagnostics) = GetGeneratedOutput(
+ source,
+ [new AdditionalTextStub("References/schema.json")],
+ globalOptions);
+
+ Assert.Empty(outputDiagnostics);
+
+ Assert.Equal(2, compilation.SyntaxTrees.Count());
+
+ var expectedOutput = shouldBeNullable ? string.Empty : "0";
+ Assert.Equal(expectedOutput, RunTest(compilation));
+ }
+
+ [Theory]
+ [InlineData(null, "true", true)]
+ [InlineData("false", "true", false)]
+ [InlineData("False", "true", false)]
+ [InlineData("true", "false", true)]
+ [InlineData("True", "false", true)]
+ public void When_GenerateOptionalPropertiesAsNullable_in_additional_files_then_generate_according_to_config_and_override_global_if_possible(
+ string generateOptionalPropertiesAsNullableAdditionalFiles,
+ string generateOptionalPropertiesAsNullableGlobalOptions,
+ bool shouldBeNullable)
+ {
+ string source = $@"
+namespace Example
+{{
+ class Test
+ {{
+ public static string RunTest()
+ {{
+ var json = new MyNamespace.Person();
+ return System.Convert.ToString(json.Age);
+ }}
+ }}
+}}";
+ var globalOptions = new Dictionary
+ {
+ { GenerateOptionalPropertiesAsNullable, generateOptionalPropertiesAsNullableGlobalOptions }
+ };
+ var additionalFilesOptions = new Dictionary
+ {
+ { GenerateOptionalPropertiesAsNullable, generateOptionalPropertiesAsNullableAdditionalFiles }
+ };
+ var (compilation, outputDiagnostics) = GetGeneratedOutput(
+ source,
+ [new AdditionalTextStub("References/schema.json", additionalFilesOptions)],
+ globalOptions);
+
+ Assert.Empty(outputDiagnostics);
+
+ Assert.Equal(2, compilation.SyntaxTrees.Count());
+
+ var expectedOutput = shouldBeNullable ? string.Empty : "0";
+ Assert.Equal(expectedOutput, RunTest(compilation));
+ }
+
+ [Theory]
+ [InlineData(null, null, "MyNamespace")]
+ [InlineData("", null, "MyNamespace")]
+ [InlineData(null, "", "MyNamespace")]
+ [InlineData(null, "NamespaceFromGlobalOptions", "NamespaceFromGlobalOptions")]
+ [InlineData("NamespaceFromLocalOptions", null, "NamespaceFromLocalOptions")]
+ [InlineData("NamespaceFromLocalOptions", "NamespaceFromGlobalOptions", "NamespaceFromLocalOptions")]
+ public void When_Namespace_in_config_then_generate(
+ string namespaceAdditionalFiles,
+ string namespaceGlobalOptions,
+ string expectedNamespace)
+ {
+ string source = $@"
+namespace Example
+{{
+ class Test
+ {{
+ public static string RunTest()
+ {{
+ var json = new {expectedNamespace}.Person();
+ return ""compiled"";
+ }}
+ }}
+}}";
+ var globalOptions = new Dictionary
+ {
+ { Namespace, namespaceGlobalOptions }
+ };
+ var additionalFilesOptions = new Dictionary
+ {
+ { Namespace, namespaceAdditionalFiles }
+ };
+ var (compilation, outputDiagnostics) = GetGeneratedOutput(
+ source,
+ [new AdditionalTextStub("References/schema.json", additionalFilesOptions)],
+ globalOptions);
+
+ Assert.Empty(outputDiagnostics);
+
+ Assert.Equal(2, compilation.SyntaxTrees.Count());
+
+ Assert.Equal("compiled", RunTest(compilation));
+ }
+
+ [Theory]
+ [InlineData(null, null, "Person")]
+ [InlineData(null, "", "Person")]
+ [InlineData("", null, "Person")]
+ [InlineData(null, "ShouldNotOverride", "Person")]
+ [InlineData("ShouldOverride", null, "ShouldOverride")]
+ public void When_TypeNameHint_in_config_then_generate_using_additional_files_only(
+ string typeNameHintAdditionalFiles,
+ string typeNameHintGlobalOptions,
+ string expectedTypeName)
+ {
+ string source = $@"
+namespace Example
+{{
+ class Test
+ {{
+ public static string RunTest()
+ {{
+ var json = new MyNamespace.{expectedTypeName}();
+ return ""compiled"";
+ }}
+ }}
+}}";
+ var globalOptions = new Dictionary
+ {
+ { TypeNameHint, typeNameHintGlobalOptions }
+ };
+ var additionalFilesOptions = new Dictionary
+ {
+ { TypeNameHint, typeNameHintAdditionalFiles }
+ };
+ var (compilation, outputDiagnostics) = GetGeneratedOutput(
+ source,
+ [new AdditionalTextStub("References/schema.json", additionalFilesOptions)],
+ globalOptions);
+
+ Assert.Empty(outputDiagnostics);
+
+ Assert.Equal(2, compilation.SyntaxTrees.Count());
+
+ Assert.Equal("compiled", RunTest(compilation));
+ }
+
+ [Theory]
+ [InlineData(null, null, "NJsonSchemaGenerated.g.cs")]
+ [InlineData("", null, "NJsonSchemaGenerated.g.cs")]
+ [InlineData(null, "", "NJsonSchemaGenerated.g.cs")]
+ [InlineData(null, "ShouldNotOverride.g.cs", "NJsonSchemaGenerated.g.cs")]
+ [InlineData("ShouldOverride.g.cs", null, "ShouldOverride.g.cs")]
+ public void When_FileName_in_config_then_generate_using_additional_files_only(
+ string fileNameAdditionalFiles,
+ string fileNameGlobalOptions,
+ string expectedFileName)
+ {
+ var globalOptions = new Dictionary
+ {
+ { FileName, fileNameGlobalOptions }
+ };
+ var additionalFilesOptions = new Dictionary
+ {
+ { FileName, fileNameAdditionalFiles }
+ };
+ var (compilation, outputDiagnostics) = GetGeneratedOutput(
+ null,
+ [new AdditionalTextStub("References/schema.json", additionalFilesOptions)],
+ globalOptions);
+
+ Assert.Empty(outputDiagnostics);
+
+ Assert.Single(compilation.SyntaxTrees);
+ var syntaxTree = compilation.SyntaxTrees.First();
+ Assert.EndsWith(expectedFileName, syntaxTree.FilePath);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/NJsonSchema.SourceGenerators.CSharp.Tests/NJsonSchema.SourceGenerators.CSharp.Tests.csproj b/src/NJsonSchema.SourceGenerators.CSharp.Tests/NJsonSchema.SourceGenerators.CSharp.Tests.csproj
new file mode 100644
index 000000000..72a3dbf91
--- /dev/null
+++ b/src/NJsonSchema.SourceGenerators.CSharp.Tests/NJsonSchema.SourceGenerators.CSharp.Tests.csproj
@@ -0,0 +1,34 @@
+
+
+
+ net8.0
+ false
+ false
+ disable
+ true
+ false
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/NJsonSchema.SourceGenerators.CSharp.Tests/TestsBase.cs b/src/NJsonSchema.SourceGenerators.CSharp.Tests/TestsBase.cs
new file mode 100644
index 000000000..540332002
--- /dev/null
+++ b/src/NJsonSchema.SourceGenerators.CSharp.Tests/TestsBase.cs
@@ -0,0 +1,240 @@
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Emit;
+using Microsoft.CodeAnalysis.Text;
+using Microsoft.CodeAnalysis;
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Threading;
+using Xunit.Abstractions;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace NJsonSchema.SourceGenerators.CSharp.Tests
+{
+ ///
+ /// Helps with AdditionalFiles by exposing them as text files.
+ ///
+ public class AdditionalTextStub : AdditionalText
+ {
+ private readonly string _path;
+
+ public AdditionalTextStub(string path, Dictionary options = null)
+ {
+ _path = path;
+ Options = options;
+ }
+
+ public Dictionary Options { get; }
+
+ public override string Path
+ {
+ get
+ {
+ return _path;
+ }
+ }
+
+ public override SourceText GetText(CancellationToken cancellationToken = default)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public abstract class TestsBase
+ {
+ protected readonly ITestOutputHelper _output;
+ private static List _metadataReferences;
+ private static readonly object Lock = new object();
+
+ protected TestsBase(ITestOutputHelper output)
+ {
+ _output = output;
+ }
+
+ ///
+ /// Retrieves and caches referenced assemblies, so that tested compilations can make use of them.
+ ///
+ private static List MetadataReferences
+ {
+ get
+ {
+ lock (Lock)
+ {
+ if (_metadataReferences == null)
+ {
+ _metadataReferences = new List();
+ Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
+ foreach (var assembly in assemblies)
+ {
+ if (!assembly.IsDynamic)
+ {
+ _metadataReferences.Add(MetadataReference.CreateFromFile(assembly.Location));
+ }
+ }
+ }
+ }
+
+ return _metadataReferences;
+ }
+ }
+
+ ///
+ /// Takes compiled input and runs the code.
+ ///
+ /// Compiled code
+ /// If specified, this list will be populated with diagnostics that can be used for debugging
+ ///
+ protected string RunTest(Compilation compilation, List diagnostics = null)
+ {
+ if (compilation == null)
+ {
+ throw new ArgumentException($"Argument {nameof(compilation)} must not be null");
+ }
+
+ // Get the compilation and load the assembly
+ using var memoryStream = new MemoryStream();
+ EmitResult result = compilation.Emit(memoryStream);
+
+ if (result.Success)
+ {
+ memoryStream.Seek(0, SeekOrigin.Begin);
+ Assembly assembly = Assembly.Load(memoryStream.ToArray());
+
+ // We assume the generated code has a type Example.Test that contains a method RunTest(Async), to run the test
+ Type testClassType = assembly.GetType("Example.Test");
+ var method = testClassType?.GetMethod("RunTest") ?? testClassType?.GetMethod("RunTestAsync");
+ if (method == null)
+ {
+ return "-- could not find test method --";
+ }
+
+ // Actually invoke the method and return the result
+ var resultObj = method.Invoke(null, Array.Empty