Skip to content

Commit 906b41c

Browse files
committed
Fixed issues with JsonConverter<object>and nullable value-types
1 parent 9cd5f5e commit 906b41c

4 files changed

Lines changed: 106 additions & 17 deletions

File tree

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -274,8 +274,7 @@ public JsonConverter GetConverter(Type typeToConvert)
274274

275275
Type converterTypeToConvert = converter.TypeToConvert;
276276

277-
if (!converterTypeToConvert.IsAssignableFrom(typeToConvert) &&
278-
!typeToConvert.IsAssignableFrom(converterTypeToConvert))
277+
if (!IsCompatibleConverter(converterTypeToConvert, typeToConvert))
279278
{
280279
ThrowHelper.ThrowInvalidOperationException_SerializationConverterNotCompatible(converter.GetType(), typeToConvert);
281280
}
@@ -292,6 +291,36 @@ public JsonConverter GetConverter(Type typeToConvert)
292291
return converter;
293292
}
294293

294+
private static bool IsNullableValueType(Type type)
295+
{
296+
return type.IsGenericType && type.GetGenericTypeDefinition() == s_nullableOfTType;
297+
}
298+
299+
private static bool IsNullableType(Type type)
300+
{
301+
return !type.IsValueType || IsNullableValueType(type);
302+
}
303+
304+
internal static bool IsAssignableFrom(Type type, Type from)
305+
{
306+
// Special case for which Type.IsAssignableFrom returns false
307+
// type: Nullable<T> where T : IInterface
308+
// from: IInterface
309+
310+
if (IsNullableValueType(from) && type.IsInterface)
311+
{
312+
return type.IsAssignableFrom(from.GetGenericArguments()[0]);
313+
}
314+
315+
return type.IsAssignableFrom(from);
316+
}
317+
318+
private static bool IsCompatibleConverter(Type converterTypeToConvert, Type typeToConvert)
319+
{
320+
return IsAssignableFrom(converterTypeToConvert, typeToConvert)
321+
|| IsAssignableFrom(typeToConvert, converterTypeToConvert);
322+
}
323+
295324
private JsonConverter GetConverterFromAttribute(JsonConverterAttribute converterAttribute, Type typeToConvert, Type classTypeAttributeIsOn, MemberInfo? memberInfo)
296325
{
297326
JsonConverter? converter;
@@ -361,9 +390,5 @@ private JsonConverter GetConverterFromAttribute(JsonConverterAttribute converter
361390
return default;
362391
}
363392

364-
private static bool IsNullableType(Type type)
365-
{
366-
return type.IsGenericType && type.GetGenericTypeDefinition() == s_nullableOfTType;
367-
}
368393
}
369394
}

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReflectionEmitMemberAccessor.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ private static DynamicMethod CreatePropertyGetter(PropertyInfo propertyInfo, Typ
231231
Debug.Assert(declaringType != null);
232232

233233
Type declaredPropertyType = propertyInfo.PropertyType;
234-
Debug.Assert(runtimePropertyType.IsAssignableFrom(declaredPropertyType));
234+
Debug.Assert(JsonSerializerOptions.IsAssignableFrom(runtimePropertyType, declaredPropertyType));
235235

236236
DynamicMethod dynamicMethod = CreateGetterMethod(propertyInfo.Name, runtimePropertyType);
237237
ILGenerator generator = dynamicMethod.GetILGenerator();
@@ -274,7 +274,7 @@ private static DynamicMethod CreatePropertySetter(PropertyInfo propertyInfo, Typ
274274
Debug.Assert(declaringType != null);
275275

276276
Type declaredPropertyType = propertyInfo.PropertyType;
277-
Debug.Assert(runtimePropertyType.IsAssignableFrom(declaredPropertyType));
277+
Debug.Assert(JsonSerializerOptions.IsAssignableFrom(runtimePropertyType, declaredPropertyType));
278278

279279
DynamicMethod dynamicMethod = CreateSetterMethod(propertyInfo.Name, runtimePropertyType);
280280
ILGenerator generator = dynamicMethod.GetILGenerator();
@@ -321,7 +321,7 @@ private static DynamicMethod CreateFieldGetter(FieldInfo fieldInfo, Type runtime
321321
Debug.Assert(declaringType != null);
322322

323323
Type declaredFieldType = fieldInfo.FieldType;
324-
Debug.Assert(runtimeFieldType.IsAssignableFrom(declaredFieldType));
324+
Debug.Assert(JsonSerializerOptions.IsAssignableFrom(runtimeFieldType, declaredFieldType));
325325

326326
DynamicMethod dynamicMethod = CreateGetterMethod(fieldInfo.Name, runtimeFieldType);
327327
ILGenerator generator = dynamicMethod.GetILGenerator();
@@ -356,7 +356,7 @@ private static DynamicMethod CreateFieldSetter(FieldInfo fieldInfo, Type runtime
356356
Debug.Assert(declaringType != null);
357357

358358
Type declaredFieldType = fieldInfo.FieldType;
359-
Debug.Assert(runtimeFieldType.IsAssignableFrom(declaredFieldType));
359+
Debug.Assert(JsonSerializerOptions.IsAssignableFrom(runtimeFieldType, declaredFieldType));
360360

361361
DynamicMethod dynamicMethod = CreateSetterMethod(fieldInfo.Name, runtimeFieldType);
362362
ILGenerator generator = dynamicMethod.GetILGenerator();

src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests/CustomConverterTests.Object.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,6 @@ private class ClassWithNullablePrimitives
460460
}
461461

462462
[Fact]
463-
[ActiveIssue("https://github.com/dotnet/runtime/issues/36329")]
464463
public static void ClassWithNullablePrimitivesObjectConverterDeserialize()
465464
{
466465
const string json =

src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests/CustomConverterTests.StructMember.cs

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,14 @@ public override IMemberInterface Read(ref Utf8JsonReader reader, Type typeToConv
2626

2727
string value = reader.GetString();
2828

29-
return new StructMember(value);
29+
return value == null ? null : new StructMember(value);
3030
}
3131

3232
public override void Write(Utf8JsonWriter writer, IMemberInterface value, JsonSerializerOptions options)
3333
{
3434
WriteCallCount++;
3535

36-
JsonSerializer.Serialize(writer, value.Value, typeof(string), options);
36+
JsonSerializer.Serialize<string>(writer, value == null ? null : value.Value, options);
3737
}
3838
}
3939

@@ -46,6 +46,7 @@ private class StructToObjectConverter : JsonConverter<object>
4646

4747
public override bool CanConvert(Type typeToConvert)
4848
{
49+
typeToConvert = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert;
4950
return typeof(IMemberInterface).IsAssignableFrom(typeToConvert);
5051
}
5152

@@ -55,14 +56,14 @@ public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS
5556

5657
string value = reader.GetString();
5758

58-
return new StructMember(value);
59+
return value == null ? null : new StructMember(value);
5960
}
6061

6162
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
6263
{
6364
WriteCallCount++;
6465

65-
JsonSerializer.Serialize(writer, ((IMemberInterface)value).Value, typeof(string), options);
66+
JsonSerializer.Serialize<string>(writer, value == null ? null : ((IMemberInterface)value).Value, options);
6667
}
6768
}
6869

@@ -131,7 +132,6 @@ public static void StructMemberToObjectConverter()
131132
}
132133

