-
-
Notifications
You must be signed in to change notification settings - Fork 64
Use NSwag's built-in System.Text.Json polymorphic serialization #772
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,6 +29,7 @@ public CustomCSharpClientGenerator Create() | |
| { | ||
| Namespace = settings.ContractsNamespace ?? settings.Namespace, | ||
| JsonLibrary = CSharpJsonLibrary.SystemTextJson, | ||
| JsonPolymorphicSerializationStyle = settings.UsePolymorphicSerialization ? CSharpJsonPolymorphicSerializationStyle.SystemTextJson : CSharpJsonPolymorphicSerializationStyle.NJsonSchema, | ||
| TypeAccessModifier = settings.TypeAccessibility.ToString().ToLowerInvariant(), | ||
| ClassStyle = | ||
| settings.ImmutableRecords || | ||
|
|
@@ -46,18 +47,11 @@ public CustomCSharpClientGenerator Create() | |
| csharpClientGeneratorSettings.ParameterNameGenerator = settings.ParameterNameGenerator; | ||
| } | ||
|
|
||
| csharpClientGeneratorSettings.CSharpGeneratorSettings.TemplateFactory = new CustomTemplateFactory( | ||
| csharpClientGeneratorSettings.CSharpGeneratorSettings, | ||
| [ | ||
| typeof(CSharpGenerator).Assembly, | ||
| typeof(CSharpGeneratorBaseSettings).Assembly, | ||
| typeof(CustomTemplateFactory).Assembly, | ||
| ]); | ||
| csharpClientGeneratorSettings.CSharpGeneratorSettings.TemplateFactory = new CustomTemplateFactory(csharpClientGeneratorSettings.CSharpGeneratorSettings); | ||
|
|
||
| var generator = new CustomCSharpClientGenerator( | ||
| document, | ||
| csharpClientGeneratorSettings, | ||
| settings.UsePolymorphicSerialization); | ||
| csharpClientGeneratorSettings); | ||
|
|
||
| MapCSharpGeneratorSettings( | ||
| settings.CodeGeneratorSettings, | ||
|
|
@@ -102,39 +96,30 @@ private static void MapCSharpGeneratorSettings( | |
|
|
||
| /// <summary> | ||
| /// custom template factory | ||
| /// solely for the purpose of supporting UsePolymorphicSerialization | ||
| /// This class and its templates should be removed when NSwag supports this feature. | ||
| /// solely for the purpose of tweaking the JsonPolymorphic attribute with UnknownDerivedTypeHandling = FallBackToBaseType and IgnoreUnrecognizedTypeDiscriminators = true | ||
| /// This class should be removed if NSwag eventually supports setting UnknownDerivedTypeHandling and IgnoreUnrecognizedTypeDiscriminators. | ||
| /// </summary> | ||
| private class CustomTemplateFactory : NSwag.CodeGeneration.DefaultTemplateFactory | ||
| { | ||
| /// <summary>Initializes a new instance of the <see cref="DefaultTemplateFactory" /> class.</summary> | ||
| /// <summary>Initializes a new instance of the <see cref="CustomTemplateFactory" /> class.</summary> | ||
| /// <param name="settings">The settings.</param> | ||
| /// <param name="assemblies">The assemblies.</param> | ||
| public CustomTemplateFactory(CodeGeneratorSettingsBase settings, Assembly[] assemblies) | ||
| : base(settings, assemblies) | ||
| public CustomTemplateFactory(CodeGeneratorSettingsBase settings) | ||
| : base(settings, [typeof(CSharpGenerator).Assembly, typeof(CSharpGeneratorBaseSettings).Assembly]) | ||
| { | ||
| } | ||
|
|
||
| /// <summary>Tries to load an embedded Liquid template.</summary> | ||
| /// <param name="language">The language.</param> | ||
| /// <param name="template">The template name.</param> | ||
| /// <returns>The template.</returns> | ||
| /// <inheritdoc /> | ||
| protected override string GetEmbeddedLiquidTemplate(string language, string template) | ||
| { | ||
| template = template.TrimEnd('!'); | ||
| var assembly = Assembly.GetExecutingAssembly(); // this code is running in Refitter.Core and Refitter.SourceGenerator | ||
| var resourceName = $"{assembly.GetName().Name}.Templates.{template}.liquid"; | ||
|
|
||
| var resource = assembly.GetManifestResourceStream(resourceName); | ||
| if (resource != null) | ||
| var templateText = base.GetEmbeddedLiquidTemplate(language, template); | ||
| return template switch | ||
| { | ||
| using (var reader = new StreamReader(resource)) | ||
| { | ||
| return reader.ReadToEnd(); | ||
| } | ||
| } | ||
|
|
||
| return base.GetEmbeddedLiquidTemplate(language, template); | ||
| "Class" => templateText | ||
| .Replace( | ||
| "[System.Text.Json.Serialization.JsonPolymorphic(TypeDiscriminatorPropertyName = \"{{ Discriminator }}\")]", | ||
| "[System.Text.Json.Serialization.JsonPolymorphic(TypeDiscriminatorPropertyName = \"{{ Discriminator }}\", UnknownDerivedTypeHandling = System.Text.Json.Serialization.JsonUnknownDerivedTypeHandling.FallBackToBaseType, IgnoreUnrecognizedTypeDiscriminators = true)]"), | ||
| _ => templateText, | ||
| }; | ||
| } | ||
|
Comment on lines
+111
to
123
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major 🧩 Analysis chainTemplate patch is brittle to upstream formatting changes; make it resilient and idempotent. Exact
- protected override string GetEmbeddedLiquidTemplate(string language, string template)
- {
- var templateText = base.GetEmbeddedLiquidTemplate(language, template);
- return template switch
- {
- "Class" => templateText
- .Replace(
- "[System.Text.Json.Serialization.JsonPolymorphic(TypeDiscriminatorPropertyName = \"{{ Discriminator }}\")]",
- "[System.Text.Json.Serialization.JsonPolymorphic(TypeDiscriminatorPropertyName = \"{{ Discriminator }}\", UnknownDerivedTypeHandling = System.Text.Json.Serialization.JsonUnknownDerivedTypeHandling.FallBackToBaseType, IgnoreUnrecognizedTypeDiscriminators = true)]")
- _ => templateText,
- };
- }
+ protected override string GetEmbeddedLiquidTemplate(string language, string template)
+ {
+ var text = base.GetEmbeddedLiquidTemplate(language, template);
+ if (!string.Equals(template, "Class", StringComparison.Ordinal))
+ return text;
+
+ // Patch [JsonPolymorphic(...)] robustly
+ const string pattern = @"\[\s*System\.Text\.Json\.Serialization\.JsonPolymorphic\s*\((?<args>[^)]*)\)\s*\]";
+ return System.Text.RegularExpressions.Regex.Replace(text, pattern, m =>
+ {
+ var args = m.Groups["args"].Value;
+ // Idempotent: if either arg already present, do nothing
+ if (args.Contains("UnknownDerivedTypeHandling") || args.Contains("IgnoreUnrecognizedTypeDiscriminators"))
+ return m.Value;
+
+ var insert = string.IsNullOrWhiteSpace(args) ? string.Empty : args.Trim() + ", ";
+ insert += "UnknownDerivedTypeHandling = System.Text.Json.Serialization.JsonUnknownDerivedTypeHandling.FallBackToBaseType, IgnoreUnrecognizedTypeDiscriminators = true";
+ return $"[System.Text.Json.Serialization.JsonPolymorphic({insert})]";
+ });
+ }Add once at the top of the file: using System.Text.RegularExpressions;
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,139 +1,12 @@ | ||
| using NJsonSchema; | ||
| using NJsonSchema.CodeGeneration; | ||
| using NJsonSchema.CodeGeneration.CSharp; | ||
| using NJsonSchema.CodeGeneration.CSharp.Models; | ||
| using NSwag; | ||
| using NSwag.CodeGeneration.CSharp; | ||
| using NSwag.CodeGeneration.CSharp.Models; | ||
|
|
||
| namespace Refitter.Core; | ||
|
|
||
| internal class CustomCSharpClientGenerator(OpenApiDocument document, CSharpClientGeneratorSettings settings, bool usePolymorphicSerialization) | ||
| #pragma warning disable CS9107 // Parameter is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well. | ||
| internal class CustomCSharpClientGenerator(OpenApiDocument document, CSharpClientGeneratorSettings settings) | ||
| : CSharpClientGenerator(document, settings) | ||
| #pragma warning restore CS9107 // Parameter is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well. | ||
| { | ||
| internal CSharpOperationModel CreateOperationModel(OpenApiOperation operation) => | ||
| CreateOperationModel(operation, Settings); | ||
|
|
||
| /// <summary> | ||
| /// override to generate DTO types with our custom CSharpGenerator | ||
| /// This code should be removed when NSwag supports STJ polymorphic serialization | ||
| /// </summary> | ||
| protected override IEnumerable<CodeArtifact> GenerateDtoTypes() | ||
| { | ||
| var generator = new CCustomSharpGenerator(document, Settings.CSharpGeneratorSettings, (CSharpTypeResolver)Resolver, usePolymorphicSerialization); | ||
| return generator.GenerateTypes(); | ||
| } | ||
|
|
||
| private class CCustomSharpGenerator(OpenApiDocument document, CSharpGeneratorSettings settings, CSharpTypeResolver resolver, bool usePolymorphicSerialization) | ||
| #pragma warning disable CS9107 // Parameter is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well. | ||
| : CSharpGenerator(document, settings, resolver) | ||
| #pragma warning restore CS9107 // Parameter is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well. | ||
| { | ||
| /// <summary> | ||
| /// override to generate Class with our custom ClassTemplateModel | ||
| /// code is taken from NJsonSchema.CodeGeneration.CSharp.CSharpGenerator.GenerateType | ||
| /// </summary> | ||
| protected override CodeArtifact GenerateType(JsonSchema schema, string typeNameHint) | ||
| { | ||
| var typeName = resolver.GetOrGenerateTypeName(schema, typeNameHint); | ||
|
|
||
| if (schema.IsEnumeration) | ||
| { | ||
| return base.GenerateType(schema, typeName); | ||
| } | ||
| else | ||
| { | ||
| return GenerateClass(schema, typeName); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// override to generate JsonInheritanceAttribute, JsonInheritanceConverter with our custom template models | ||
| /// code is taken from NJsonSchema.CodeGeneration.CSharp.CSharpGenerator.GenerateTypes | ||
| /// </summary> | ||
| public override IEnumerable<CodeArtifact> GenerateTypes() | ||
| { | ||
| var baseArtifacts = base.GenerateTypes(); | ||
| var artifacts = new List<CodeArtifact>(); | ||
|
|
||
| if (baseArtifacts.Any(r => r.Code.Contains("JsonInheritanceConverter"))) | ||
| { | ||
| if (Settings.ExcludedTypeNames?.Contains("JsonInheritanceAttribute") != true) | ||
| { | ||
| var template = Settings.TemplateFactory.CreateTemplate("CSharp", "JsonInheritanceAttribute", new CustomJsonInheritanceConverterTemplateModel(Settings, usePolymorphicSerialization)); | ||
| artifacts.Add(new CodeArtifact("JsonInheritanceAttribute", CodeArtifactType.Class, CodeArtifactLanguage.CSharp, CodeArtifactCategory.Utility, template)); | ||
| } | ||
|
|
||
| if (Settings.ExcludedTypeNames?.Contains("JsonInheritanceConverter") != true) | ||
| { | ||
| var template = Settings.TemplateFactory.CreateTemplate("CSharp", "JsonInheritanceConverter", new CustomJsonInheritanceConverterTemplateModel(Settings, usePolymorphicSerialization)); | ||
| artifacts.Add(new CodeArtifact("JsonInheritanceConverter", CodeArtifactType.Class, CodeArtifactLanguage.CSharp, CodeArtifactCategory.Utility, template)); | ||
| } | ||
| } | ||
|
|
||
| return baseArtifacts.Concat(artifacts); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Code is taken from NJsonSchema.CodeGeneration.CSharp.CSharpGenerator.GenerateClass | ||
| /// to instantiate our custom ClassTemplateModel | ||
| /// </summary> | ||
| private CodeArtifact GenerateClass(JsonSchema schema, string typeName) | ||
| { | ||
| var model = new CustomClassTemplateModel(typeName, Settings, resolver, schema, RootObject, usePolymorphicSerialization); | ||
|
|
||
| RenamePropertyWithSameNameAsClass(typeName, model.Properties); | ||
|
|
||
| var template = Settings.TemplateFactory.CreateTemplate("CSharp", "Class", model); | ||
| return new CodeArtifact(typeName, model.BaseClassName, CodeArtifactType.Class, CodeArtifactLanguage.CSharp, CodeArtifactCategory.Contract, template); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Code is taken from NJsonSchema.CodeGeneration.CSharp.CSharpGenerator.RenamePropertyWithSameNameAsClass | ||
| /// </summary> | ||
| private static void RenamePropertyWithSameNameAsClass(string typeName, IEnumerable<PropertyModel> properties) | ||
| { | ||
| var propertyModels = properties as PropertyModel[] ?? properties.ToArray(); | ||
| PropertyModel? propertyWithSameNameAsClass = null; | ||
| foreach (var p in propertyModels) | ||
| { | ||
| if (p.PropertyName == typeName) | ||
| { | ||
| propertyWithSameNameAsClass = p; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| if (propertyWithSameNameAsClass != null) | ||
| { | ||
| var number = 1; | ||
| var candidate = typeName + number; | ||
| while (propertyModels.Any(p => p.PropertyName == candidate)) | ||
| { | ||
| number++; | ||
| } | ||
|
|
||
| propertyWithSameNameAsClass.PropertyName = propertyWithSameNameAsClass.PropertyName + number; | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// finally, our custom ClassTemplateModel and CustomJsonInheritanceConverterTemplateModel | ||
| /// to have access to UsePolymorphicSerialization | ||
| /// This code should be removed when NSwag supports STJ polymorphic serialization | ||
| /// </summary> | ||
| private class CustomClassTemplateModel(string typeName, CSharpGeneratorSettings settings, CSharpTypeResolver resolver, JsonSchema schema, object rootObject, bool usePolymorphicSerialization) | ||
| : ClassTemplateModel(typeName, settings, resolver, schema, rootObject) | ||
| { | ||
| public bool UsePolymorphicSerialization => usePolymorphicSerialization; | ||
| } | ||
|
|
||
| private class CustomJsonInheritanceConverterTemplateModel(CSharpGeneratorSettings settings, bool usePolymorphicSerialization) | ||
| : JsonInheritanceConverterTemplateModel(settings) | ||
| { | ||
| public bool UsePolymorphicSerialization => usePolymorphicSerialization; | ||
| } | ||
| } | ||
| base.CreateOperationModel(operation, Settings); | ||
| } |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
Doc comment intent is clear. Add a safety note about STJ version requirement.
Generated attributes with IgnoreUnrecognizedTypeDiscriminators may require STJ v8+. Consider documenting the minimum required STJ version to avoid consumer build breaks.
🌐 Web query:
💡 Result:
The property appears starting in System.Text.Json package version 9.0.0. [1][2]
Sources:
[1] Microsoft Docs — JsonPolymorphicAttribute.IgnoreUnrecognizedTypeDiscriminators (shows Package: System.Text.Json v9.0.0).
[2] Microsoft Docs — JsonPolymorphicAttribute class (includes the property).
Doc comment intent is clear. Document minimum STJ version. Generated attributes with IgnoreUnrecognizedTypeDiscriminators require System.Text.Json v9.0.0 or later; include this in the doc comment to prevent consumer build breaks. cite12
🤖 Prompt for AI Agents