Skip to content

Commit b435516

Browse files
committed
JsonTypeConverterAdapterFactory code review.
1 parent af2d7ad commit b435516

File tree

8 files changed

+270
-148
lines changed

8 files changed

+270
-148
lines changed

ClassLibraries/Macross.Json.Extensions/Code/Macross.Json.Extensions.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
System.Text.Json.Serialization.JsonIPAddressConverter
1818
System.Text.Json.Serialization.JsonIPEndPointConverter
1919
System.Text.Json.Serialization.JsonDelegatedStringConverter
20+
System.Text.Json.Serialization.JsonTypeConverterAdapterFactory
2021
System.Net.Http.PushStreamContent
2122
</Description>
2223
<Product>Macross.Json.Extensions</Product>

ClassLibraries/Macross.Json.Extensions/Code/Properties/AssemblyInfo.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@
1010

1111
[assembly: InternalsVisibleTo("Macross.Json.Extensions.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010051b7a480b13cecfa44862449486c6884bd6168c325445a0848f48deca9643657c5ae85df3cf6ffdb24d5bd3e9b71dc074ca602544b83511fbce1f83f1d06bb8b7b564414c9d8c719e4e39b95643dfc8e9ce997b5e2a1542a8ff6379186f87b8b695fee82c506170c4fb8ffcbf2e68f4b5d270083f8909c67916500608ce747e9")]
1212

13-
[assembly: AssemblyVersion("2.1.0.21074")]
13+
[assembly: AssemblyVersion("2.1.0.21080")]
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
using System.ComponentModel;
2+
using System.Linq;
3+
using System.Reflection;
4+
5+
using Macross.Json.Extensions;
6+
7+
namespace System.Text.Json.Serialization
8+
{
9+
/// <summary>
10+
/// <see cref="JsonConverterFactory"/> to convert types to and from strings using <see cref="TypeConverter"/>s. Supports nullable value types.
11+
/// </summary>
12+
public class JsonTypeConverterAdapterFactory : JsonConverterFactory
13+
{
14+
#pragma warning disable CA1062 // Validate arguments of public methods
15+
/// <inheritdoc />
16+
public override bool CanConvert(Type typeToConvert)
17+
{
18+
typeToConvert = ResolveTypeToConvert(typeToConvert).TypeToConvert;
19+
if (typeToConvert.GetCustomAttributes<TypeConverterAttribute>(inherit: true).Any())
20+
{
21+
TypeConverter typeConverter = TypeDescriptor.GetConverter(typeToConvert);
22+
return typeConverter.CanConvertFrom(typeof(string)) && typeConverter.CanConvertTo(typeof(string));
23+
}
24+
return false;
25+
}
26+
27+
/// <inheritdoc />
28+
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
29+
{
30+
(bool IsNullableType, Type TypeToConvert) = ResolveTypeToConvert(typeToConvert);
31+
if (IsNullableType)
32+
{
33+
Type converterType = typeof(NullableTypeConverterAdapter<>).MakeGenericType(TypeToConvert);
34+
return (JsonConverter)Activator.CreateInstance(converterType);
35+
}
36+
else
37+
{
38+
Type converterType = typeof(TypeConverterAdapter<>).MakeGenericType(TypeToConvert);
39+
return (JsonConverter)Activator.CreateInstance(converterType);
40+
}
41+
}
42+
#pragma warning restore CA1062 // Validate arguments of public methods
43+
44+
private static (bool IsNullableType, Type TypeToConvert) ResolveTypeToConvert(Type typeToConvert)
45+
{
46+
if (typeToConvert.IsGenericType)
47+
{
48+
Type? underlyingType = Nullable.GetUnderlyingType(typeToConvert);
49+
if (underlyingType != null)
50+
return (true, underlyingType);
51+
}
52+
return (false, typeToConvert);
53+
}
54+
55+
private class TypeConverterAdapter<T> : JsonConverter<T>
56+
{
57+
private readonly TypeConverter _Converter;
58+
59+
public TypeConverterAdapter()
60+
{
61+
_Converter = TypeDescriptor.GetConverter(typeof(T));
62+
}
63+
64+
/// <inheritdoc/>
65+
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
66+
{
67+
return reader.TokenType != JsonTokenType.String
68+
? throw ThrowHelper.GenerateJsonException_DeserializeUnableToConvertValue(typeof(T))
69+
: (T)_Converter.ConvertFromString(reader.GetString());
70+
}
71+
72+
/// <inheritdoc/>
73+
public override void Write(Utf8JsonWriter writer, T objectToWrite, JsonSerializerOptions options)
74+
=> writer.WriteStringValue(_Converter.ConvertToString(objectToWrite));
75+
}
76+
77+
private class NullableTypeConverterAdapter<T> : JsonConverter<T?>
78+
where T : struct
79+
{
80+
private readonly TypeConverter _Converter;
81+
82+
public NullableTypeConverterAdapter()
83+
{
84+
_Converter = TypeDescriptor.GetConverter(typeof(T));
85+
}
86+
87+
/// <inheritdoc/>
88+
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
89+
{
90+
return reader.TokenType != JsonTokenType.String
91+
? throw ThrowHelper.GenerateJsonException_DeserializeUnableToConvertValue(typeof(T))
92+
: (T)_Converter.ConvertFromString(reader.GetString());
93+
}
94+
95+
/// <inheritdoc/>
96+
public override void Write(Utf8JsonWriter writer, T? objectToWrite, JsonSerializerOptions options)
97+
=> writer.WriteStringValue(_Converter.ConvertToString(objectToWrite));
98+
}
99+
}
100+
}