133134
[Fact]
134-
[ActiveIssue("https://github.com/dotnet/runtime/issues/36329")]
135135
public static void NullableStructMemberToInterfaceConverter()
136136
{
137137
const string expected = @"{""MyStructProperty"":""StructProperty"",""MyClassProperty"":""ClassProperty"",""MyStructField"":""StructField"",""MyClassField"":""ClassField""}";
@@ -164,7 +164,6 @@ public static void NullableStructMemberToInterfaceConverter()
164164
}
165165

166166
[Fact]
167-
[ActiveIssue("https://github.com/dotnet/runtime/issues/36329")]
168167
public static void NullableStructMemberToObjectConverter()
169168
{
170169
const string expected = @"{""MyStructProperty"":""StructProperty"",""MyClassProperty"":""ClassProperty"",""MyStructField"":""StructField"",""MyClassField"":""ClassField""}";
@@ -196,5 +195,71 @@ public static void NullableStructMemberToObjectConverter()
196195
}
197196
}
198197

198+
[Fact]
199+
public static void NullableStructMemberWithNullsToInterfaceConverter()
200+
{
201+
const string expected = @"{""MyStructProperty"":null,""MyClassProperty"":null,""MyStructField"":null,""MyClassField"":null}";
202+
203+
var converter = new StructToInterfaceConverter();
204+
var options = new JsonSerializerOptions()
205+
{
206+
IncludeFields = true,
207+
};
208+
options.Converters.Add(converter);
209+
210+
string json;
211+
212+
{
213+
var obj = new TestClassWithNullableStructMember();
214+
json = JsonSerializer.Serialize(obj, options);
215+
216+
Assert.Equal(4, converter.WriteCallCount);
217+
Assert.Equal(expected, json);
218+
}
219+
220+
{
221+
var obj = JsonSerializer.Deserialize<TestClassWithNullableStructMember>(json, options);
222+
223+
Assert.Equal(4, converter.ReadCallCount);
224+
Assert.Null(obj.MyStructProperty);
225+
Assert.Null(obj.MyStructField);
226+
Assert.Null(obj.MyClassProperty);
227+
Assert.Null(obj.MyClassField);
228+
}
229+
}
230+
231+
[Fact]
232+
public static void NullableStructMemberWithNullsToObjectConverter()
233+
{
234+
const string expected = @"{""MyStructProperty"":null,""MyClassProperty"":null,""MyStructField"":null,""MyClassField"":null}";
235+
236+
var converter = new StructToObjectConverter();
237+
var options = new JsonSerializerOptions()
238+
{
239+
IncludeFields = true,
240+
};
241+
options.Converters.Add(converter);
242+
243+
string json;
244+
245+
{
246+
var obj = new TestClassWithNullableStructMember();
247+
json = JsonSerializer.Serialize(obj, options);
248+
249+
Assert.Equal(4, converter.WriteCallCount);
250+
Assert.Equal(expected, json);
251+
}
252+
253+
{
254+
var obj = JsonSerializer.Deserialize<TestClassWithNullableStructMember>(json, options);
255+
256+
Assert.Equal(4, converter.ReadCallCount);
257+
Assert.Null(obj.MyStructProperty);
258+
Assert.Null(obj.MyStructField);
259+
Assert.Null(obj.MyClassProperty);
260+
Assert.Null(obj.MyClassField);
261+
}
262+
}
263+
199264
}
200265
}

0 commit comments

Comments
 (0)