Skip to content
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

Add support for STJ-native polymorphic JsonDerivedType and JsonPolymorphic attributes to C# client/schema generator #1595

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "6.0.100",
"version": "7.0.100",
"rollForward": "latestMinor"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@ public async Task When_schema_has_base_schema_then_it_is_referenced_with_STJ()

//// Assert
Assert.True(json.Properties["Item"].ActualTypeSchema.AllOf.First().HasReference);
Assert.Contains("[JsonInheritanceConverter(typeof(Fruit), \"discriminator\")]", code);
Assert.Contains("[JsonInheritanceAttribute(\"Banana\", typeof(Banana))]", code);
Assert.Contains("public class JsonInheritanceConverter<TBase> : System.Text.Json.Serialization.JsonConverter<TBase>", code);
Assert.Contains("[System.Text.Json.Serialization.JsonPolymorphic(TypeDiscriminatorPropertyName = \"discriminator\")]", code);
Assert.Contains("[System.Text.Json.Serialization.JsonDerivedType(typeof(Banana), typeDiscriminator: \"Banana\")]", code);
Assert.DoesNotContain("public class JsonInheritanceConverter<TBase> : System.Text.Json.Serialization.JsonConverter<TBase>", code);
}
}
}
38 changes: 36 additions & 2 deletions src/NJsonSchema.CodeGeneration.CSharp.Tests/InheritanceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,14 +135,20 @@ public async Task When_property_references_any_schema_with_inheritance_then_prop
}

[Fact]
public async Task When_definitions_inherit_from_root_schema()
public async Task When_definitions_inherit_from_root_schema_Newtonsoft()
{
//// Arrange
var path = GetTestDirectory() + "/References/Animal.json";

//// Act
var schema = await JsonSchema.FromFileAsync(path);
var generator = new CSharpGenerator(schema, new CSharpGeneratorSettings { ClassStyle = CSharpClassStyle.Record });
var generator = new CSharpGenerator(
schema,
new CSharpGeneratorSettings
{
ClassStyle = CSharpClassStyle.Record,
JsonLibrary = CSharpJsonLibrary.NewtonsoftJson
});

//// Act
var code = generator.GenerateFile();
Expand All @@ -155,6 +161,33 @@ public async Task When_definitions_inherit_from_root_schema()
Assert.Contains("[JsonInheritanceAttribute(\"PersianCat\", typeof(PersianCat))]", code);
}

[Fact]
public async Task When_definitions_inherit_from_root_schema_STJ()
{
//// Arrange
var path = GetTestDirectory() + "/References/Animal.json";

//// Act
var schema = await JsonSchema.FromFileAsync(path);
var generator = new CSharpGenerator(
schema,
new CSharpGeneratorSettings
{
ClassStyle = CSharpClassStyle.Record,
JsonLibrary = CSharpJsonLibrary.SystemTextJson
});

//// Act
var code = generator.GenerateFile();

//// Assert
Assert.Contains("public abstract partial class Animal", code);
Assert.Contains("public partial class Cat : Animal", code);
Assert.Contains("public partial class PersianCat : Cat", code);
Assert.Contains("[System.Text.Json.Serialization.JsonDerivedType(typeof(Cat), typeDiscriminator: \"Cat\")]", code);
Assert.Contains("[System.Text.Json.Serialization.JsonDerivedType(typeof(PersianCat), typeDiscriminator: \"PersianCat\")]", code);
}

private string GetTestDirectory()
{
var codeBase = Assembly.GetExecutingAssembly().CodeBase;
Expand All @@ -163,3 +196,4 @@ private string GetTestDirectory()
}
}
}

6 changes: 5 additions & 1 deletion src/NJsonSchema.CodeGeneration.CSharp/Templates/Class.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@
{%- endif -%}
{%- if HasDiscriminator -%}
{%- if UseSystemTextJson -%}
[JsonInheritanceConverter(typeof({{ ClassName }}), "{{ Discriminator }}")]
[System.Text.Json.Serialization.JsonPolymorphic(TypeDiscriminatorPropertyName = "{{ Discriminator }}")]
{%- else -%}
[Newtonsoft.Json.JsonConverter(typeof(JsonInheritanceConverter), "{{ Discriminator }}")]
{%- endif -%}
{%- for derivedClass in DerivedClasses -%}
{%- if derivedClass.IsAbstract != true -%}
{%- if UseSystemTextJson -%}
[System.Text.Json.Serialization.JsonDerivedType(typeof({{ derivedClass.ClassName }}), typeDiscriminator: "{{ derivedClass.Discriminator }}")]
{%- else -%}
[JsonInheritanceAttribute("{{ derivedClass.Discriminator }}", typeof({{ derivedClass.ClassName }}))]
{%- endif -%}
{%- endif -%}
{%- endfor -%}
{%- endif -%}
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "{{ ToolchainVersion }}")]
Expand Down
51 changes: 51 additions & 0 deletions src/NJsonSchema.Tests/Generation/InheritanceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -439,5 +439,56 @@ public async Task When_using_JsonInheritanceAttribute_then_schema_is_correct()
Assert.Contains(@"""a"": """, data);
Assert.Contains(@"""o"": """, data);
}

