Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Commit

Permalink
Support JsonConverterAttribute on enumerable properties (#41653)
Browse files Browse the repository at this point in the history
  • Loading branch information
steveharter committed Oct 10, 2019
1 parent e753ecf commit 4580a4d
Show file tree
Hide file tree
Showing 2 changed files with 160 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -97,28 +97,16 @@ internal static JsonPropertyInfo CreateProperty(
return JsonPropertyInfo.CreateIgnoredPropertyPlaceholder(propertyInfo, options);
}

Type collectionElementType = null;
switch (GetClassType(runtimePropertyType, options))
// Obtain the custom converter for the property.
if (converter == null)
{
case ClassType.Enumerable:
case ClassType.Dictionary:
case ClassType.IDictionaryConstructible:
case ClassType.Unknown:
collectionElementType = GetElementType(runtimePropertyType, parentClassType, propertyInfo, options);
break;
converter = options.DetermineConverterForProperty(parentClassType, runtimePropertyType, propertyInfo);
}

// Create the JsonPropertyInfo<TType, TProperty>
// Obtain the type of the JsonPropertyInfo class to construct.
Type propertyInfoClassType;
if (runtimePropertyType.IsGenericType && runtimePropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
// First try to find a converter for the Nullable, then if not found use the underlying type.
// This supports custom converters that want to (de)serialize as null when the value is not null.
if (converter == null)
{
converter = options.DetermineConverterForProperty(parentClassType, runtimePropertyType, propertyInfo);
}

if (converter != null)
{
propertyInfoClassType = typeof(JsonPropertyInfoNotNullable<,,,>).MakeGenericType(
Expand All @@ -129,18 +117,14 @@ internal static JsonPropertyInfo CreateProperty(
}
else
{
// Attempt to find converter for underlying type.
Type typeToConvert = Nullable.GetUnderlyingType(runtimePropertyType);
converter = options.DetermineConverterForProperty(parentClassType, typeToConvert, propertyInfo);
propertyInfoClassType = typeof(JsonPropertyInfoNullable<,>).MakeGenericType(parentClassType, typeToConvert);
}
}
else
{
if (converter == null)
{
converter = options.DetermineConverterForProperty(parentClassType, runtimePropertyType, propertyInfo);
}

Type typeToConvert = converter?.TypeToConvert;
if (typeToConvert == null)
{
Expand Down Expand Up @@ -176,16 +160,41 @@ internal static JsonPropertyInfo CreateProperty(
}
}

JsonPropertyInfo jsonInfo = (JsonPropertyInfo)Activator.CreateInstance(
// Create the JsonPropertyInfo instance.
JsonPropertyInfo jsonPropertyInfo = (JsonPropertyInfo)Activator.CreateInstance(
propertyInfoClassType,
BindingFlags.Instance | BindingFlags.Public,
binder: null,
args: null,
culture: null);

jsonInfo.Initialize(parentClassType, declaredPropertyType, runtimePropertyType, implementedPropertyType, propertyInfo, collectionElementType, converter, options);
// Obtain the collection element type.
Type collectionElementType = null;
if (converter == null)
{
switch (GetClassType(runtimePropertyType, options))
{
case ClassType.Enumerable:
case ClassType.Dictionary:
case ClassType.IDictionaryConstructible:
case ClassType.Unknown:
collectionElementType = GetElementType(runtimePropertyType, parentClassType, propertyInfo, options);
break;
}
}

return jsonInfo;
// Initialize the JsonPropertyInfo.
jsonPropertyInfo.Initialize(
parentClassType,
declaredPropertyType,
runtimePropertyType,
implementedPropertyType,
propertyInfo,
collectionElementType,
converter,
options);

return jsonPropertyInfo;
}

internal JsonPropertyInfo CreateRootObject(JsonSerializerOptions options)
Expand Down
127 changes: 127 additions & 0 deletions src/System.Text.Json/tests/Serialization/CustomConverterTests.List.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Xunit;

Expand Down Expand Up @@ -237,5 +238,131 @@ public static void ListConverterPolymorphic()
jsonSerialized = JsonSerializer.Serialize(contraVariantList, options);
Assert.Equal(json, jsonSerialized);
}

private class MyModelWithConverterAttributes
{
[JsonConverter(typeof(ListToStringElementsConverter))]
public List<string> ItemsList { get; set; }

[JsonConverter(typeof(ListToArrayElementsConverter))]
public string[] ItemsArray { get; set; }

[JsonConverter(typeof(ListToDictionaryElementsConverter))]
public Dictionary<string, string> ItemsDictionary { get; set; }
}

private class MyModelWithNoConverterAttributes
{
public List<string> ItemsList { get; set; }
public string[] ItemsArray { get; set; }
public Dictionary<string, string> ItemsDictionary { get; set; }
}

[Fact]
public static void CustomListWithJsonConverterAttribute()
{
const string Json =
@"{""ItemsList"":[""hello"",1,true]," +
@"""ItemsArray"":[""hello"",1,true]," +
@"""ItemsDictionary"":{""hello"":""hello"",""1"":1,""true"":true}}";

// Baseline failure (no JsonConverterAttributes).
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<MyModelWithNoConverterAttributes>(Json));

// Success case.
MyModelWithConverterAttributes obj;

void Verify()
{
Assert.Equal("hello", obj.ItemsList[0]);
Assert.Equal("1", obj.ItemsList[1]);
Assert.Equal("True", obj.ItemsList[2]);

Assert.Equal("hello", obj.ItemsArray[0]);
Assert.Equal("1", obj.ItemsArray[1]);
Assert.Equal("True", obj.ItemsArray[2]);

Assert.Equal("hello", obj.ItemsDictionary["hello"]);
Assert.Equal("1", obj.ItemsDictionary["1"]);
Assert.Equal("True", obj.ItemsDictionary["true"]);
}

obj = JsonSerializer.Deserialize<MyModelWithConverterAttributes>(Json);
Verify();

string jsonRoundTripped = JsonSerializer.Serialize<MyModelWithConverterAttributes>(obj);
Assert.Equal(
@"{""ItemsList"":[""hello"",""1"",""True""]," +
@"""ItemsArray"":[""hello"",""1"",""True""]," +
@"""ItemsDictionary"":{""hello"":""hello"",""1"":""1"",""true"":""True""}}",
jsonRoundTripped);

obj = JsonSerializer.Deserialize<MyModelWithConverterAttributes>(jsonRoundTripped);
Verify();
}

