Skip to content

Commit

Permalink
Add System.Text.Json support in DTO generation (#1308)
Browse files Browse the repository at this point in the history
* Add System.Text.Json support in DTO generation

* Update

* Improve JsonInheritanceConverter

* Add inheritance converter + tests

* Update

* Update liquid
  • Loading branch information
RicoSuter committed Feb 15, 2021
1 parent 39d09be commit 5df1cce
Show file tree
Hide file tree
Showing 21 changed files with 609 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public CSharpGeneratorSettings()
DictionaryBaseType = "System.Collections.Generic.Dictionary";

ClassStyle = CSharpClassStyle.Poco;
JsonLibrary = CSharpJsonLibrary.NewtonsoftJson;

RequiredPropertiesMustBeDefined = true;
GenerateDataAnnotations = true;
Expand Down Expand Up @@ -99,6 +100,9 @@ public CSharpGeneratorSettings()
/// <summary>Gets or sets the CSharp class style (default: 'Poco').</summary>
public CSharpClassStyle ClassStyle { get; set; }

/// <summary>Gets or sets the CSharp JSON library to use (default: 'NewtonsoftJson', 'SystemTextJson' is experimental/not complete).</summary>
public CSharpJsonLibrary JsonLibrary { get; set; }

/// <summary>Gets or sets the access modifier of generated classes and interfaces (default: 'public').</summary>
public string TypeAccessModifier { get; set; }

Expand Down
20 changes: 20 additions & 0 deletions src/NJsonSchema.CodeGeneration.CSharp/CSharpJsonLibrary.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//-----------------------------------------------------------------------
// <copyright file="CSharpClassStyle.cs" company="NJsonSchema">
// Copyright (c) Rico Suter. All rights reserved.
// </copyright>
// <license>https://github.com/RicoSuter/NJsonSchema/blob/master/LICENSE.md</license>
// <author>Rico Suter, mail@rsuter.com</author>
//-----------------------------------------------------------------------

namespace NJsonSchema.CodeGeneration.CSharp
{
/// <summary>The CSharp JSON library to use.</summary>
public enum CSharpJsonLibrary
{
/// <summary>Use Newtonsoft.Json</summary>
NewtonsoftJson,

/// <summary>Use System.Text.Json</summary>
SystemTextJson
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,16 @@ public static string GenerateJsonSerializerParameterCode(CSharpGeneratorSettings
var hasJsonConverters = jsonConverters.Any();

var useSettingsTransformationMethod = !string.IsNullOrEmpty(settings.JsonSerializerSettingsTransformationMethod);
return settings.JsonLibrary == CSharpJsonLibrary.SystemTextJson ?
string.Empty : // TODO(system.text.json): What to do here?
GenerateForNewtonsoftJson(settings, jsonConverters, hasJsonConverters, useSettingsTransformationMethod);
}

private static string GenerateForNewtonsoftJson(CSharpGeneratorSettings settings, List<string> jsonConverters, bool hasJsonConverters, bool useSettingsTransformationMethod)
{
if (settings.HandleReferences || useSettingsTransformationMethod)
{
// TODO(system.text.json): Also support System.Text.Json
return ", " +
(useSettingsTransformationMethod ? settings.JsonSerializerSettingsTransformationMethod + "(" : string.Empty) +
"new Newtonsoft.Json.JsonSerializerSettings { " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ public ClassTemplateModel(string typeName, CSharpGeneratorSettings settings,
}
}

/// <summary>Gets a value indicating whether to use System.Text.Json</summary>
public bool UseSystemTextJson => _settings.JsonLibrary == CSharpJsonLibrary.SystemTextJson;

/// <summary>Gets or sets the class name.</summary>
public override string ClassName { get; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
{% assign sortedProperties = AllProperties | sort: "Name" -%}
{% assign sortedParentProperties = parentProperties | sort: "Name" -%}

{% if UseSystemTextJson -%}
[System.Text.Json.Serialization.JsonConstructor]
{% else -%}
[Newtonsoft.Json.JsonConstructor]
{% endif -%}
{% if IsAbstract %}protected{% else %}public{% endif %} {{ClassName}}({% for property in sortedProperties -%}{% if skipComma -%}{% assign skipComma = false %}{% else %}, {% endif -%} {{ property.Type }} @{{ property.Name | lowercamelcase }}{% endfor -%})
{% assign skipComma = true -%}
{% if HasInheritance -%}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
public static {{ ClassName }} FromJson(string data)
{
{% if UseSystemTextJson -%}
return System.Text.Json.JsonSerializer.Deserialize<{{ ClassName }}>(data{{ JsonSerializerParameterCode }});
{% else -%}
return Newtonsoft.Json.JsonConvert.DeserializeObject<{{ ClassName }}>(data{{ JsonSerializerParameterCode }});
{% endif -%}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
public string ToJson()
{
{% if UseSystemTextJson -%}
return System.Text.Json.JsonSerializer.Serialize(this{{ JsonSerializerParameterCode }});
{% else -%}
return Newtonsoft.Json.JsonConvert.SerializeObject(this{{ JsonSerializerParameterCode }});
{% endif -%}
}
29 changes: 28 additions & 1 deletion src/NJsonSchema.CodeGeneration.CSharp/Templates/Class.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
/// <summary>{{ Description | csharpdocs }}</summary>
{% endif -%}
{% if HasDiscriminator -%}
{% if UseSystemTextJson -%}
[JsonInheritanceConverter(typeof({{ ClassName }}), "{{ Discriminator }}")]
{% else -%}
[Newtonsoft.Json.JsonConverter(typeof(JsonInheritanceConverter), "{{ Discriminator }}")]
{% endif -%}
{% for derivedClass in DerivedClasses -%}
{% if derivedClass.IsAbstract != true -%}
[JsonInheritanceAttribute("{{ derivedClass.Discriminator }}", typeof({{ derivedClass.ClassName }}))]
Expand All @@ -11,7 +15,11 @@
{% endif -%}
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "{{ ToolchainVersion }}")]
{% if InheritsExceptionSchema -%}
{% if UseSystemTextJson -%}
// TODO(system.text.json): What to do here?
{% else -%}
[Newtonsoft.Json.JsonObjectAttribute]
{% endif -%}
{% endif -%}
{% if IsDeprecated -%}
[System.Obsolete{% if HasDeprecatedMessage %}({{ DeprecatedMessage | literal }}){% endif %}]
Expand All @@ -20,7 +28,7 @@
{{ TypeAccessModifier }} {% if IsAbstract %}abstract {% endif %}partial class {{ClassName}} {% template Class.Inheritance %}
{
{% if IsTuple -%}
public {{ClassName}}({% for tupleType in TupleTypes -%}{{ tupleType }} item{{ forloop.index }}{% if forloop.last == false %}, {% endif %}{% endfor -%}) : base({% for tupleType in TupleTypes -%}item{{ forloop.index }}{% if forloop.last == false %}, {% endif %}{% endfor -%})
public {{ ClassName }}({% for tupleType in TupleTypes -%}{{ tupleType }} item{{ forloop.index }}{% if forloop.last == false %}, {% endif %}{% endfor -%}) : base({% for tupleType in TupleTypes -%}item{{ forloop.index }}{% if forloop.last == false %}, {% endif %}{% endfor -%})
{
}

Expand All @@ -40,7 +48,14 @@
{% if property.HasDescription -%}
/// <summary>{{ property.Description | csharpdocs }}</summary>
{% endif -%}
{% if UseSystemTextJson -%}
[System.Text.Json.Serialization.JsonPropertyName("{{ property.Name }}"]
{% if property.IsStringEnumArray -%}
// TODO(system.text.json): Add string enum item converter
{% endif -%}
{% else -%}
[Newtonsoft.Json.JsonProperty("{{ property.Name }}", Required = {{ property.JsonPropertyRequiredCode }}{% if property.IsStringEnumArray %}, ItemConverterType = typeof(Newtonsoft.Json.Converters.StringEnumConverter){% endif %})]
{% endif -%}
{% if property.RenderRequiredAttribute -%}
[System.ComponentModel.DataAnnotations.Required{% if property.AllowEmptyStrings %}(AllowEmptyStrings = true){% endif %}]
{% endif -%}
Expand All @@ -60,10 +75,18 @@
[System.ComponentModel.DataAnnotations.RegularExpression(@"{{ property.RegularExpressionValue }}")]
{% endif -%}
{% if property.IsStringEnum -%}
{% if UseSystemTextJson -%}
[System.Text.Json.Serialization.JsonConverter(System.Text.Json.Serialization.JsonStringEnumConverter)]
{% else -%}
[Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
{% endif -%}
{% endif -%}
{% if property.IsDate and UseDateFormatConverter -%}
{% if UseSystemTextJson -%}
[System.Text.Json.Serialization.JsonConverter(typeof(DateFormatConverter))]
{% else -%}
[Newtonsoft.Json.JsonConverter(typeof(DateFormatConverter))]
{% endif -%}
{% endif -%}
{% if property.IsDeprecated -%}
[System.Obsolete{% if property.HasDeprecatedMessage %}({{ property.DeprecatedMessage | literal }}){% endif %}]
Expand Down Expand Up @@ -94,7 +117,11 @@
{% if HasAdditionalPropertiesType -%}
private System.Collections.Generic.IDictionary<string, {{ AdditionalPropertiesType }}> _additionalProperties = new System.Collections.Generic.Dictionary<string, {{ AdditionalPropertiesType }}>();

{% if UseSystemTextJson -%}
[System.Text.Json.Serialization.JsonExtensionData]
{% else -%}
[Newtonsoft.Json.JsonExtensionData]
{% endif -%}
public System.Collections.Generic.IDictionary<string, {{ AdditionalPropertiesType }}> AdditionalProperties
{
get { return _additionalProperties; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "{{ ToolchainVersion }}")]
{% if UseSystemTextJson -%}
internal class DateFormatConverter : JsonConverter<DateTime>
{
public override System.DateTime Read(ref System.Text.Json.Utf8JsonReader reader, System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options)
{
return DateTime.Parse(reader.GetString());
}

public override void Write(System.Text.Json.Utf8JsonReader writer, System.DateTime value, System.Text.Json.JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString("yyyy-MM-dd"));
}
}
{% else -%}
internal class DateFormatConverter : Newtonsoft.Json.Converters.IsoDateTimeConverter
{
public DateFormatConverter()
{
DateTimeFormat = "yyyy-MM-dd";
}
}
}
{% endif -%}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "{{ ToolchainVersion }}")]
[System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = true)]
[System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Interface, AllowMultiple = true)]
internal class JsonInheritanceAttribute : System.Attribute
{
public JsonInheritanceAttribute(string key, System.Type type)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,120 @@
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "{{ ToolchainVersion }}")]
{% if UseSystemTextJson -%}
internal class JsonInheritanceConverterAttribute : JsonConverterAttribute
{
public string DiscriminatorName { get; }

public JsonInheritanceConverterAttribute(System.Type baseType, string discriminatorName = "discriminator")
: base(typeof(JsonInheritanceConverter<>).MakeGenericType(baseType))
{
DiscriminatorName = discriminatorName;
}
}