#if NET7_0_OR_GREATER
public class AppleSTJ : FruitSTJ
{
public string Foo { get; set; }
}

public class OrangeSTJ : FruitSTJ
{
public string Bar { get; set; }
}

[System.Text.Json.Serialization.JsonDerivedType(typeof(AppleSTJ), typeDiscriminator: "a")]
[System.Text.Json.Serialization.JsonDerivedType(typeof(OrangeSTJ), typeDiscriminator: "o")]
[System.Text.Json.Serialization.JsonPolymorphic(TypeDiscriminatorPropertyName = "k")]
public class FruitSTJ
{
public string Baz { get; set; }
}

[Fact]
public async Task When_using_JsonDerivedType_then_schema_is_correct()
{
//// Act
var schema = JsonSchema.FromType<FruitSTJ>();
var data = schema.ToJson();

//// Assert
Assert.NotNull(data);
Assert.Contains(@"""a"": """, data);
Assert.Contains(@"""o"": """, data);
}

[Fact]
public async Task When_using_JsonDerivedType_then_schema_is_equivalent_to_schema_using_JsonDerivedType()
{
//// Arrange
var expectedJson = JsonSchema
.FromType<Fruit>()
.ToJson();

//// Act
var actualJson = JsonSchema
.FromType<FruitSTJ>()
.ToJson()
.Replace("STJ", string.Empty);

//// Assert
Assert.Equal(expectedJson, actualJson);
}
#endif
}
}
2 changes: 1 addition & 1 deletion src/NJsonSchema.Tests/NJsonSchema.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net6.0;net461</TargetFrameworks>
<TargetFrameworks>net7.0;net6.0;net461</TargetFrameworks>
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
Expand Down
34 changes: 33 additions & 1 deletion src/NJsonSchema/Generation/JsonSchemaGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -988,6 +988,26 @@ private void GenerateKnownTypes(Type type, JsonSchemaResolver schemaResolver)

