diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx
index f6b8261f35b76a..4562c9a84eb458 100644
--- a/src/libraries/System.Text.Json/src/Resources/Strings.resx
+++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx
@@ -548,4 +548,10 @@
The object with reference id '{0}' of type '{1}' cannot be assigned to the type '{2}'.
+
+ Unable to cast object of type '{0}' to type '{1}'.
+
+
+ Unable to assign 'null' to the property or field of type '{0}'.
+
diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj
index 9db4b3972418f9..fc5c8ce5591c9f 100644
--- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj
+++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj
@@ -180,6 +180,7 @@
+
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverterFactory.cs
index 5f61b9586951a2..19679337337b90 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverterFactory.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverterFactory.cs
@@ -22,6 +22,12 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer
JsonConverter valueConverter = options.GetConverter(valueTypeToConvert);
Debug.Assert(valueConverter != null);
+ // If the value type has an interface or object converter, just return that converter directly.
+ if (!valueConverter.TypeToConvert.IsValueType && valueTypeToConvert.IsValueType)
+ {
+ return valueConverter;
+ }
+
return CreateValueConverter(valueTypeToConvert, valueConverter);
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs
index 987e897f59eb15..9ce34a858bfe1f 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs
@@ -330,9 +330,10 @@ internal bool TryWrite(Utf8JsonWriter writer, in T value, JsonSerializerOptions
return true;
}
- if (type != TypeToConvert)
+ if (type != TypeToConvert && IsInternalConverter)
{
- // Handle polymorphic case and get the new converter.
+ // For internal converter only: Handle polymorphic case and get the new converter.
+ // Custom converter, even though polymorphic converter, get called for reading AND writing.
JsonConverter jsonConverter = state.Current.InitializeReEntry(type, options);
if (jsonConverter != this)
{
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfT.cs
index 7a0d0155ded87a..ed3da3ac632967 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfT.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfT.cs
@@ -230,6 +230,21 @@ public override bool ReadJsonAndSetMember(object obj, ref ReadStack state, ref U
success = Converter.TryRead(ref reader, RuntimePropertyType!, Options, ref state, out T value);
if (success)
{
+ if (!Converter.IsInternalConverter)
+ {
+ if (value != null)
+ {
+ Type typeOfValue = value.GetType();
+ if (!DeclaredPropertyType.IsAssignableFrom(typeOfValue))
+ {
+ ThrowHelper.ThrowInvalidCastException_DeserializeUnableToAssignValue(typeOfValue, DeclaredPropertyType);
+ }
+ }
+ else if (DeclaredPropertyType.IsValueType && !DeclaredPropertyType.IsNullableValueType())
+ {
+ ThrowHelper.ThrowInvalidOperationException_DeserializeUnableToAssignNull(DeclaredPropertyType);
+ }
+ }
Set!(obj, value!);
}
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs
index 249cf371121377..9a442a2224f7c8 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs
@@ -18,8 +18,6 @@ public sealed partial class JsonSerializerOptions
// The global list of built-in simple converters.
private static readonly Dictionary s_defaultSimpleConverters = GetDefaultSimpleConverters();
- private static readonly Type s_nullableOfTType = typeof(Nullable<>);
-
// The global list of built-in converters that override CanConvert().
private static readonly JsonConverter[] s_defaultFactoryConverters = new JsonConverter[]
{
@@ -186,7 +184,7 @@ internal JsonConverter DetermineConverter(Type? parentClassType, Type runtimePro
// We also throw to avoid passing an invalid argument to setters for nullable struct properties,
// which would cause an InvalidProgramException when the generated IL is invoked.
// This is not an issue of the converter is wrapped in NullableConverter.
- if (IsNullableType(runtimePropertyType) && !IsNullableType(converter.TypeToConvert))
+ if (runtimePropertyType.IsNullableType() && !converter.TypeToConvert.IsNullableType())
{
ThrowHelper.ThrowInvalidOperationException_ConverterCanConvertNullableRedundant(runtimePropertyType, converter);
}
@@ -274,8 +272,8 @@ public JsonConverter GetConverter(Type typeToConvert)
Type converterTypeToConvert = converter.TypeToConvert;
- if (!converterTypeToConvert.IsAssignableFrom(typeToConvert) &&
- !typeToConvert.IsAssignableFrom(converterTypeToConvert))
+ if (!converterTypeToConvert.IsAssignableFromInternal(typeToConvert)
+ && !typeToConvert.IsAssignableFromInternal(converterTypeToConvert))
{
ThrowHelper.ThrowInvalidOperationException_SerializationConverterNotCompatible(converter.GetType(), typeToConvert);
}
@@ -366,9 +364,5 @@ private JsonConverter GetConverterFromAttribute(JsonConverterAttribute converter
return default;
}
- private static bool IsNullableType(Type type)
- {
- return type.IsGenericType && type.GetGenericTypeDefinition() == s_nullableOfTType;
- }
}
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReflectionEmitMemberAccessor.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReflectionEmitMemberAccessor.cs
index 5e7d1e8c094427..042a64a547cb63 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReflectionEmitMemberAccessor.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReflectionEmitMemberAccessor.cs
@@ -219,10 +219,10 @@ private static DynamicMethod CreateImmutableDictionaryCreateRangeDelegate(Type c
return dynamicMethod;
}
- public override Func CreatePropertyGetter(PropertyInfo propertyInfo) =>
- CreateDelegate>(CreatePropertyGetter(propertyInfo, typeof(TProperty)));
+ public override Func CreatePropertyGetter(PropertyInfo propertyInfo) =>
+ CreateDelegate>(CreatePropertyGetter(propertyInfo, typeof(TProperty)));
- private static DynamicMethod CreatePropertyGetter(PropertyInfo propertyInfo, Type propertyType)
+ private static DynamicMethod CreatePropertyGetter(PropertyInfo propertyInfo, Type runtimePropertyType)
{
MethodInfo? realMethod = propertyInfo.GetMethod;
Debug.Assert(realMethod != null);
@@ -230,7 +230,9 @@ private static DynamicMethod CreatePropertyGetter(PropertyInfo propertyInfo, Typ
Type? declaringType = propertyInfo.DeclaringType;
Debug.Assert(declaringType != null);
- DynamicMethod dynamicMethod = CreateGetterMethod(propertyInfo.Name, propertyType);
+ Type declaredPropertyType = propertyInfo.PropertyType;
+
+ DynamicMethod dynamicMethod = CreateGetterMethod(propertyInfo.Name, runtimePropertyType);
ILGenerator generator = dynamicMethod.GetILGenerator();
generator.Emit(OpCodes.Ldarg_0);
@@ -246,15 +248,23 @@ private static DynamicMethod CreatePropertyGetter(PropertyInfo propertyInfo, Typ
generator.Emit(OpCodes.Callvirt, realMethod);
}
+ // declaredPropertyType: Type of the property
+ // runtimePropertyType: of JsonConverter / JsonPropertyInfo
+
+ if (declaredPropertyType != runtimePropertyType && declaredPropertyType.IsValueType)
+ {
+ generator.Emit(OpCodes.Box, declaredPropertyType);
+ }
+
generator.Emit(OpCodes.Ret);
return dynamicMethod;
}
- public override Action CreatePropertySetter(PropertyInfo propertyInfo) =>
- CreateDelegate>(CreatePropertySetter(propertyInfo, typeof(TProperty)));
+ public override Action CreatePropertySetter(PropertyInfo propertyInfo) =>
+ CreateDelegate>(CreatePropertySetter(propertyInfo, typeof(TProperty)));
- private static DynamicMethod CreatePropertySetter(PropertyInfo propertyInfo, Type propertyType)
+ private static DynamicMethod CreatePropertySetter(PropertyInfo propertyInfo, Type runtimePropertyType)
{
MethodInfo? realMethod = propertyInfo.SetMethod;
Debug.Assert(realMethod != null);
@@ -262,24 +272,24 @@ private static DynamicMethod CreatePropertySetter(PropertyInfo propertyInfo, Typ
Type? declaringType = propertyInfo.DeclaringType;
Debug.Assert(declaringType != null);
- DynamicMethod dynamicMethod = CreateSetterMethod(propertyInfo.Name, propertyType);
+ Type declaredPropertyType = propertyInfo.PropertyType;
+
+ DynamicMethod dynamicMethod = CreateSetterMethod(propertyInfo.Name, runtimePropertyType);
ILGenerator generator = dynamicMethod.GetILGenerator();
generator.Emit(OpCodes.Ldarg_0);
+ generator.Emit(declaringType.IsValueType ? OpCodes.Unbox : OpCodes.Castclass, declaringType);
+ generator.Emit(OpCodes.Ldarg_1);
- if (declaringType.IsValueType)
+ // declaredPropertyType: Type of the property
+ // runtimePropertyType: of JsonConverter / JsonPropertyInfo
+
+ if (declaredPropertyType != runtimePropertyType && declaredPropertyType.IsValueType)
{
- generator.Emit(OpCodes.Unbox, declaringType);
- generator.Emit(OpCodes.Ldarg_1);
- generator.Emit(OpCodes.Call, realMethod);
+ generator.Emit(OpCodes.Unbox_Any, declaredPropertyType);
}
- else
- {
- generator.Emit(OpCodes.Castclass, declaringType);
- generator.Emit(OpCodes.Ldarg_1);
- generator.Emit(OpCodes.Callvirt, realMethod);
- };
+ generator.Emit(declaringType.IsValueType ? OpCodes.Call : OpCodes.Callvirt, realMethod);
generator.Emit(OpCodes.Ret);
return dynamicMethod;
@@ -288,12 +298,14 @@ private static DynamicMethod CreatePropertySetter(PropertyInfo propertyInfo, Typ
public override Func CreateFieldGetter(FieldInfo fieldInfo) =>
CreateDelegate>(CreateFieldGetter(fieldInfo, typeof(TProperty)));
- private static DynamicMethod CreateFieldGetter(FieldInfo fieldInfo, Type fieldType)
+ private static DynamicMethod CreateFieldGetter(FieldInfo fieldInfo, Type runtimeFieldType)
{
Type? declaringType = fieldInfo.DeclaringType;
Debug.Assert(declaringType != null);
- DynamicMethod dynamicMethod = CreateGetterMethod(fieldInfo.Name, fieldType);
+ Type declaredFieldType = fieldInfo.FieldType;
+
+ DynamicMethod dynamicMethod = CreateGetterMethod(fieldInfo.Name, runtimeFieldType);
ILGenerator generator = dynamicMethod.GetILGenerator();
generator.Emit(OpCodes.Ldarg_0);
@@ -303,6 +315,15 @@ private static DynamicMethod CreateFieldGetter(FieldInfo fieldInfo, Type fieldTy
: OpCodes.Castclass,
declaringType);
generator.Emit(OpCodes.Ldfld, fieldInfo);
+
+ // declaredFieldType: Type of the field
+ // runtimeFieldType: of JsonConverter / JsonPropertyInfo
+
+ if (declaredFieldType.IsValueType && declaredFieldType != runtimeFieldType)
+ {
+ generator.Emit(OpCodes.Box, declaredFieldType);
+ }
+
generator.Emit(OpCodes.Ret);
return dynamicMethod;
@@ -311,21 +332,28 @@ private static DynamicMethod CreateFieldGetter(FieldInfo fieldInfo, Type fieldTy
public override Action CreateFieldSetter(FieldInfo fieldInfo) =>
CreateDelegate>(CreateFieldSetter(fieldInfo, typeof(TProperty)));
- private static DynamicMethod CreateFieldSetter(FieldInfo fieldInfo, Type fieldType)
+ private static DynamicMethod CreateFieldSetter(FieldInfo fieldInfo, Type runtimeFieldType)
{
Type? declaringType = fieldInfo.DeclaringType;
Debug.Assert(declaringType != null);
- DynamicMethod dynamicMethod = CreateSetterMethod(fieldInfo.Name, fieldType);
+ Type declaredFieldType = fieldInfo.FieldType;
+
+ DynamicMethod dynamicMethod = CreateSetterMethod(fieldInfo.Name, runtimeFieldType);
ILGenerator generator = dynamicMethod.GetILGenerator();
generator.Emit(OpCodes.Ldarg_0);
- generator.Emit(
- declaringType.IsValueType
- ? OpCodes.Unbox
- : OpCodes.Castclass,
- declaringType);
+ generator.Emit(declaringType.IsValueType ? OpCodes.Unbox : OpCodes.Castclass, declaringType);
generator.Emit(OpCodes.Ldarg_1);
+
+ // declaredFieldType: Type of the field
+ // runtimeFieldType: of JsonConverter / JsonPropertyInfo
+
+ if (declaredFieldType != runtimeFieldType && declaredFieldType.IsValueType)
+ {
+ generator.Emit(OpCodes.Unbox_Any, declaredFieldType);
+ }
+
generator.Emit(OpCodes.Stfld, fieldInfo);
generator.Emit(OpCodes.Ret);
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs
index bb0e1566481e36..842b5dc3033158 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs
@@ -49,6 +49,20 @@ public static void ThrowJsonException_DeserializeUnableToConvertValue(Type prope
throw ex;
}
+ [DoesNotReturn]
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static void ThrowInvalidCastException_DeserializeUnableToAssignValue(Type typeOfValue, Type declaredType)
+ {
+ throw new InvalidCastException(SR.Format(SR.DeserializeUnableToAssignValue, typeOfValue, declaredType));
+ }
+
+ [DoesNotReturn]
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static void ThrowInvalidOperationException_DeserializeUnableToAssignNull(Type declaredType)
+ {
+ throw new InvalidOperationException(SR.Format(SR.DeserializeUnableToAssignNull, declaredType));
+ }
+
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void ThrowJsonException_SerializationConverterRead(JsonConverter? converter)
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/TypeExtensions.cs b/src/libraries/System.Text.Json/src/System/Text/Json/TypeExtensions.cs
new file mode 100644
index 00000000000000..8abbf6d6c7b2fd
--- /dev/null
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/TypeExtensions.cs
@@ -0,0 +1,42 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+namespace System.Text.Json
+{
+ internal static class TypeExtensions
+ {
+ ///
+ /// Returns when the given type is of type .
+ ///
+ public static bool IsNullableValueType(this Type type)
+ {
+ return Nullable.GetUnderlyingType(type) != null;
+ }
+
+ ///
+ /// Returns when the given type is either a reference type or of type .
+ ///
+ public static bool IsNullableType(this Type type)
+ {
+ return !type.IsValueType || IsNullableValueType(type);
+ }
+
+ ///
+ /// Returns when the given type is assignable from .
+ ///
+ ///
+ /// Other than also returns when is of type where : and is of type .
+ ///
+ public static bool IsAssignableFromInternal(this Type type, Type from)
+ {
+ if (IsNullableValueType(from) && type.IsInterface)
+ {
+ return type.IsAssignableFrom(from.GetGenericArguments()[0]);
+ }
+
+ return type.IsAssignableFrom(from);
+ }
+ }
+}
diff --git a/src/libraries/System.Text.Json/tests/AssertHelper.cs b/src/libraries/System.Text.Json/tests/AssertHelper.cs
new file mode 100644
index 00000000000000..0821027726a8aa
--- /dev/null
+++ b/src/libraries/System.Text.Json/tests/AssertHelper.cs
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using Xunit;
+
+namespace System.Text.Json.Tests
+{
+ public static class AssertHelper
+ {
+ public static void ValidateJson(IEnumerable expectedProperties, string json)
+ {
+ Assert.StartsWith("{", json);
+ Assert.EndsWith("}", json);
+ foreach (string expectedProperty in expectedProperties)
+ Assert.Contains(expectedProperty, json);
+ }
+
+ }
+}
diff --git a/src/libraries/System.Text.Json/tests/JsonPropertyTests.cs b/src/libraries/System.Text.Json/tests/JsonPropertyTests.cs
index 4d48938ebe39e0..8193c928342f71 100644
--- a/src/libraries/System.Text.Json/tests/JsonPropertyTests.cs
+++ b/src/libraries/System.Text.Json/tests/JsonPropertyTests.cs
@@ -97,7 +97,7 @@ private static void AssertContents(string expectedValue, ArrayBufferWriter
[InlineData(null)]
public static void NameEquals_InvalidInstance_Throws(string text)
{
- const string ErrorMessage = "Operation is not valid due to the current state of the object.";
+ string ErrorMessage = new InvalidOperationException().Message;
JsonProperty prop = default;
AssertExtensions.Throws(() => prop.NameEquals(text), ErrorMessage);
AssertExtensions.Throws(() => prop.NameEquals(text.AsSpan()), ErrorMessage);
diff --git a/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests/CustomConverterTests.InvalidCast.cs b/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests/CustomConverterTests.InvalidCast.cs
new file mode 100644
index 00000000000000..8013edbfd845d2
--- /dev/null
+++ b/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests/CustomConverterTests.InvalidCast.cs
@@ -0,0 +1,226 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Xunit;
+
+namespace System.Text.Json.Serialization.Tests
+{
+ public static partial class CustomConverterTests
+ {
+ [Fact]
+ public static void InvalidCastRefTypedPropertyFails()
+ {
+ var obj = new ObjectWrapperWithProperty
+ {
+ Object = new WrittenObject
+ {
+ Int = 123,
+ String = "Hello",
+ }
+ };
+
+ var json = JsonSerializer.Serialize(obj);
+
+ var ex = Assert.Throws(() => JsonSerializer.Deserialize(json));
+ }
+
+ [Fact]
+ public static void InvalidCastRefTypedFieldFails()
+ {
+ var options = new JsonSerializerOptions { IncludeFields = true };
+ var obj = new ObjectWrapperWithField
+ {
+ Object = new WrittenObject
+ {
+ Int = 123,
+ String = "Hello",
+ }
+ };
+
+ var json = JsonSerializer.Serialize(obj);
+
+ var ex = Assert.Throws(() => JsonSerializer.Deserialize(json, options));
+ }
+
+ ///
+ /// A converter that intentionally deserialize a completely unrelated typed object.
+ ///
+ public class InvalidCastConverter : JsonConverter
+ {
+ public override bool CanConvert(Type typeToConvert)
+ => true;
+
+ public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ JsonSerializer.Deserialize(ref reader, options);
+ return new ReadObject { Double = Math.PI };
+ }
+
+ public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
+ {
+ JsonSerializer.Serialize(writer, (WrittenObject)value, options);
+ }
+ }
+
+ private class ObjectWrapperWithProperty
+ {
+ [JsonConverter(typeof(InvalidCastConverter))]
+ public WrittenObject Object { get; set; }
+ }
+
+ private class ObjectWrapperWithField
+ {
+ [JsonConverter(typeof(InvalidCastConverter))]
+ public WrittenObject Object { get; set; }
+ }
+
+ private class WrittenObject
+ {
+ public string String { get; set; }
+ public int Int { get; set; }
+ }
+
+ private class ReadObject
+ {
+ public double Double { get; set; }
+ }
+
+ [Fact]
+ public static void CastDerivedWorks()
+ {
+ var options = new JsonSerializerOptions { IncludeFields = true };
+ var obj = JsonSerializer.Deserialize(@"{""DerivedProperty"":"""",""DerivedField"":""""}", options);
+
+ Assert.IsType(obj.DerivedField);
+ Assert.IsType(obj.DerivedProperty);
+ }
+
+ [Fact]
+ public static void CastBaseWorks()
+ {
+ var options = new JsonSerializerOptions { IncludeFields = true };
+ var obj = JsonSerializer.Deserialize(@"{""BaseProperty"":"""",""BaseField"":""""}", options);
+
+ Assert.IsType(obj.BaseField);
+ Assert.IsType(obj.BaseProperty);
+ }
+
+ [Fact]
+ public static void CastBasePropertyFails()
+ {
+ var options = new JsonSerializerOptions { IncludeFields = true };
+ var ex = Assert.Throws(() => JsonSerializer.Deserialize(@"{""DerivedProperty"":""""}", options));
+ }
+
+ [Fact]
+ public static void CastBaseFieldFails()
+ {
+ var options = new JsonSerializerOptions { IncludeFields = true };
+ var ex = Assert.Throws(() => JsonSerializer.Deserialize(@"{""DerivedField"":""""}", options));
+ }
+
+ ///
+ /// A converter that deserializes an object of an derived class.
+ ///
+ private class BaseConverter : JsonConverter
+ {
+ public override bool CanConvert(Type typeToConvert)
+ => true;
+
+ public override Base Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ reader.GetString();
+ return new Derived() { String = "Hello", Double = Math.PI };
+ }
+
+ public override void Write(Utf8JsonWriter writer, Base value, JsonSerializerOptions options)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ ///
+ /// A converter that deserializes an object of an derived class.
+ ///
+ private class DerivedConverter : JsonConverter
+ {
+ public override bool CanConvert(Type typeToConvert)
+ => true;
+
+ public override Derived Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ reader.GetString();
+ return new Derived() { String = "Hello", Double = Math.PI };
+ }
+
+ public override void Write(Utf8JsonWriter writer, Derived value, JsonSerializerOptions options)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ ///
+ /// A converter that deserializes an object of the base class where the wrapper expects an derived object.
+ ///
+ private class InvalidBaseConverter : JsonConverter
+ {
+ public override bool CanConvert(Type typeToConvert)
+ => true;
+
+ public override Base Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ reader.GetString();
+ return new Base() { String = "Hello" };
+ }
+
+ public override void Write(Utf8JsonWriter writer, Base value, JsonSerializerOptions options)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private class Base
+ {
+ public string String;
+ }
+
+ private class Derived : Base
+ {
+ public double Double;
+ }
+
+ private class ObjectWrapperDerived
+ {
+ [JsonConverter(typeof(BaseConverter))]
+ public Derived DerivedProperty { get; set; }
+ [JsonConverter(typeof(BaseConverter))]
+#pragma warning disable 0649
+ public Derived DerivedField;
+#pragma warning restore
+ }
+
+ private class ObjectWrapperDerivedWithProperty
+ {
+ [JsonConverter(typeof(InvalidBaseConverter))]
+ public Derived DerivedProperty { get; set; }
+ }
+
+ private class ObjectWrapperDerivedWithField
+ {
+ [JsonConverter(typeof(InvalidBaseConverter))]
+#pragma warning disable 0649
+ public Derived DerivedField;
+#pragma warning restore
+ }
+
+ private class ObjectWrapperBase
+ {
+ [JsonConverter(typeof(DerivedConverter))]
+ public Base BaseProperty { get; set; }
+ [JsonConverter(typeof(DerivedConverter))]
+#pragma warning disable 0649
+ public Base BaseField;
+#pragma warning restore
+ }
+ }
+}
diff --git a/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests/CustomConverterTests.Object.cs b/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests/CustomConverterTests.Object.cs
index bdb1c6be82ff8c..1ecdfebc935ada 100644
--- a/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests/CustomConverterTests.Object.cs
+++ b/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests/CustomConverterTests.Object.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Text.Json.Tests;
using Xunit;
namespace System.Text.Json.Serialization.Tests
@@ -311,6 +312,199 @@ public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOp
}
}
+ private class PrimitiveConverter : JsonConverter
+ {
+ public int ReadCallCount { get; private set; }
+ public int WriteCallCount { get; private set; }
+
+ public override bool CanConvert(Type typeToConvert)
+ => typeToConvert != typeof(ClassWithPrimitives)
+ && typeToConvert != typeof(ClassWithNullablePrimitives);
+
+ public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ ReadCallCount++;
+
+ if (reader.TokenType == JsonTokenType.True)
+ {
+ return true;
+ }
+
+ if (reader.TokenType == JsonTokenType.False)
+ {
+ return false;
+ }
+
+ if (reader.TokenType == JsonTokenType.Number)
+ {
+ if (reader.TryGetInt32(out int i))
+ {
+ return i;
+ }
+
+ return reader.GetDouble();
+ }
+
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ if (reader.TryGetDateTime(out DateTime datetime))
+ {
+ return datetime;
+ }
+
+ return reader.GetString();
+ }
+
+ throw new JsonException();
+ }
+
+ public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
+ {
+ WriteCallCount++;
+
+ if (value is int i)
+ {
+ writer.WriteNumberValue(i);
+ }
+ else if (value is bool b)
+ {
+ writer.WriteBooleanValue(b);
+ }
+ else if (value is string s)
+ {
+ writer.WriteStringValue(s);
+ }
+ else
+ {
+ throw new NotSupportedException();
+ }
+ }
+ }
+
+ private class ClassWithPrimitives
+ {
+ public int MyIntProperty { get; set; }
+ public bool MyBoolProperty { get; set; }
+ public string MyStringProperty { get; set; }
+#pragma warning disable 0649
+ public int MyIntField;
+ public bool MyBoolField;
+ public string MyStringField;
+#pragma warning restore
+ }
+
+ [Fact]
+ public static void ClassWithPrimitivesObjectConverter()
+ {
+ string[] expected = new[]
+ {
+ @"""MyIntProperty"":123",
+ @"""MyBoolProperty"":true",
+ @"""MyStringProperty"":""Hello""",
+ @"""MyIntField"":321",
+ @"""MyBoolField"":true",
+ @"""MyStringField"":""World"""
+ };
+
+ string json;
+ var converter = new PrimitiveConverter();
+ var options = new JsonSerializerOptions
+ {
+ IncludeFields = true
+ };
+ options.Converters.Add(converter);
+
+ {
+ var obj = new ClassWithPrimitives
+ {
+ MyIntProperty = 123,
+ MyBoolProperty = true,
+ MyStringProperty = "Hello",
+ MyIntField = 321,
+ MyBoolField = true,
+ MyStringField = "World",
+ };
+
+ json = JsonSerializer.Serialize(obj, options);
+
+ Assert.Equal(6, converter.WriteCallCount);
+ AssertHelper.ValidateJson(expected, json);
+ }
+ {
+ var obj = JsonSerializer.Deserialize(json, options);
+
+ Assert.Equal(6, converter.ReadCallCount);
+
+ Assert.Equal(123, obj.MyIntProperty);
+ Assert.True(obj.MyBoolProperty);
+ Assert.Equal("Hello", obj.MyStringProperty);
+ Assert.Equal(321, obj.MyIntField);
+ Assert.True(obj.MyBoolField);
+ Assert.Equal("World", obj.MyStringField);
+ }
+ }
+
+ private class ClassWithNullablePrimitives
+ {
+ public int? MyIntProperty { get; set; }
+ public bool? MyBoolProperty { get; set; }
+ public string MyStringProperty { get; set; }
+#pragma warning disable 0649
+ public int? MyIntField;
+ public bool? MyBoolField;
+ public string MyStringField;
+#pragma warning restore
+ }
+
+ [Fact]
+ public static void ClassWithNullablePrimitivesObjectConverter()
+ {
+ string[] expected = new[]
+ {
+ @"""MyIntProperty"":123",
+ @"""MyBoolProperty"":true",
+ @"""MyStringProperty"":""Hello""",
+ @"""MyIntField"":321",
+ @"""MyBoolField"":true",
+ @"""MyStringField"":""World"""
+ };
+
+ string json;
+ var converter = new PrimitiveConverter();
+ var options = new JsonSerializerOptions
+ {
+ IncludeFields = true
+ };
+ options.Converters.Add(converter);
+
+ {
+ var obj = new ClassWithNullablePrimitives
+ {
+ MyIntProperty = 123,
+ MyBoolProperty = true,
+ MyStringProperty = "Hello",
+ MyIntField = 321,
+ MyBoolField = true,
+ MyStringField = "World",
+ };
+
+ json = JsonSerializer.Serialize(obj, options);
+
+ Assert.Equal(6, converter.WriteCallCount);
+ AssertHelper.ValidateJson(expected, json);
+ }
+ {
+ var obj = JsonSerializer.Deserialize(json, options);
+
+ Assert.Equal(123, obj.MyIntProperty);
+ Assert.True(obj.MyBoolProperty);
+ Assert.Equal("Hello", obj.MyStringProperty);
+ Assert.Equal(321, obj.MyIntField);
+ Assert.True(obj.MyBoolField);
+ Assert.Equal("World", obj.MyStringField);
+ }
+ }
+
[Fact]
public static void SystemObjectNewtonsoftCompatibleConverterDeserialize()
{
diff --git a/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests/CustomConverterTests.ValueTypedMember.cs b/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests/CustomConverterTests.ValueTypedMember.cs
new file mode 100644
index 00000000000000..a4fa41de979f79
--- /dev/null
+++ b/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests/CustomConverterTests.ValueTypedMember.cs
@@ -0,0 +1,385 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Xunit;
+
+namespace System.Text.Json.Serialization.Tests
+{
+ public static partial class CustomConverterTests
+ {
+ private class ValueTypeToInterfaceConverter : JsonConverter
+ {
+ public int ReadCallCount { get; private set; }
+ public int WriteCallCount { get; private set; }
+
+ public override bool HandleNull => true;
+
+ public override bool CanConvert(Type typeToConvert)
+ {
+ return typeof(IMemberInterface).IsAssignableFrom(typeToConvert);
+ }
+
+ public override IMemberInterface Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ ReadCallCount++;
+
+ string value = reader.GetString();
+
+ if (value == null)
+ {
+ return null;
+ }
+
+ if (value.IndexOf("ValueTyped", StringComparison.Ordinal) >= 0)
+ {
+ return new ValueTypedMember(value);
+ }
+ if (value.IndexOf("RefTyped", StringComparison.Ordinal) >= 0)
+ {
+ return new RefTypedMember(value);
+ }
+ if (value.IndexOf("OtherVT", StringComparison.Ordinal) >= 0)
+ {
+ return new OtherVTMember(value);
+ }
+ if (value.IndexOf("OtherRT", StringComparison.Ordinal) >= 0)
+ {
+ return new OtherRTMember(value);
+ }
+ throw new JsonException();
+ }
+
+ public override void Write(Utf8JsonWriter writer, IMemberInterface value, JsonSerializerOptions options)
+ {
+ WriteCallCount++;
+
+ JsonSerializer.Serialize(writer, value == null ? null : value.Value, options);
+ }
+ }
+
+ private class ValueTypeToObjectConverter : JsonConverter
+ {
+ public int ReadCallCount { get; private set; }
+ public int WriteCallCount { get; private set; }
+
+ public override bool HandleNull => true;
+
+ public override bool CanConvert(Type typeToConvert)
+ {
+ return typeof(IMemberInterface).IsAssignableFrom(typeToConvert);
+ }
+
+ public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ ReadCallCount++;
+
+ string value = reader.GetString();
+
+ if (value == null)
+ {
+ return null;
+ }
+
+ if (value.IndexOf("ValueTyped", StringComparison.Ordinal) >= 0)
+ {
+ return new ValueTypedMember(value);
+ }
+ if (value.IndexOf("RefTyped", StringComparison.Ordinal) >= 0)
+ {
+ return new RefTypedMember(value);
+ }
+ if (value.IndexOf("OtherVT", StringComparison.Ordinal) >= 0)
+ {
+ return new OtherVTMember(value);
+ }
+ if (value.IndexOf("OtherRT", StringComparison.Ordinal) >= 0)
+ {
+ return new OtherRTMember(value);
+ }
+ throw new JsonException();
+ }
+
+ public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
+ {
+ WriteCallCount++;
+
+ JsonSerializer.Serialize(writer, value == null ? null : ((IMemberInterface)value).Value, options);
+ }
+ }
+
+ [Fact]
+ public static void AssignmentToValueTypedMemberInterface()
+ {
+ var converter = new ValueTypeToInterfaceConverter();
+ var options = new JsonSerializerOptions { IncludeFields = true };
+ options.Converters.Add(converter);
+
+ Exception ex;
+ // Invalid cast OtherVTMember
+ ex = Assert.Throws(() => JsonSerializer.Deserialize(@"{""MyValueTypedProperty"":""OtherVTProperty""}", options));
+ ex = Assert.Throws(() => JsonSerializer.Deserialize(@"{""MyValueTypedField"":""OtherVTField""}", options));
+ // Invalid cast OtherRTMember
+ ex = Assert.Throws(() => JsonSerializer.Deserialize(@"{""MyValueTypedProperty"":""OtherRTProperty""}", options));
+ ex = Assert.Throws(() => JsonSerializer.Deserialize(@"{""MyValueTypedField"":""OtherRTField""}", options));
+ // Invalid null
+ ex = Assert.Throws(() => JsonSerializer.Deserialize(@"{""MyValueTypedProperty"":null}", options));
+ ex = Assert.Throws(() => JsonSerializer.Deserialize(@"{""MyValueTypedField"":null}", options));
+ }
+
+ [Fact]
+ public static void AssignmentToValueTypedMemberObject()
+ {
+ var converter = new ValueTypeToObjectConverter();
+ var options = new JsonSerializerOptions { IncludeFields = true };
+ options.Converters.Add(converter);
+
+ Exception ex;
+ // Invalid cast OtherVTMember
+ ex = Assert.Throws(() => JsonSerializer.Deserialize(@"{""MyValueTypedProperty"":""OtherVTProperty""}", options));
+ ex = Assert.Throws(() => JsonSerializer.Deserialize(@"{""MyValueTypedField"":""OtherVTField""}", options));
+ // Invalid cast OtherRTMember
+ ex = Assert.Throws(() => JsonSerializer.Deserialize(@"{""MyValueTypedProperty"":""OtherRTProperty""}", options));
+ ex = Assert.Throws(() => JsonSerializer.Deserialize(@"{""MyValueTypedField"":""OtherRTField""}", options));
+ // Invalid null
+ ex = Assert.Throws(() => JsonSerializer.Deserialize(@"{""MyValueTypedProperty"":null}", options));
+ ex = Assert.Throws(() => JsonSerializer.Deserialize(@"{""MyValueTypedField"":null}", options));
+ }
+
+ [Fact]
+ public static void AssignmentToNullableValueTypedMemberInterface()
+ {
+ var converter = new ValueTypeToInterfaceConverter();
+ var options = new JsonSerializerOptions { IncludeFields = true };
+ options.Converters.Add(converter);
+
+ TestClassWithNullableValueTypedMember obj;
+ Exception ex;
+ // Invalid cast OtherVTMember
+ ex = Assert.Throws(() => JsonSerializer.Deserialize(@"{""MyValueTypedProperty"":""OtherVTProperty""}", options));
+ ex = Assert.Throws(() => JsonSerializer.Deserialize(@"{""MyValueTypedField"":""OtherVTField""}", options));
+ // Invalid cast OtherRTMember
+ ex = Assert.Throws(() => JsonSerializer.Deserialize(@"{""MyValueTypedProperty"":""OtherRTProperty""}", options));
+ ex = Assert.Throws(() => JsonSerializer.Deserialize(@"{""MyValueTypedField"":""OtherRTField""}", options));
+ // Valid null
+ obj = JsonSerializer.Deserialize(@"{""MyValueTypedProperty"":null,""MyValueTypedField"":null}", options);
+ Assert.Null(obj.MyValueTypedProperty);
+ Assert.Null(obj.MyValueTypedField);
+ }
+
+ [Fact]
+ public static void AssignmentToNullableValueTypedMemberObject()
+ {
+ var converter = new ValueTypeToObjectConverter();
+ var options = new JsonSerializerOptions { IncludeFields = true };
+ options.Converters.Add(converter);
+
+ TestClassWithNullableValueTypedMember obj;
+ Exception ex;
+ // Invalid cast OtherVTMember
+ ex = Assert.Throws(() => JsonSerializer.Deserialize(@"{""MyValueTypedProperty"":""OtherVTProperty""}", options));
+ ex = Assert.Throws(() => JsonSerializer.Deserialize(@"{""MyValueTypedField"":""OtherVTField""}", options));
+ // Invalid cast OtherRTMember
+ ex = Assert.Throws(() => JsonSerializer.Deserialize(@"{""MyValueTypedProperty"":""OtherRTProperty""}", options));
+ ex = Assert.Throws(() => JsonSerializer.Deserialize(@"{""MyValueTypedField"":""OtherRTField""}", options));
+ // Valid null
+ obj = JsonSerializer.Deserialize(@"{""MyValueTypedProperty"":null,""MyValueTypedField"":null}", options);
+ Assert.Null(obj.MyValueTypedProperty);
+ Assert.Null(obj.MyValueTypedField);
+ }
+
+ [Fact]
+ public static void ValueTypedMemberToInterfaceConverter()
+ {
+ const string expected = @"{""MyValueTypedProperty"":""ValueTypedProperty"",""MyRefTypedProperty"":""RefTypedProperty"",""MyValueTypedField"":""ValueTypedField"",""MyRefTypedField"":""RefTypedField""}";
+
+ var converter = new ValueTypeToInterfaceConverter();
+ var options = new JsonSerializerOptions()
+ {
+ IncludeFields = true,
+ };
+ options.Converters.Add(converter);
+
+ string json;
+
+ {
+ var obj = new TestClassWithValueTypedMember();
+ obj.Initialize();
+ obj.Verify();
+ json = JsonSerializer.Serialize(obj, options);
+
+ Assert.Equal(4, converter.WriteCallCount);
+ Assert.Equal(expected, json);
+ }
+
+ {
+ var obj = JsonSerializer.Deserialize(json, options);
+ obj.Verify();
+
+ Assert.Equal(4, converter.ReadCallCount);
+ }
+ }
+
+ [Fact]
+ public static void ValueTypedMemberToObjectConverter()
+ {
+ const string expected = @"{""MyValueTypedProperty"":""ValueTypedProperty"",""MyRefTypedProperty"":""RefTypedProperty"",""MyValueTypedField"":""ValueTypedField"",""MyRefTypedField"":""RefTypedField""}";
+
+ var converter = new ValueTypeToObjectConverter();
+ var options = new JsonSerializerOptions()
+ {
+ IncludeFields = true,
+ };
+ options.Converters.Add(converter);
+
+ string json;
+
+ {
+ var obj = new TestClassWithValueTypedMember();
+ obj.Initialize();
+ obj.Verify();
+ json = JsonSerializer.Serialize(obj, options);
+
+ Assert.Equal(4, converter.WriteCallCount);
+ Assert.Equal(expected, json);
+ }
+
+ {
+ var obj = JsonSerializer.Deserialize(json, options);
+ obj.Verify();
+
+ Assert.Equal(4, converter.ReadCallCount);
+ }
+ }
+
+ [Fact]
+ public static void NullableValueTypedMemberToInterfaceConverter()
+ {
+ const string expected = @"{""MyValueTypedProperty"":""ValueTypedProperty"",""MyRefTypedProperty"":""RefTypedProperty"",""MyValueTypedField"":""ValueTypedField"",""MyRefTypedField"":""RefTypedField""}";
+
+ var converter = new ValueTypeToInterfaceConverter();
+ var options = new JsonSerializerOptions()
+ {
+ IncludeFields = true,
+ };
+ options.Converters.Add(converter);
+
+ string json;
+
+ {
+ var obj = new TestClassWithNullableValueTypedMember();
+ obj.Initialize();
+ obj.Verify();
+ json = JsonSerializer.Serialize(obj, options);
+
+ Assert.Equal(4, converter.WriteCallCount);
+ Assert.Equal(expected, json);
+ }
+
+ {
+ var obj = JsonSerializer.Deserialize(json, options);
+ obj.Verify();
+
+ Assert.Equal(4, converter.ReadCallCount);
+ }
+ }
+
+ [Fact]
+ public static void NullableValueTypedMemberToObjectConverter()
+ {
+ const string expected = @"{""MyValueTypedProperty"":""ValueTypedProperty"",""MyRefTypedProperty"":""RefTypedProperty"",""MyValueTypedField"":""ValueTypedField"",""MyRefTypedField"":""RefTypedField""}";
+
+ var converter = new ValueTypeToObjectConverter();
+ var options = new JsonSerializerOptions()
+ {
+ IncludeFields = true,
+ };
+ options.Converters.Add(converter);
+
+ string json;
+
+ {
+ var obj = new TestClassWithNullableValueTypedMember();
+ obj.Initialize();
+ obj.Verify();
+ json = JsonSerializer.Serialize(obj, options);
+
+ Assert.Equal(4, converter.WriteCallCount);
+ Assert.Equal(expected, json);
+ }
+
+ {
+ var obj = JsonSerializer.Deserialize(json, options);
+ obj.Verify();
+
+ Assert.Equal(4, converter.ReadCallCount);
+ }
+ }
+
+ [Fact]
+ public static void NullableValueTypedMemberWithNullsToInterfaceConverter()
+ {
+ const string expected = @"{""MyValueTypedProperty"":null,""MyRefTypedProperty"":null,""MyValueTypedField"":null,""MyRefTypedField"":null}";
+
+ var converter = new ValueTypeToInterfaceConverter();
+ var options = new JsonSerializerOptions()
+ {
+ IncludeFields = true,
+ };
+ options.Converters.Add(converter);
+
+ string json;
+
+ {
+ var obj = new TestClassWithNullableValueTypedMember();
+ json = JsonSerializer.Serialize(obj, options);
+
+ Assert.Equal(4, converter.WriteCallCount);
+ Assert.Equal(expected, json);
+ }
+
+ {
+ var obj = JsonSerializer.Deserialize(json, options);
+
+ Assert.Equal(4, converter.ReadCallCount);
+ Assert.Null(obj.MyValueTypedProperty);
+ Assert.Null(obj.MyValueTypedField);
+ Assert.Null(obj.MyRefTypedProperty);
+ Assert.Null(obj.MyRefTypedField);
+ }
+ }
+
+ [Fact]
+ public static void NullableValueTypedMemberWithNullsToObjectConverter()
+ {
+ const string expected = @"{""MyValueTypedProperty"":null,""MyRefTypedProperty"":null,""MyValueTypedField"":null,""MyRefTypedField"":null}";
+
+ var converter = new ValueTypeToObjectConverter();
+ var options = new JsonSerializerOptions()
+ {
+ IncludeFields = true,
+ };
+ options.Converters.Add(converter);
+
+ string json;
+
+ {
+ var obj = new TestClassWithNullableValueTypedMember();
+ json = JsonSerializer.Serialize(obj, options);
+
+ Assert.Equal(4, converter.WriteCallCount);
+ Assert.Equal(expected, json);
+ }
+
+ {
+ var obj = JsonSerializer.Deserialize(json, options);
+
+ Assert.Equal(4, converter.ReadCallCount);
+ Assert.Null(obj.MyValueTypedProperty);
+ Assert.Null(obj.MyValueTypedField);
+ Assert.Null(obj.MyRefTypedProperty);
+ Assert.Null(obj.MyRefTypedField);
+ }
+ }
+
+ }
+}
diff --git a/src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs b/src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs
index a005ec38e6ef04..b2c6baa772d749 100644
--- a/src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs
+++ b/src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs
@@ -1077,8 +1077,9 @@ public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
{
- // Since we are converter for object, the string converter will be called instead of this.
- throw new InvalidOperationException();
+ // Since we are in a user-provided (not internal to S.T.Json) object converter,
+ // this converter will be called, not the internal string converter.
+ writer.WriteStringValue((string)value);
}
}
diff --git a/src/libraries/System.Text.Json/tests/Serialization/TestClasses/TestClasses.ValueTypedMember.cs b/src/libraries/System.Text.Json/tests/Serialization/TestClasses/TestClasses.ValueTypedMember.cs
new file mode 100644
index 00000000000000..ab7384afce90e1
--- /dev/null
+++ b/src/libraries/System.Text.Json/tests/Serialization/TestClasses/TestClasses.ValueTypedMember.cs
@@ -0,0 +1,109 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Linq;
+using Xunit;
+
+namespace System.Text.Json.Serialization.Tests
+{
+ public class TestClassWithValueTypedMember : ITestClass
+ {
+ public ValueTypedMember MyValueTypedProperty { get; set; }
+
+ public ValueTypedMember MyValueTypedField;
+
+ public RefTypedMember MyRefTypedProperty { get; set; }
+
+ public RefTypedMember MyRefTypedField;
+
+ public void Initialize()
+ {
+ MyValueTypedProperty = new ValueTypedMember("ValueTypedProperty");
+ MyValueTypedField = new ValueTypedMember("ValueTypedField");
+ MyRefTypedProperty = new RefTypedMember("RefTypedProperty");
+ MyRefTypedField = new RefTypedMember("RefTypedField");
+ }
+
+ public void Verify()
+ {
+ Assert.Equal("ValueTypedProperty", MyValueTypedProperty.Value);
+ Assert.Equal("ValueTypedField", MyValueTypedField.Value);
+ Assert.Equal("RefTypedProperty", MyRefTypedProperty.Value);
+ Assert.Equal("RefTypedField", MyRefTypedField.Value);
+ }
+ }
+
+ public class TestClassWithNullableValueTypedMember : ITestClass
+ {
+ public ValueTypedMember? MyValueTypedProperty { get; set; }
+
+ public ValueTypedMember? MyValueTypedField;
+
+ public RefTypedMember MyRefTypedProperty { get; set; }
+
+ public RefTypedMember MyRefTypedField;
+
+ public void Initialize()
+ {
+ MyValueTypedProperty = new ValueTypedMember("ValueTypedProperty");
+ MyValueTypedField = new ValueTypedMember("ValueTypedField");
+ MyRefTypedProperty = new RefTypedMember("RefTypedProperty");
+ MyRefTypedField = new RefTypedMember("RefTypedField");
+ }
+
+ public void Verify()
+ {
+ Assert.Equal("ValueTypedProperty", MyValueTypedProperty.Value.Value);
+ Assert.Equal("ValueTypedField", MyValueTypedField.Value.Value);
+ Assert.Equal("RefTypedProperty", MyRefTypedProperty.Value);
+ Assert.Equal("RefTypedField", MyRefTypedField.Value);
+ }
+ }
+
+ public interface IMemberInterface
+ {
+ string Value { get; }
+ }
+
+ public struct ValueTypedMember : IMemberInterface
+ {
+ public string Value { get; }
+
+ public ValueTypedMember(string value)
+ {
+ Value = value;
+ }
+ }
+
+ public struct OtherVTMember : IMemberInterface
+ {
+ public string Value { get; }
+
+ public OtherVTMember(string value)
+ {
+ Value = value;
+ }
+ }
+
+ public class RefTypedMember : IMemberInterface
+ {
+ public string Value { get; }
+
+ public RefTypedMember(string value)
+ {
+ Value = value;
+ }
+ }
+
+ public class OtherRTMember : IMemberInterface
+ {
+ public string Value { get; }
+
+ public OtherRTMember(string value)
+ {
+ Value = value;
+ }
+ }
+
+}
diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj
index 74459b8f4f6b7a..1e499bbd54d9eb 100644
--- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj
+++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj
@@ -9,6 +9,7 @@
+
@@ -73,6 +74,7 @@
+
@@ -80,6 +82,7 @@
+
@@ -128,6 +131,7 @@
+