diff --git a/Directory.Packages.props b/Directory.Packages.props
index b0584ebac..3e3917cb5 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -14,8 +14,11 @@
+
+
+
diff --git a/src/NJsonSchema.Demo/NJsonSchema.Demo.csproj b/src/NJsonSchema.Demo/NJsonSchema.Demo.csproj
index 5237f338f..b341986f0 100644
--- a/src/NJsonSchema.Demo/NJsonSchema.Demo.csproj
+++ b/src/NJsonSchema.Demo/NJsonSchema.Demo.csproj
@@ -1,4 +1,5 @@
+
net8.0
@@ -10,14 +11,26 @@
disable
$(NoWarn);CS1591;IDE0005
-
+
+
+
+
+
+
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..1a531d272
--- /dev/null
+++ b/src/NJsonSchema.SourceGenerators.CSharp.Tests/JsonSchemaSourceGeneratorTests.cs
@@ -0,0 +1,263 @@
+using Microsoft.CodeAnalysis;
+using Xunit.Abstractions;
+using static NJsonSchema.SourceGenerators.CSharp.GeneratorConfigurationKeys;
+
+namespace NJsonSchema.SourceGenerators.CSharp.Tests
+{
+ public class JsonSchemaSourceGeneratorTests(ITestOutputHelper output) : TestsBase(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..df03167f6
--- /dev/null
+++ b/src/NJsonSchema.SourceGenerators.CSharp.Tests/NJsonSchema.SourceGenerators.CSharp.Tests.csproj
@@ -0,0 +1,35 @@
+
+
+
+ net8.0
+ false
+ false
+ disable
+ true
+ false
+ $(NoWarn);1591
+
+
+
+
+ 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..68e1822b6
--- /dev/null
+++ b/src/NJsonSchema.SourceGenerators.CSharp.Tests/TestsBase.cs
@@ -0,0 +1,195 @@
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Emit;
+using Microsoft.CodeAnalysis.Text;
+using Microsoft.CodeAnalysis;
+using System.Collections.Immutable;
+using System.Reflection;
+using Xunit.Abstractions;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace NJsonSchema.SourceGenerators.CSharp.Tests
+{
+ ///
+ /// Helps with AdditionalFiles by exposing them as text files.
+ ///
+ public class AdditionalTextStub(string path, Dictionary options = null) : AdditionalText
+ {
+ public Dictionary Options { get; } = options;
+
+ public override string Path => path;
+
+ public override SourceText GetText(CancellationToken cancellationToken = default)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public abstract class TestsBase(ITestOutputHelper output)
+ {
+ private static List _metadataReferences;
+ private static readonly object Lock = new object();
+
+ ///
+ /// 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 = [];
+ 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, []);
+ if (resultObj is not string stringResult)
+ {
+ return "-- result was not a string --";
+ }
+
+ // Log the test output, for debugging purposes
+ output.WriteLine($"Generated test output:\r\n===\r\n{stringResult}\r\n===\r\n");
+
+ return stringResult;
+ }
+
+ // If diagnostics list is specified, fill it with any diagnostics. If not, fail the unit test directly.
+ if (diagnostics == null)
+ {
+ Assert.Fail(
+ $"Compilation did not succeed:\r\n{string.Join("\r\n", result.Diagnostics.Select(d => $"{Enum.GetName(typeof(DiagnosticSeverity), d.Severity)} ({d.Location}) - {d.GetMessage()}"))}");
+ }
+ else
+ {
+ diagnostics.AddRange(result.Diagnostics);
+ }
+
+ return null;
+ }
+
+ ///
+ /// Build a compilation and run the source generator.
+ ///
+ /// Input source
+ /// Additional files that must be added to the compilation
+ /// Global options
+ ///
+ protected (Compilation, IImmutableList) GetGeneratedOutput(
+ string source,
+ AdditionalTextStub[] additionalTexts,
+ Dictionary globalOptions = null)
+ {
+ List syntaxTrees = null;
+ if (source != null)
+ {
+ syntaxTrees = [CSharpSyntaxTree.ParseText(source)];
+ }
+
+ var references = MetadataReferences;
+
+ var compilation = CSharpCompilation.Create(
+ "TestImplementation",
+ syntaxTrees,
+ references,
+ new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+ var generator = new JsonSchemaSourceGenerator();
+
+ var analyzerConfigOptionsProvider = new TestAnalyzerConfigOptionsProvider(additionalTexts, globalOptions);
+
+ CSharpGeneratorDriver.Create(
+ [generator.AsSourceGenerator()],
+ additionalTexts,
+ new CSharpParseOptions(),
+ analyzerConfigOptionsProvider)
+ .RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generateDiagnostics);
+
+ var output1 = outputCompilation
+ .SyntaxTrees
+ .Where(t => t.GetText().ToString().Contains(""))
+ .ToArray();
+
+ if (output1.Length > 0)
+ {
+ output.WriteLine($"Generated code:\r\n===\r\n{string.Join("\r\n===\r\n", output1.AsEnumerable())}\r\n===\r\n");
+ }
+
+ return (outputCompilation, generateDiagnostics);
+ }
+ }
+
+ public class TestAnalyzerConfigOptionsProvider(
+ IEnumerable additionalTexts,
+ Dictionary globalOptions = null)
+ : AnalyzerConfigOptionsProvider
+ {
+ public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => new AdditionalFilesTestAnalyzerConfigOptions();
+
+ public override AnalyzerConfigOptions GetOptions(AdditionalText textFile)
+ {
+ return new AdditionalFilesTestAnalyzerConfigOptions(additionalTexts.FirstOrDefault(x => x.Path == textFile.Path)?.Options);
+ }
+
+ public override AnalyzerConfigOptions GlobalOptions { get; } = new GlobalTestAnalyzerConfigOptions(globalOptions);
+
+ private abstract class TestAnalyzerConfigOptions(Dictionary options = null)
+ : AnalyzerConfigOptions
+ {
+ private readonly Dictionary _options = options ?? [];
+
+ public override bool TryGetValue(string key, out string value) => _options.TryGetValue(key, out value);
+ }
+
+ private class AdditionalFilesTestAnalyzerConfigOptions(Dictionary options = null)
+ : TestAnalyzerConfigOptions(options?.Select(x => ("build_metadata.AdditionalFiles." + x.Key, x.Value))
+ .ToDictionary(pair => pair.Item1, p => p.Value));
+
+ private class GlobalTestAnalyzerConfigOptions(Dictionary options) : TestAnalyzerConfigOptions(
+ options?.Select(x => ("build_property." + x.Key, x.Value)).ToDictionary(pair => pair.Item1, p => p.Value));
+ }
+}
diff --git a/src/NJsonSchema.SourceGenerators.CSharp/AnalyzerConfigOptionsProviderExtensions.cs b/src/NJsonSchema.SourceGenerators.CSharp/AnalyzerConfigOptionsProviderExtensions.cs
new file mode 100644
index 000000000..7906d7ffc
--- /dev/null
+++ b/src/NJsonSchema.SourceGenerators.CSharp/AnalyzerConfigOptionsProviderExtensions.cs
@@ -0,0 +1,66 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using static NJsonSchema.SourceGenerators.CSharp.GeneratorConfigurationKeys;
+
+namespace NJsonSchema.SourceGenerators.CSharp
+{
+ ///
+ /// Extension methods for .
+ ///
+ public static class AnalyzerConfigOptionsProviderExtensions
+ {
+ ///
+ /// Converts options from to .
+ ///
+ /// Analyzer options provider.
+ /// Additional text item.
+ ///
+ public static JsonSchemaSourceGeneratorConfig ToJsonSchemaSourceGeneratorConfig(
+ this AnalyzerConfigOptionsProvider analyzerConfigOptionsProvider,
+ AdditionalText additionalText)
+ {
+ var additionTextOptions = analyzerConfigOptionsProvider.GetOptions(additionalText);
+ var globalOptions = analyzerConfigOptionsProvider.GlobalOptions;
+
+ return new JsonSchemaSourceGeneratorConfig(
+ GetOptionBoolean(GenerateOptionalPropertiesAsNullable, globalOptions, additionTextOptions),
+ GetOption(Namespace, globalOptions, additionTextOptions),
+ GetAdditionalFileOption(TypeNameHint, additionTextOptions),
+ GetAdditionalFileOption(FileName, additionTextOptions));
+ }
+
+ private static bool? GetOptionBoolean(
+ string key,
+ AnalyzerConfigOptions globalOptions,
+ AnalyzerConfigOptions additionTextOptions)
+ {
+ var option = GetOption(key, globalOptions, additionTextOptions);
+
+ return bool.TryParse(option, out var result) ? result : null;
+ }
+
+ private static string? GetOption(
+ string key,
+ AnalyzerConfigOptions globalOptions,
+ AnalyzerConfigOptions additionTextOptions)
+ {
+ return GetAdditionalFileOption(key, additionTextOptions) ?? GetGlobalOption(key, globalOptions);
+ }
+
+ private static string? GetGlobalOption(
+ string key,
+ AnalyzerConfigOptions globalOptions)
+ {
+ globalOptions.TryGetValue($"build_property.{key}", out var value);
+ return value;
+ }
+
+ private static string? GetAdditionalFileOption(
+ string key,
+ AnalyzerConfigOptions additionTextOptions)
+ {
+ additionTextOptions.TryGetValue($"build_metadata.AdditionalFiles.{key}", out var value);
+ return value;
+ }
+ }
+}
diff --git a/src/NJsonSchema.SourceGenerators.CSharp/GeneratorConfigurationKeys.cs b/src/NJsonSchema.SourceGenerators.CSharp/GeneratorConfigurationKeys.cs
new file mode 100644
index 000000000..75fdb32dc
--- /dev/null
+++ b/src/NJsonSchema.SourceGenerators.CSharp/GeneratorConfigurationKeys.cs
@@ -0,0 +1,30 @@
+namespace NJsonSchema.SourceGenerators.CSharp
+{
+ ///
+ /// Contains contstants with configuration keys used for Source Generation.
+ ///
+ public static class GeneratorConfigurationKeys
+ {
+ private const string _prefix = "NJsonSchema_";
+
+ ///
+ /// Indicating whether optional schema properties (not required) are generated as nullable properties.
+ ///
+ public const string GenerateOptionalPropertiesAsNullable = _prefix + "GenerateOptionalPropertiesAsNullable";
+
+ ///
+ /// .NET namespace of the generated types.
+ ///
+ public const string Namespace = _prefix + "Namespace";
+
+ ///
+ /// C# class name.
+ ///
+ public const string TypeNameHint = _prefix + "TypeNameHint";
+
+ ///
+ /// Name of the file containing generated classes.
+ ///
+ public const string FileName = _prefix + "FileName";
+ }
+}
diff --git a/src/NJsonSchema.SourceGenerators.CSharp/JsonSchemaSourceGenerator.cs b/src/NJsonSchema.SourceGenerators.CSharp/JsonSchemaSourceGenerator.cs
new file mode 100644
index 000000000..5d0802c3b
--- /dev/null
+++ b/src/NJsonSchema.SourceGenerators.CSharp/JsonSchemaSourceGenerator.cs
@@ -0,0 +1,63 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Text;
+using NJsonSchema.CodeGeneration.CSharp;
+using System.Text;
+
+namespace NJsonSchema.SourceGenerators.CSharp
+{
+ ///
+ /// Generates C# classes from JSON schema
+ ///
+ [Generator]
+ public class JsonSchemaSourceGenerator : IIncrementalGenerator
+ {
+ ///
+ /// Configure source generation.
+ ///
+ /// Source generator context.
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var emitLoggingPipeline = context.AdditionalTextsProvider
+ .Combine(context.AnalyzerConfigOptionsProvider)
+ .Select((pair, _) => (pair.Left.Path, Config: pair.Right.ToJsonSchemaSourceGeneratorConfig(pair.Left)));
+
+ context.RegisterSourceOutput(emitLoggingPipeline,
+ (ctx, pair) =>
+ {
+
+ (string path, JsonSchemaSourceGeneratorConfig config) = pair;
+ var settings = new CSharpGeneratorSettings();
+
+ if (config.GenerateOptionalPropertiesAsNullable.HasValue)
+ {
+ settings.GenerateOptionalPropertiesAsNullable = config.GenerateOptionalPropertiesAsNullable.Value;
+ }
+
+ if (!string.IsNullOrEmpty(config.Namespace))
+ {
+ settings.Namespace = config.Namespace!;
+ }
+
+ try
+ {
+ var schema = JsonSchema.FromFileAsync(path).GetAwaiter().GetResult();
+ var generator = new CSharpGenerator(schema, settings);
+ var classesFileContent = string.IsNullOrEmpty(config.TypeNameHint)
+ ? generator.GenerateFile()
+ : generator.GenerateFile(config.TypeNameHint!);
+
+ var fileName = string.IsNullOrEmpty(config.FileName) ? "NJsonSchemaGenerated.g.cs" : config.FileName;
+ ctx.AddSource(fileName!, SourceText.From(classesFileContent, Encoding.UTF8));
+ }
+ catch (Exception ex)
+ {
+ ctx.ReportDiagnostic(
+ Diagnostic.Create(
+ new DiagnosticDescriptor(
+ "NJSG001", "Source Generator error", ex.ToString(), "SourceGenerator", DiagnosticSeverity.Error, true),
+ Location.None));
+ }
+ });
+ }
+ }
+}
diff --git a/src/NJsonSchema.SourceGenerators.CSharp/JsonSchemaSourceGeneratorConfig.cs b/src/NJsonSchema.SourceGenerators.CSharp/JsonSchemaSourceGeneratorConfig.cs
new file mode 100644
index 000000000..91dabcf96
--- /dev/null
+++ b/src/NJsonSchema.SourceGenerators.CSharp/JsonSchemaSourceGeneratorConfig.cs
@@ -0,0 +1,17 @@
+namespace NJsonSchema.SourceGenerators.CSharp
+{
+ ///
+ /// Configuration for source generator.
+ ///
+ /// Value indicating whether optional schema properties (not required) are generated as nullable properties (default: false).
+ /// .NET namespace of the generated types (default: MyNamespace).
+ /// C# class name.
+ /// Name of the file containing generated classes.
+ public record JsonSchemaSourceGeneratorConfig(
+ bool? GenerateOptionalPropertiesAsNullable,
+ string? Namespace,
+ string? TypeNameHint,
+ string? FileName)
+ {
+ }
+}
diff --git a/src/NJsonSchema.SourceGenerators.CSharp/NJsonSchema.SourceGenerators.CSharp.csproj b/src/NJsonSchema.SourceGenerators.CSharp/NJsonSchema.SourceGenerators.CSharp.csproj
new file mode 100644
index 000000000..df3739c2b
--- /dev/null
+++ b/src/NJsonSchema.SourceGenerators.CSharp/NJsonSchema.SourceGenerators.CSharp.csproj
@@ -0,0 +1,74 @@
+
+
+
+
+ netstandard2.0
+ bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml
+ latest
+ true
+ false
+ true
+ $(NoWarn);1701;1702;NU5128
+
+
+
+
+ TargetFramework=netstandard2.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $(GetTargetPathDependsOn);GetDependencyTargetPaths
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/NJsonSchema.SourceGenerators.CSharp/NJsonSchema.SourceGenerators.CSharp.nuspec b/src/NJsonSchema.SourceGenerators.CSharp/NJsonSchema.SourceGenerators.CSharp.nuspec
new file mode 100644
index 000000000..8a3b520a4
--- /dev/null
+++ b/src/NJsonSchema.SourceGenerators.CSharp/NJsonSchema.SourceGenerators.CSharp.nuspec
@@ -0,0 +1,12 @@
+
+
+
+ $id$
+ $version$
+ $author$
+ $description$
+ json schema validation source generator codegen .net
+ http://NJsonSchema.org
+ https://github.com/RicoSuter/NJsonSchema/blob/master/LICENSE.md
+
+
\ No newline at end of file
diff --git a/src/NJsonSchema.SourceGenerators.CSharp/NJsonSchema.SourceGenerators.CSharp.props b/src/NJsonSchema.SourceGenerators.CSharp/NJsonSchema.SourceGenerators.CSharp.props
new file mode 100644
index 000000000..1882bdd09
--- /dev/null
+++ b/src/NJsonSchema.SourceGenerators.CSharp/NJsonSchema.SourceGenerators.CSharp.props
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/NJsonSchema.SourceGenerators.CSharp/NuGetIcon.png b/src/NJsonSchema.SourceGenerators.CSharp/NuGetIcon.png
new file mode 100644
index 000000000..bd2ae5416
Binary files /dev/null and b/src/NJsonSchema.SourceGenerators.CSharp/NuGetIcon.png differ
diff --git a/src/NJsonSchema.SourceGenerators.CSharp/Properties/launchSettings.json b/src/NJsonSchema.SourceGenerators.CSharp/Properties/launchSettings.json
new file mode 100644
index 000000000..4f0376f63
--- /dev/null
+++ b/src/NJsonSchema.SourceGenerators.CSharp/Properties/launchSettings.json
@@ -0,0 +1,8 @@
+{
+ "profiles": {
+ "DebugGenerator": {
+ "commandName": "DebugRoslynComponent",
+ "targetProject": "..\\NJsonSchema.Demo\\NJsonSchema.Demo.csproj"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/NJsonSchema.sln b/src/NJsonSchema.sln
index 2763070c3..050df6724 100644
--- a/src/NJsonSchema.sln
+++ b/src/NJsonSchema.sln
@@ -47,6 +47,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "02 CodeGeneration", "02 Cod
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NJsonSchema.Annotations", "NJsonSchema.Annotations\NJsonSchema.Annotations.csproj", "{526020E1-D3B5-49F6-BE11-5F4402A02E92}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "03 SourceGeneration", "03 SourceGeneration", "{6E8895D5-BDA2-4F8B-919E-01EA89D0C012}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NJsonSchema.SourceGenerators.CSharp", "NJsonSchema.SourceGenerators.CSharp\NJsonSchema.SourceGenerators.CSharp.csproj", "{AE9517AE-C469-4ED5-BC46-9EBEEE4332FC}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NJsonSchema.SourceGenerators.CSharp.Tests", "NJsonSchema.SourceGenerators.CSharp.Tests\NJsonSchema.SourceGenerators.CSharp.Tests.csproj", "{9CF2F754-27FE-4A05-974A-A9EED7D8ECC4}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -119,6 +125,14 @@ Global
{526020E1-D3B5-49F6-BE11-5F4402A02E92}.Debug|Any CPU.Build.0 = Debug|Any CPU
{526020E1-D3B5-49F6-BE11-5F4402A02E92}.Release|Any CPU.ActiveCfg = Release|Any CPU
{526020E1-D3B5-49F6-BE11-5F4402A02E92}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AE9517AE-C469-4ED5-BC46-9EBEEE4332FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AE9517AE-C469-4ED5-BC46-9EBEEE4332FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AE9517AE-C469-4ED5-BC46-9EBEEE4332FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AE9517AE-C469-4ED5-BC46-9EBEEE4332FC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9CF2F754-27FE-4A05-974A-A9EED7D8ECC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9CF2F754-27FE-4A05-974A-A9EED7D8ECC4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9CF2F754-27FE-4A05-974A-A9EED7D8ECC4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9CF2F754-27FE-4A05-974A-A9EED7D8ECC4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -139,6 +153,8 @@ Global
{A9C2A9CD-44F6-4A21-9D72-00CF5BE0A36F} = {CDBC7AC4-9D3D-430D-86BF-CDAECBDD715A}
{AFA1E1E7-37F9-4958-B0E3-ADB12F53E563} = {CDBC7AC4-9D3D-430D-86BF-CDAECBDD715A}
{526020E1-D3B5-49F6-BE11-5F4402A02E92} = {CDBC7AC4-9D3D-430D-86BF-CDAECBDD715A}
+ {AE9517AE-C469-4ED5-BC46-9EBEEE4332FC} = {6E8895D5-BDA2-4F8B-919E-01EA89D0C012}
+ {9CF2F754-27FE-4A05-974A-A9EED7D8ECC4} = {6E8895D5-BDA2-4F8B-919E-01EA89D0C012}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {9D5EDC80-5611-493B-804B-8B364816952B}
diff --git a/src/NJsonSchema/CompilerFeatures.cs b/src/NJsonSchema/CompilerFeatures.cs
index 581104987..a7ead1499 100644
--- a/src/NJsonSchema/CompilerFeatures.cs
+++ b/src/NJsonSchema/CompilerFeatures.cs
@@ -21,7 +21,7 @@ namespace System.Runtime.CompilerServices
/// This class should not be used by developers in source code.
///
[EditorBrowsable(EditorBrowsableState.Never)]
- public static class IsExternalInit
+ internal static class IsExternalInit
{ }
}