/// <summary>
/// This converter coerces JSON array items into <see cref="List{string}"/> when deserializing.
/// </summary>
class ListToStringElementsConverter : JsonConverter<List<string>>
{
public override List<string> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using (var doc = JsonDocument.ParseValue(ref reader))
{
return doc.RootElement.EnumerateArray().Select(e => e.ToString()).ToList();
}
}

public override void Write(Utf8JsonWriter writer, List<string> value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, options);
}
}

/// <summary>
/// This converter coerces JSON array items into <see cref="string[]"/> when deserializing.
/// </summary>
class ListToArrayElementsConverter : JsonConverter<string[]>
{
public override string[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using (var doc = JsonDocument.ParseValue(ref reader))
{
return doc.RootElement.EnumerateArray().Select(e => e.ToString()).ToArray();
}
}

public override void Write(Utf8JsonWriter writer, string[] value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, options);
}
}

/// <summary>
/// This converter coerces JSON property values into <see cref="Dictionary{string, string}"> when deserializing.
/// </summary>
class ListToDictionaryElementsConverter : JsonConverter<Dictionary<string, string>>
{
public override Dictionary<string, string> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var dictionary = new Dictionary<string, string>();

using (var doc = JsonDocument.ParseValue(ref reader))
{
foreach (JsonProperty property in doc.RootElement.EnumerateObject())
{
dictionary.Add(property.Name, property.Value.ToString());
}
}

return dictionary;
}

public override void Write(Utf8JsonWriter writer, Dictionary<string, string> value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, options);
}
}
}
}

0 comments on commit 4580a4d

Please sign in to comment.