Skip to content

Commit a297cae

Browse files
Add KeyValuePairCollectionConverterFactory
1 parent a566634 commit a297cae

10 files changed

Lines changed: 357 additions & 10 deletions

System.Text.Json.Extensions.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ EndProject
55
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E18A8452-2B48-4C7F-89C1-F0B81FDA6D7E}"
66
ProjectSection(SolutionItems) = preProject
77
Readme.md = Readme.md
8+
.github/workflows/dotnet-core.yml = .github/workflows/dotnet-core.yml
89
EndProjectSection
910
EndProject
1011
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Json.ExtensionsTest", "System.Text.Json.ExtensionsTest\System.Text.Json.ExtensionsTest.csproj", "{160C8D7A-C96E-4FCC-8585-7CE264641458}"
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using System.Collections.Generic;
2+
using System.Text.Json.Serialization;
3+
4+
namespace System.Text.Json.Extensions.Converters
5+
{
6+
public class KeyValuePairCollectionConverter<TValue, TCollection>
7+
: JsonConverter<TCollection>
8+
where TCollection : ICollection<KeyValuePair<string, TValue>>, new()
9+
{
10+
public override TCollection Read(ref Utf8JsonReader reader, Type typeToConvert,
11+
JsonSerializerOptions options)
12+
{
13+
reader.Read();
14+
15+
if (reader.TokenType != JsonTokenType.StartObject)
16+
{
17+
throw reader.GetException($"Expected StartObject but found {reader.TokenType}");
18+
}
19+
20+
var collection = new TCollection();
21+
22+
while (reader.Read())
23+
{
24+
if (reader.TokenType == JsonTokenType.EndObject)
25+
{
26+
return collection;
27+
}
28+
29+
if (reader.TokenType != JsonTokenType.PropertyName)
30+
{
31+
throw reader.GetException($"Expected PropertyName but found {reader.TokenType}");
32+
}
33+
34+
var propertyName = reader.GetString();
35+
var propertyValue = reader.ReadObject<TValue>(options);
36+
collection.Add(new KeyValuePair<string, TValue>(propertyName, propertyValue));
37+
}
38+
39+
throw reader.GetException($"Expected EndObject but JSON ended");
40+
}
41+
42+
public override void Write(Utf8JsonWriter writer, TCollection collection, JsonSerializerOptions options)
43+
{
44+
if (collection == null)
45+
{
46+
writer.WriteNullValue();
47+
return;
48+
}
49+
50+
writer.WriteStartObject();
51+
52+
foreach (var (key, value) in collection)
53+
{
54+
writer.WriteObject(key, value, options);
55+
}
56+
57+
writer.WriteEndObject();
58+
}
59+
}
60+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using System.Collections.Generic;
2+
using System.Text.Json.Serialization;
3+
4+
namespace System.Text.Json.Extensions.Converters
5+
{
6+
public class KeyValuePairCollectionConverterFactory : JsonConverterFactory
7+
{
8+
public Type GetValueType(Type typeToConvert)
9+
{
10+
if (typeToConvert.GetConstructor(new Type[0]) == null)
11+
{
12+
return null;
13+
}
14+
15+
Type valueType = null;
16+
17+
foreach (var @interface in typeToConvert.GetInterfaces())
18+
{
19+
if (!@interface.IsGenericType
20+
|| @interface.GetGenericTypeDefinition() != typeof(ICollection<>))
21+
{
22+
continue;
23+
}
24+
25+
var itemType = @interface.GetGenericArguments()[0];
26+
if (!itemType.IsGenericType
27+
|| itemType.GetGenericTypeDefinition() != typeof(KeyValuePair<,>))
28+
{
29+
continue;
30+
}
31+
32+
var genericArguments = itemType.GetGenericArguments();
33+
if (genericArguments[0] != typeof(string))
34+
{
35+
continue;
36+
}
37+
38+
if (valueType != null && valueType != genericArguments[1])
39+
{
40+
return null;
41+
}
42+
43+
valueType = genericArguments[1];
44+
}
45+
46+
return valueType;
47+
}
48+
49+
public override bool CanConvert(Type typeToConvert)
50+
{
51+
return GetValueType(typeToConvert) != null;
52+
}
53+
54+
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
55+
{
56+
return (JsonConverter) Activator.CreateInstance(
57+
typeof(KeyValuePairCollectionConverter<,>)
58+
.MakeGenericType(GetValueType(typeToConvert), typeToConvert));
59+
}
60+
}
61+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
namespace System.Text.Json.Extensions
2+
{
3+
public static class JsonSerializerOptionsExtensions
4+
{
5+
public static JsonReaderOptions GetReaderOptions(this JsonSerializerOptions jsonSerializerOptions)
6+
{
7+
return new JsonReaderOptions
8+
{
9+
CommentHandling = jsonSerializerOptions.ReadCommentHandling,
10+
AllowTrailingCommas = jsonSerializerOptions.AllowTrailingCommas,
11+
MaxDepth = jsonSerializerOptions.MaxDepth
12+
};
13+
}
14+
15+
public static JsonWriterOptions GetWriterOptions(this JsonSerializerOptions jsonSerializerOptions, bool skipValidation = true)
16+
{
17+
return new JsonWriterOptions
18+
{
19+
Encoder = jsonSerializerOptions.Encoder,
20+
Indented = jsonSerializerOptions.WriteIndented,
21+
SkipValidation = skipValidation
22+
};
23+
}
24+
}
25+
}

System.Text.Json.Extensions/Utf8JsonReaderExtensions.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,20 @@ public static T GetObject<T>(this ref Utf8JsonReader reader, JsonSerializerOptio
4242
return JsonSerializer.Deserialize<T>(ref reader, options);
4343
}
4444
}
45+
46+
public static T Throw<T>(this ref Utf8JsonReader reader, string message)
47+
{
48+
throw GetException(ref reader, message);
49+
}
50+
51+
public static void Throw(this ref Utf8JsonReader reader, string message)
52+
{
53+
throw GetException(ref reader, message);
54+
}
55+
56+
public static JsonException GetException(this ref Utf8JsonReader reader, string message)
57+
{
58+
return new JsonException(message, null, null, reader.BytesConsumed);
59+
}
4560
}
4661
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System.Text.Json.Serialization;
2+
using System.Text.Json.Serialization.Converters;
3+
4+
namespace System.Text.Json.Extensions
5+
{
6+
public static class Utf8JsonWriterExtensions
7+
{
8+
/// <summary>
9+
/// Writes the object
10+
///
11+
/// Based on <see cref="JsonKeyValuePairConverter{TKey,TValue}.WriteProperty{T}"/>
12+
/// </summary>
13+
public static void WriteObject<T>(this Utf8JsonWriter writer, string propertyName, T value, JsonSerializerOptions options)
14+
{
15+
writer.WritePropertyName(propertyName);
16+
writer.WriteObject(value, options);
17+
}
18+
19+
/// <summary>
20+
/// Writes the object
21+
///
22+
/// Based on <see cref="JsonKeyValuePairConverter{TKey,TValue}.WriteProperty{T}"/>
23+
/// </summary>
24+
public static void WriteObject<T>(this Utf8JsonWriter writer, T value, JsonSerializerOptions options)
25+
{
26+
var typeToConvert = typeof(T);
27+
28+
// Attempt to use existing converter first before re-entering through JsonSerializer.Serialize().
29+
// The default converter for object does not support writing.
30+
if (typeToConvert != typeof(object)
31+
&& options?.GetConverter(typeToConvert) is JsonConverter<T> keyConverter)
32+
{
33+
keyConverter.Write(writer, value, options);
34+
}
35+
else
36+
{
37+
JsonSerializer.Serialize<T>(writer, value, options);
38+
}
39+
}
40+
}
41+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
using System.Collections.Generic;
2+
using System.IO;
3+
using System.Text.Json.Extensions.Converters;
4+
using System.Text.Json.Serialization;
5+
using Shouldly;
6+
using Xunit;
7+
8+
namespace System.Text.Json.ExtensionsTest.Converters
9+
{
10+
public class KeyValuePairCollectionConverterFactoryFixture
11+
{
12+
[Theory]
13+
[InlineData(typeof(List<string>), false)]
14+
[InlineData(typeof(IEnumerable<KeyValuePair<string, string>>), false)]
15+
[InlineData(typeof(KeyValuePair<string, string>), false)]
16+
[InlineData(typeof(List<KeyValuePair<string, string>>), true)]
17+
[InlineData(typeof(List<KeyValuePair<int, string>>), false)]
18+
[InlineData(typeof(List<KeyValuePair<string, int>>), true)]
19+
[InlineData(typeof(Dictionary<string, int>), true)]
20+
[InlineData(typeof(Dictionary<int, string>), false)]
21+
public void CanConvertReportsCorrectly(Type typeToConvert, bool canConvert)
22+
{
23+
var factory = new KeyValuePairCollectionConverterFactory();
24+
factory.CanConvert(typeToConvert)
25+
.ShouldBe(canConvert);
26+
}
27+
28+
[Fact]
29+
public void CanReadToList()
30+
{
31+
var jsonSerializerOptions = new JsonSerializerOptions();
32+
TestHelper.GetReader("{\"Key\": \"Value\"}", jsonSerializerOptions, out var reader);
33+
34+
var factory = new KeyValuePairCollectionConverterFactory();
35+
var jsonConverter = factory
36+
.CreateConverter(typeof(List<KeyValuePair<string, string>>), jsonSerializerOptions)
37+
.ShouldBeAssignableTo<JsonConverter<List<KeyValuePair<string, string>>>>();
38+
39+
var keyValuePair = jsonConverter.Read(ref reader, typeof(List<KeyValuePair<string, string>>),
40+
jsonSerializerOptions)
41+
.ShouldHaveSingleItem();
42+
keyValuePair.ShouldSatisfyAllConditions(
43+
() => keyValuePair.Key.ShouldBe("Key"),
44+
() => keyValuePair.Value.ShouldBe("Value"));
45+
}
46+
47+
[Fact]
48+
public void CanReadToDictionary()
49+
{
50+
var jsonSerializerOptions = new JsonSerializerOptions();
51+
TestHelper.GetReader("{\"Key\": \"Value\"}", jsonSerializerOptions, out var reader);
52+
53+
var factory = new KeyValuePairCollectionConverterFactory();
54+
var jsonConverter = factory
55+
.CreateConverter(typeof(Dictionary<string, string>), jsonSerializerOptions)
56+
.ShouldBeAssignableTo<JsonConverter<Dictionary<string, string>>>();
57+
58+
var keyValuePair = jsonConverter.Read(ref reader, typeof(Dictionary<string, string>),
59+
jsonSerializerOptions)
60+
.ShouldHaveSingleItem();
61+
keyValuePair.ShouldSatisfyAllConditions(
62+
() => keyValuePair.Key.ShouldBe("Key"),
63+
() => keyValuePair.Value.ShouldBe("Value"));
64+
}
65+
66+
[Fact]
67+
public void CanWriteList()
68+
{
69+
var jsonSerializerOptions = new JsonSerializerOptions();
70+
71+
var factory = new KeyValuePairCollectionConverterFactory();
72+
var jsonConverter = factory
73+
.CreateConverter(typeof(List<KeyValuePair<string, string>>), jsonSerializerOptions)
74+
.ShouldBeAssignableTo<JsonConverter<List<KeyValuePair<string, string>>>>();
75+
76+
var json = TestHelper.WithWriter(writer =>
77+
{
78+
jsonConverter.Write(writer, new List<KeyValuePair<string, string>>
79+
{
80+
new KeyValuePair<string, string>("Key", "Value")
81+
},
82+
jsonSerializerOptions);
83+
},
84+
jsonSerializerOptions);
85+
86+
json.ShouldBe("{\"Key\":\"Value\"}");
87+
}
88+
89+
[Fact]
90+
public void CanWriteDictionary()
91+
{
92+
var jsonSerializerOptions = new JsonSerializerOptions();
93+
94+
var factory = new KeyValuePairCollectionConverterFactory();
95+
var jsonConverter = factory
96+
.CreateConverter(typeof(Dictionary<string, string>), jsonSerializerOptions)
97+
.ShouldBeAssignableTo<JsonConverter<Dictionary<string, string>>>();
98+
99+
var json = TestHelper.WithWriter(writer =>
100+
{
101+
jsonConverter.Write(writer, new Dictionary<string, string>
102+
{
103+
{"Key", "Value"}
104+
},
105+
jsonSerializerOptions);
106+
},
107+
jsonSerializerOptions);
108+
109+
json.ShouldBe("{\"Key\":\"Value\"}");
110+
}
111+
}
112+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System.IO;
2+
using System.Text.Json.Extensions;
3+
4+
namespace System.Text.Json.ExtensionsTest
5+
{
6+
public static class TestHelper
7+
{
8+
public static void GetReader(string json, JsonSerializerOptions options, out Utf8JsonReader reader)
9+
{
10+
var jsonReaderOptions = options.GetReaderOptions();
11+
var utf8Bytes = Encoding.UTF8.GetBytes(json);
12+
reader = new Utf8JsonReader(utf8Bytes, jsonReaderOptions);
13+
}
14+
15+
public static string WithWriter(Action<Utf8JsonWriter> action, JsonSerializerOptions options, bool skipValidation = true)
16+
{
17+
using var buffer = new MemoryStream();
18+
using (var writer = new Utf8JsonWriter(buffer, options.GetWriterOptions(skipValidation)))
19+
{
20+
action(writer);
21+
writer.Flush();
22+
}
23+
24+
return Encoding.UTF8.GetString(buffer.GetBuffer().AsSpan(0, (int) buffer.Length));
25+
}
26+
27+
public class DummyObject
28+
{
29+
public string MyValue { get; set; }
30+
}
31+
}
32+
}

System.Text.Json.ExtensionsTest/Utf8JsonReaderExtensionsFixture.cs

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,14 @@ namespace System.Text.Json.ExtensionsTest
66
{
77
public class Utf8JsonReaderExtensionsFixture
88
{
9-
private class DummyObject
10-
{
11-
public string MyValue { get; set; }
12-
}
13-
149
[Fact]
1510
public void ReadObjectReadDummyObject()
1611
{
17-
var jsonReaderOptions = new JsonReaderOptions();
1812
var jsonSerializerOptions = new JsonSerializerOptions();
19-
var utf8Bytes = Encoding.UTF8.GetBytes("{\"MyValue\": \"MyValue\"}");
20-
var reader = new Utf8JsonReader(utf8Bytes, jsonReaderOptions);
13+
TestHelper.GetReader("{\"MyValue\": \"MyValue\"}", jsonSerializerOptions, out var reader);
14+
15+
var dummyObject = reader.ReadObject<TestHelper.DummyObject>(jsonSerializerOptions);
2116

22-
var dummyObject = reader.ReadObject<DummyObject>(jsonSerializerOptions);
23-
2417
dummyObject.MyValue
2518
.ShouldBe("MyValue");
2619
}

0 commit comments

Comments
 (0)