if (Settings.GenerateKnownTypes)
{

#if NET7_0_OR_GREATER
schnerring marked this conversation as resolved.
Show resolved Hide resolved
var jsonDerivedTypeAttributes = attributes
// Known types of inherited classes will be generated later (in GenerateInheritance)
.GetAssignableToTypeName(nameof(System.Text.Json.Serialization.JsonDerivedTypeAttribute), TypeNameStyle.Name)
.OfType<Attribute>();

foreach (dynamic attribute in jsonDerivedTypeAttributes)
{
if (attribute.DerivedType != null)
{
AddKnownType(attribute.DerivedType, schemaResolver);
}
else
{
throw new ArgumentException($"A KnownType attribute on {type.FullName} does not specify a type or a method name.", nameof(type));
}
}
#endif

var knownTypeAttributes = attributes
// Known types of inherited classes will be generated later (in GenerateInheritance)
.GetAssignableToTypeName("KnownTypeAttribute", TypeNameStyle.Name)
Expand Down Expand Up @@ -1184,7 +1204,13 @@ private void GenerateInheritanceDiscriminator(Type type, JsonSchema schema, Json
private object TryGetInheritanceDiscriminatorConverter(Type type)
{
var typeAttributes = type.GetTypeInfo().GetCustomAttributes(false).OfType<Attribute>();

#if NET7_0_OR_GREATER
dynamic jsonPolymorphicAttribute = typeAttributes.FirstAssignableToTypeNameOrDefault(nameof(System.Text.Json.Serialization.JsonPolymorphicAttribute), TypeNameStyle.Name);
if (jsonPolymorphicAttribute != null)
{
return jsonPolymorphicAttribute;
schnerring marked this conversation as resolved.
Show resolved Hide resolved
}
#endif
dynamic jsonConverterAttribute = typeAttributes.FirstAssignableToTypeNameOrDefault(nameof(JsonConverterAttribute), TypeNameStyle.Name);
if (jsonConverterAttribute != null)
{
Expand All @@ -1207,6 +1233,12 @@ private object TryGetInheritanceDiscriminatorConverter(Type type)

private string TryGetInheritanceDiscriminatorName(object jsonInheritanceConverter)
{
#if NET7_0_OR_GREATER
if (jsonInheritanceConverter is System.Text.Json.Serialization.JsonPolymorphicAttribute jsonPolymorphicAttribute)
return !string.IsNullOrWhiteSpace(jsonPolymorphicAttribute.TypeDiscriminatorPropertyName) ?
jsonPolymorphicAttribute.TypeDiscriminatorPropertyName :
JsonInheritanceConverter.DefaultDiscriminatorName;
#endif
return ObjectExtensions.TryGetPropertyValue(
jsonInheritanceConverter,
nameof(JsonInheritanceConverter.DiscriminatorName),
Expand Down
2 changes: 1 addition & 1 deletion src/NJsonSchema/NJsonSchema.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard1.0;netstandard2.0;net40;net45</TargetFrameworks>
<TargetFrameworks>net7.0;netstandard1.0;netstandard2.0;net40;net45</TargetFrameworks>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net40'">
<DefineConstants>LEGACY</DefineConstants>
Expand Down
52 changes: 41 additions & 11 deletions src/NJsonSchema/OpenApiDiscriminator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
using System;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Namotion.Reflection;
using NJsonSchema.Infrastructure;
using NJsonSchema.References;
using Newtonsoft.Json.Linq;
Expand All @@ -28,7 +30,16 @@ public class OpenApiDiscriminator
[JsonConverter(typeof(DiscriminatorMappingConverter))]
public IDictionary<string, JsonSchema> Mapping { get; } = new Dictionary<string, JsonSchema>();

/// <summary>The currently used <see cref="JsonInheritanceConverter"/>.</summary>
#if NET7_0_OR_GREATER
/// <summary>
/// The currently used <see cref="JsonInheritanceConverter"/>, or
/// the currently used <see cref="System.Text.Json.Serialization.JsonPolymorphicAttribute" /> if using System.Text.Json polymorphic type hierarchy serialization features.
/// </summary>
#else
/// <summary>
/// The currently used <see cref="JsonInheritanceConverter"/>
/// </summary>
#endif
[JsonIgnore]
public object JsonInheritanceConverter { get; set; }

Expand All @@ -37,7 +48,31 @@ public class OpenApiDiscriminator
/// <param name="schema">The schema.</param>
public void AddMapping(Type type, JsonSchema schema)
{
dynamic converter = JsonInheritanceConverter;
var discriminatorValue = GetDiscriminatorValue(type);
Mapping[discriminatorValue] = new JsonSchema { Reference = schema.ActualSchema };
}

private string GetDiscriminatorValue(Type derivedType)
{
#if NET7_0_OR_GREATER
var type = derivedType;
do
{
var jsonDerivedTypeAttribute = type
.GetTypeInfo()
.GetCustomAttributes()
.OfType<System.Text.Json.Serialization.JsonDerivedTypeAttribute>()
.SingleOrDefault(a => a.DerivedType == derivedType);

if (jsonDerivedTypeAttribute is not null)
{
var typeDiscriminator = jsonDerivedTypeAttribute.TypeDiscriminator?.ToString();
return typeDiscriminator ?? type.Name;
}

type = type.BaseType;
} while (type is not null);
#endif

var getDiscriminatorValueMethod = JsonInheritanceConverter?.GetType()
#if LEGACY
Expand All @@ -46,15 +81,10 @@ public void AddMapping(Type type, JsonSchema schema)
.GetRuntimeMethod(nameof(Converters.JsonInheritanceConverter.GetDiscriminatorValue), new Type[] { typeof(Type) });
#endif

if (getDiscriminatorValueMethod != null)
{
var discriminatorValue = converter.GetDiscriminatorValue(type);
Mapping[discriminatorValue] = new JsonSchema { Reference = schema.ActualSchema };
}
else
{
Mapping[type.Name] = new JsonSchema { Reference = schema.ActualSchema };
}
dynamic converter = JsonInheritanceConverter;
return getDiscriminatorValueMethod != null ?
(string)converter.GetDiscriminatorValue(derivedType) :
derivedType.Name;
}

/// <summary>
Expand Down