From c08be254963194f023c2e76867093a300ff713fe Mon Sep 17 00:00:00 2001 From: devsko Date: Wed, 26 Aug 2020 01:39:57 +0200 Subject: [PATCH] Support polymorphic value-type converters and add validation on values returned by custom converters (#40914) * IL emit Box/Unbox in member access * Test case for object converter * Test case for class with primitive members and object converter * Address feedback * Fixed issues with JsonConverterand nullable value-types * Address feedback * Fix test for .NETStandard version * Fix #41146 * Accidentally commited the local test hack * Throw JsonException when a deserialized value cannot be assigned to the property/field * Fix merge * Fix merge again * Undo Fix #41146 * Consolidate nullable annotations * Addressed feedback * Addressed feedback --- .../System.Text.Json/src/Resources/Strings.resx | 6 + .../System.Text.Json/src/System.Text.Json.csproj | 1 + .../Converters/Value/NullableConverterFactory.cs | 6 + .../Text/Json/Serialization/JsonConverterOfT.cs | 5 +- .../Text/Json/Serialization/JsonPropertyInfoOfT.cs | 15 + .../JsonSerializerOptions.Converters.cs | 12 +- .../Serialization/ReflectionEmitMemberAccessor.cs | 82 +++-- .../System/Text/Json/ThrowHelper.Serialization.cs | 14 + .../src/System/Text/Json/TypeExtensions.cs | 42 +++ .../System.Text.Json/tests/AssertHelper.cs | 20 ++ .../System.Text.Json/tests/JsonPropertyTests.cs | 2 +- .../CustomConverterTests.InvalidCast.cs | 226 ++++++++++++ .../CustomConverterTests.Object.cs | 194 +++++++++++ .../CustomConverterTests.ValueTypedMember.cs | 385 +++++++++++++++++++++ .../tests/Serialization/ExtensionDataTests.cs | 5 +- .../TestClasses/TestClasses.ValueTypedMember.cs | 109 ++++++ .../tests/System.Text.Json.Tests.csproj | 4 + 17 files changed, 1087 insertions(+), 41 deletions(-) create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/TypeExtensions.cs create mode 100644 src/libraries/System.Text.Json/tests/AssertHelper.cs create mode 100644 src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests/CustomConverterTests.InvalidCast.cs create mode 100644 src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests/CustomConverterTests.ValueTypedMember.cs create mode 100644 src/libraries/System.Text.Json/tests/Serialization/TestClasses/TestClasses.ValueTypedMember.cs diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index f6b8261..4562c9a 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 9db4b39..fc5c8ce 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 5f61b95..1967933 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 @@ namespace System.Text.Json.Serialization.Converters 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 987e897..9ce34a8 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 @@ namespace System.Text.Json.Serialization 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 7a0d015..ed3da3a 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 @@ namespace System.Text.Json 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 249cf37..9a442a2 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 @@ namespace System.Text.Json // 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 @@ namespace System.Text.Json // 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 @@ namespace System.Text.Json 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 @@ namespace System.Text.Json 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 5e7d1e8..042a64a 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 @@ namespace System.Text.Json.Serialization 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 @@ namespace System.Text.Json.Serialization 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 @@ namespace System.Text.Json.Serialization 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 @@ namespace System.Text.Json.Serialization 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 @@ namespace System.Text.Json.Serialization 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 @@ namespace System.Text.Json.Serialization : 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 @@ namespace System.Text.Json.Serialization 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 bb0e156..842b5dc 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 @@ -51,6 +51,20 @@ namespace System.Text.Json [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) { var ex = new JsonException(SR.Format(SR.SerializationConverterRead, 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 0000000..8abbf6d --- /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 0000000..0821027 --- /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 4d48938..8193c92 100644 --- a/src/libraries/System.Text.Json/tests/JsonPropertyTests.cs +++ b/src/libraries/System.Text.Json/tests/JsonPropertyTests.cs @@ -97,7 +97,7 @@ namespace System.Text.Json.Tests [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 0000000..8013edb --- /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 bdb1c6b..1ecdfeb 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 @@ namespace System.Text.Json.Serialization.Tests } } + 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 0000000..a4fa41d --- /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 a005ec3..b2c6baa 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 @@ namespace System.Text.Json.Serialization.Tests 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 0000000..ab7384a --- /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 f3a485b..adb637c 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 @@ + @@ -129,6 +132,7 @@ + -- 2.7.4