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 System.Text.Json support in DTO generation #1308

Merged
merged 7 commits into from
Feb 15, 2021
Merged
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
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