ClassLibraries/Macross.Json.Extensions/Code/System.Text.Json.Serialization/TypeConverterJsonAdapter.cs

Lines changed: 0 additions & 39 deletions
This file was deleted.

ClassLibraries/Macross.Json.Extensions/Code/System.Text.Json.Serialization/TypeConverterJsonAdapterFactory.cs

Lines changed: 0 additions & 26 deletions
This file was deleted.
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.ComponentModel;
4+
using System.Globalization;
5+
using System.Text.Json;
6+
using System.Text.Json.Serialization;
7+
using System.Text.RegularExpressions;
8+
9+
using Microsoft.VisualStudio.TestTools.UnitTesting;
10+
11+
namespace Macross.Json.Extensions.Tests
12+
{
13+
[TestClass]
14+
public class JsonTypeConverterAdapterFactoryTests
15+
{
16+
[TestMethod]
17+
public void TypeConverterTest()
18+
{
19+
JsonSerializerOptions options = new JsonSerializerOptions();
20+
options.Converters.Add(new JsonTypeConverterAdapterFactory());
21+
22+
TestClass testObj = new TestClass()
23+
{
24+
One = new ReferenceTest { X = 10, Y = "str" },
25+
Many = new List<ReferenceTest>
26+
{
27+
new ReferenceTest { X = 20, Y = "abc" },
28+
new ReferenceTest { X = 30, Y = "zyx" },
29+
}
30+
};
31+
32+
string jsonExpected = "{\"One\":\"10,str\",\"Many\":[\"20,abc\",\"30,zyx\"],\"NonnullableStruct\":\"0,\",\"NullableStruct\":null}";
33+
string json = JsonSerializer.Serialize(testObj, options);
34+
Assert.AreEqual(jsonExpected, json);
35+
36+
TestClass? output = JsonSerializer.Deserialize<TestClass>(json, options);
37+
Assert.IsNotNull(output);
38+
39+
Assert.AreEqual(testObj.One, output.One);
40+
CollectionAssert.AreEqual(testObj.Many, output.Many);
41+
Assert.AreEqual(testObj.NonnullableStruct, output.NonnullableStruct);
42+
Assert.AreEqual(testObj.NullableStruct, output.NullableStruct);
43+
44+
testObj = new TestClass()
45+
{
46+
NonnullableStruct = new StructTest
47+
{
48+
A = 18,
49+
B = "str"
50+
},
51+
NullableStruct = new StructTest
52+
{
53+
A = 18,
54+
B = "str"
55+
}
56+
};
57+
58+
jsonExpected = "{\"One\":null,\"Many\":null,\"NonnullableStruct\":\"18,str\",\"NullableStruct\":\"18,str\"}";
59+
json = JsonSerializer.Serialize(testObj, options);
60+
Assert.AreEqual(jsonExpected, json);
61+
62+
output = JsonSerializer.Deserialize<TestClass>(json, options);
63+
Assert.IsNotNull(output);
64+
Assert.AreEqual(testObj.NonnullableStruct, output.NonnullableStruct);
65+
Assert.AreEqual(testObj.NullableStruct, output.NullableStruct);
66+
}
67+
68+
[TestMethod]
69+
[DataRow(typeof(ReferenceTest))]
70+
[DataRow(typeof(StructTest?))]
71+
[ExpectedException(typeof(JsonException))]
72+
public void InvalidReferenceValueTest(Type typeToConvert)
73+
{
74+
JsonSerializerOptions options = new JsonSerializerOptions();
75+
options.Converters.Add(new JsonTypeConverterAdapterFactory());
76+
77+
JsonSerializer.Deserialize("1234", typeToConvert, options);
78+
}
79+
80+
private class TestClass
81+
{
82+
public ReferenceTest? One { get; set; }
83+
84+
public List<ReferenceTest>? Many { get; set; }
85+
86+
public StructTest NonnullableStruct { get; set; }
87+
88+
public StructTest? NullableStruct { get; set; }
89+
}
90+
91+
[TypeConverter(typeof(ReferenceTypeConverter))]
92+
[JsonConverter(typeof(JsonTypeConverterAdapterFactory))]
93+
private record ReferenceTest
94+
{
95+
public int X { get; set; }
96+
97+
public string? Y { get; set; }
98+
99+
public override string ToString() => $"{X},{Y}";
100+
}
101+
102+
[TypeConverter(typeof(StructTypeConverter))]
103+
private struct StructTest : IEquatable<StructTest>
104+
{
105+
public int A { get; set; }
106+
107+
public string? B { get; set; }
108+
109+
public override bool Equals(object? obj)
110+
=> obj is StructTest structTest && Equals(structTest);
111+
112+
public bool Equals(StructTest other)
113+
=> A == other.A && B == other.B;
114+
115+
public override string ToString() => $"{A},{B}";
116+
117+
public override int GetHashCode()
118+
=> HashCode.Combine(A, B);
119+
}
120+
121+
#pragma warning disable CA1812 // Remove class never instantiated
122+
private class ReferenceTypeConverter : TypeConverter
123+
#pragma warning restore CA1812 // Remove class never instantiated
124+
{
125+
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) =>
126+
sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
127+
128+
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
129+
{
130+
if (value is string str)
131+
{
132+
Match match = Regex.Match(str, @"^(\d+),((?:\w+)|$)");
133+
return new ReferenceTest
134+
{
135+
X = int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture),
136+
Y = string.IsNullOrEmpty(match.Groups[2].Value) ? null : match.Groups[2].Value
137+
};
138+
}
139+
140+
return base.ConvertFrom(context, culture, value);
141+
}
142+
}
143+
144+
#pragma warning disable CA1812 // Remove class never instantiated
145+
private class StructTypeConverter : TypeConverter
146+
#pragma warning restore CA1812 // Remove class never instantiated
147+
{
148+
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) =>
149+
sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
150+
151+
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
152+
{
153+
if (value is string str)
154+
{
155+
Match match = Regex.Match(str, @"^(\d+),((?:\w+)|$)");
156+
return new StructTest
157+
{
158+
A = int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture),
159+
B = string.IsNullOrEmpty(match.Groups[2].Value) ? null : match.Groups[2].Value
160+
};
161+
}
162+
163+
return base.ConvertFrom(context, culture, value);
164+
}
165+
}
166+
}
167+
}

ClassLibraries/Macross.Json.Extensions/Test/Properties/AssemblyInfo.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77

88
[assembly: Guid("c365f59c-0a68-463e-91f0-9d026b568f01")]
99

10-
[assembly: AssemblyVersion("0.0.0.21074")]
10+
[assembly: AssemblyVersion("0.0.0.21080")]

0 commit comments

Comments
 (0)