internal class JsonInheritanceConverter<TBase> : System.Text.Json.Serialization.JsonConverter<TBase>
{
private readonly string _discriminatorName;

public JsonInheritanceConverter()
{
var attribute = System.Reflection.CustomAttributeExtensions.GetCustomAttribute<JsonInheritanceConverterAttribute>(typeof(TBase));
_discriminatorName = attribute?.DiscriminatorName ?? "discriminator";
}

public JsonInheritanceConverter(string discriminatorName)
{
_discriminatorName = discriminatorName;
}

public virtual string DiscriminatorName => _discriminatorName;

public override TBase Read(ref System.Text.Json.Utf8JsonReader reader, System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options)
{
var document = System.Text.Json.JsonDocument.ParseValue(ref reader);
var hasDiscriminator = document.RootElement.TryGetProperty(_discriminatorName, out var discriminator);
var subtype = GetDiscriminatorType(document.RootElement, typeToConvert, hasDiscriminator ? discriminator.GetString() : null);

var bufferWriter = new System.IO.MemoryStream();
using (var writer = new System.Text.Json.Utf8JsonWriter(bufferWriter))
{
document.RootElement.WriteTo(writer);
}

return (TBase)System.Text.Json.JsonSerializer.Deserialize(bufferWriter.ToArray(), subtype, options);
}

public override void Write(System.Text.Json.Utf8JsonWriter writer, TBase value, System.Text.Json.JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteString(_discriminatorName, GetDiscriminatorValue(value.GetType()));

var bytes = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes((object)value, options);
var document = System.Text.Json.JsonDocument.Parse(bytes);
foreach (var property in document.RootElement.EnumerateObject())
{
property.WriteTo(writer);
}

writer.WriteEndObject();
}

public virtual string GetDiscriminatorValue(System.Type type)
{
var jsonInheritanceAttributeDiscriminator = GetSubtypeDiscriminator(type);
if (jsonInheritanceAttributeDiscriminator != null)
{
return jsonInheritanceAttributeDiscriminator;
}

return type.Name;
}

protected virtual System.Type GetDiscriminatorType(System.Text.Json.JsonElement jObject, System.Type objectType, string discriminatorValue)
{
var jsonInheritanceAttributeSubtype = GetObjectSubtype(objectType, discriminatorValue);
if (jsonInheritanceAttributeSubtype != null)
{
return jsonInheritanceAttributeSubtype;
}

if (objectType.Name == discriminatorValue)
{
return objectType;
}

var typeName = objectType.Namespace + "." + discriminatorValue;
var subtype = System.Reflection.IntrospectionExtensions.GetTypeInfo(objectType).Assembly.GetType(typeName);
if (subtype != null)
{
return subtype;
}

throw new System.InvalidOperationException("Could not find subtype of '" + objectType.Name + "' with discriminator '" + discriminatorValue + "'.");
}

private System.Type GetObjectSubtype(System.Type objectType, string discriminator)
{
foreach (var attribute in System.Reflection.CustomAttributeExtensions.GetCustomAttributes<JsonInheritanceAttribute>(System.Reflection.IntrospectionExtensions.GetTypeInfo(objectType), true))
{
if (attribute.Key == discriminator)
return attribute.Type;
}

return objectType;
}

private string GetSubtypeDiscriminator(System.Type objectType)
{
foreach (var attribute in System.Reflection.CustomAttributeExtensions.GetCustomAttributes<JsonInheritanceAttribute>(System.Reflection.IntrospectionExtensions.GetTypeInfo(objectType), true))
{
if (attribute.Type == objectType)
return attribute.Key;
}

return objectType.Name;
}
}
{% else -%}
internal class JsonInheritanceConverter : Newtonsoft.Json.JsonConverter
{
internal static readonly string DefaultDiscriminatorName = "discriminator";
Expand Down Expand Up @@ -117,3 +233,4 @@ internal class JsonInheritanceConverter : Newtonsoft.Json.JsonConverter
return objectType.Name;
}
}
{% endif -%}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<WarningsAsErrors />
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DotLiquid" Version="2.0.314" />
<PackageReference Include="DotLiquid" Version="2.0.385" />
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard1.3'">
Expand Down
33 changes: 31 additions & 2 deletions src/NJsonSchema.Tests/Generation/InheritanceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,6 @@ public async Task When_allOf_schema_is_object_type_then_it_is_an_inherited_schem
[Fact]
public async Task When_generating_type_with_inheritance_then_allOf_has_one_item()
{
//// Arrange

//// Act
var schema = JsonSchema.FromType<Teacher>();

Expand Down Expand Up @@ -410,5 +408,36 @@ public async Task When_class_with_discriminator_has_base_class_then_mapping_is_p
Assert.NotNull(exceptionBase.ActualTypeSchema.DiscriminatorObject);
Assert.True(exceptionBase.ActualTypeSchema.DiscriminatorObject.Mapping.ContainsKey("MyException"));
}

public class Apple : Fruit
{
public string Foo { get; set; }
}

public class Orange : Fruit
{
public string Bar { get; set; }
}

[JsonInheritance("a", typeof(Apple))]
[JsonInheritance("o", typeof(Orange))]
[JsonConverter(typeof(JsonInheritanceConverter), "k")]
public class Fruit
{
public string Baz { get; set; }
}

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

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

0 comments on commit 5df1cce

Please sign in to comment.