From 529563a2aba647d10ed447d2272bb1ae96980a09 Mon Sep 17 00:00:00 2001 From: Layomi Akinrinade Date: Tue, 17 Sep 2019 08:38:41 -0400 Subject: [PATCH] Support nullable values in dictionaries (dotnet/corefx#40991) * Support nullable values in dictionaries * Address review feedback * Address feedback * Defer dictionary key escaping to writer Commit migrated from https://github.com/dotnet/corefx/commit/a308e93b1f1303be7eab5ce2b8ed11e66cc8e01e --- .../Json/Serialization/JsonPropertyInfoCommon.cs | 29 +- .../Serialization/JsonPropertyInfoNotNullable.cs | 5 + .../JsonPropertyInfoNotNullableContravariant.cs | 5 + .../Json/Serialization/JsonPropertyInfoNullable.cs | 47 +++ .../JsonSerializer.Write.HandleDictionary.cs | 23 +- .../Serialization/DictionaryTests.KeyPolicy.cs | 40 +++ .../tests/Serialization/Null.ReadTests.cs | 6 + .../tests/Serialization/Null.WriteTests.cs | 15 + .../tests/Serialization/NullableTests.cs | 398 +++++++++++++++++++++ .../tests/Serialization/TestClasses.cs | 6 + .../tests/System.Text.Json.Tests.csproj | 1 + 11 files changed, 545 insertions(+), 30 deletions(-) create mode 100644 src/libraries/System.Text.Json/tests/Serialization/NullableTests.cs diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoCommon.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoCommon.cs index 7640201..3eb08b7 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoCommon.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoCommon.cs @@ -102,11 +102,6 @@ namespace System.Text.Json return new List(); } - public override Type GetDictionaryConcreteType() - { - return typeof(Dictionary); - } - public override Type GetConcreteType(Type parentType) { if (JsonClassInfo.IsDeserializedByAssigningFromList(parentType)) @@ -145,28 +140,28 @@ namespace System.Text.Json return instanceOfIList; } } - else if (instance is ICollection instanceOfICollection) + else if (instance is ICollection instanceOfICollection) { if (!instanceOfICollection.IsReadOnly) { - foreach (TRuntimeProperty item in sourceList) + foreach (TDeclaredProperty item in sourceList) { instanceOfICollection.Add(item); } return instanceOfICollection; } } - else if (instance is Stack instanceOfStack) + else if (instance is Stack instanceOfStack) { - foreach (TRuntimeProperty item in sourceList) + foreach (TDeclaredProperty item in sourceList) { instanceOfStack.Push(item); } return instanceOfStack; } - else if (instance is Queue instanceOfQueue) + else if (instance is Queue instanceOfQueue) { - foreach (TRuntimeProperty item in sourceList) + foreach (TDeclaredProperty item in sourceList) { instanceOfQueue.Enqueue(item); } @@ -206,13 +201,13 @@ namespace System.Text.Json return instanceOfIDictionary; } } - else if (instance is IDictionary instanceOfGenericIDictionary) + else if (instance is IDictionary instanceOfGenericIDictionary) { if (!instanceOfGenericIDictionary.IsReadOnly) { foreach (DictionaryEntry entry in sourceDictionary) { - instanceOfGenericIDictionary.Add((string)entry.Key, (TRuntimeProperty)entry.Value); + instanceOfGenericIDictionary.Add((string)entry.Key, (TDeclaredProperty)entry.Value); } return instanceOfGenericIDictionary; } @@ -285,9 +280,9 @@ namespace System.Text.Json } } - // Creates an IEnumerable and populates it with the items in the + // Creates an IEnumerable and populates it with the items in the // sourceList argument then uses the delegateKey argument to identify the appropriate cached - // CreateRange method to create and return the desired immutable collection type. + // CreateRange method to create and return the desired immutable collection type. public override IEnumerable CreateImmutableCollectionInstance(ref ReadStack state, Type collectionType, string delegateKey, IList sourceList, JsonSerializerOptions options) { IEnumerable collection = null; @@ -301,9 +296,9 @@ namespace System.Text.Json return collection; } - // Creates an IEnumerable and populates it with the items in the + // Creates an IEnumerable and populates it with the items in the // sourceList argument then uses the delegateKey argument to identify the appropriate cached - // CreateRange method to create and return the desired immutable collection type. + // CreateRange method to create and return the desired immutable collection type. public override IDictionary CreateImmutableDictionaryInstance(ref ReadStack state, Type collectionType, string delegateKey, IDictionary sourceDictionary, JsonSerializerOptions options) { IDictionary collection = null; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNotNullable.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNotNullable.cs index 39004e0..6a0538b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNotNullable.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNotNullable.cs @@ -122,5 +122,10 @@ namespace System.Text.Json } } } + + public override Type GetDictionaryConcreteType() + { + return typeof(Dictionary); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNotNullableContravariant.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNotNullableContravariant.cs index f1294c2..cc37701 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNotNullableContravariant.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNotNullableContravariant.cs @@ -123,5 +123,10 @@ namespace System.Text.Json.Serialization } } } + + public override Type GetDictionaryConcreteType() + { + return typeof(Dictionary); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNullable.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNullable.cs index 43c03b3..431df44 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNullable.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNullable.cs @@ -2,8 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Collections; using System.Collections.Generic; using System.Diagnostics; +using System.Text.Json.Serialization; namespace System.Text.Json { @@ -79,6 +81,46 @@ namespace System.Text.Json } } + protected override void OnWriteDictionary(ref WriteStackFrame current, Utf8JsonWriter writer) + { + Debug.Assert(Converter != null && current.CollectionEnumerator != null); + + string key = null; + TProperty? value = null; + if (current.CollectionEnumerator is IEnumerator> enumerator) + { + key = enumerator.Current.Key; + value = enumerator.Current.Value; + } + else if (current.IsIDictionaryConstructible || current.IsIDictionaryConstructibleProperty) + { + key = (string)((DictionaryEntry)current.CollectionEnumerator.Current).Key; + value = (TProperty?)((DictionaryEntry)current.CollectionEnumerator.Current).Value; + } + + Debug.Assert(key != null); + + if (value == null) + { + writer.WriteNull(key); + } + else + { + if (Options.DictionaryKeyPolicy != null) + { + key = Options.DictionaryKeyPolicy.ConvertName(key); + + if (key == null) + { + ThrowHelper.ThrowInvalidOperationException_SerializerDictionaryKeyNull(Options.DictionaryKeyPolicy.GetType()); + } + } + + writer.WritePropertyName(key); + Converter.Write(writer, value.GetValueOrDefault(), Options); + } + } + protected override void OnWriteEnumerable(ref WriteStackFrame current, Utf8JsonWriter writer) { if (Converter != null) @@ -106,5 +148,10 @@ namespace System.Text.Json } } } + + public override Type GetDictionaryConcreteType() + { + return typeof(Dictionary); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleDictionary.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleDictionary.cs index bfe3ef5..067e129 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleDictionary.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleDictionary.cs @@ -113,35 +113,33 @@ namespace System.Text.Json ref WriteStackFrame current, Utf8JsonWriter writer) { - if (converter == null) - { - return; - } - - Debug.Assert(current.CollectionEnumerator != null); + Debug.Assert(converter != null && current.CollectionEnumerator != null); string key; TProperty value; if (current.CollectionEnumerator is IEnumerator> enumerator) { - // Avoid boxing for strongly-typed enumerators such as returned from IDictionary - value = enumerator.Current.Value; key = enumerator.Current.Key; + value = enumerator.Current.Value; } else if (current.CollectionEnumerator is IEnumerator> polymorphicEnumerator) { - value = (TProperty)polymorphicEnumerator.Current.Value; key = polymorphicEnumerator.Current.Key; + value = (TProperty)polymorphicEnumerator.Current.Value; } else if (current.IsIDictionaryConstructible || current.IsIDictionaryConstructibleProperty) { - value = (TProperty)((DictionaryEntry)current.CollectionEnumerator.Current).Value; key = (string)((DictionaryEntry)current.CollectionEnumerator.Current).Key; + value = (TProperty)((DictionaryEntry)current.CollectionEnumerator.Current).Value; } else { // Todo: support non-generic Dictionary here (IDictionaryEnumerator) - throw new NotSupportedException(); + // https://github.com/dotnet/corefx/issues/41034 + throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection( + current.JsonPropertyInfo.DeclaredPropertyType, + current.JsonPropertyInfo.ParentClassType, + current.JsonPropertyInfo.PropertyInfo); } if (value == null) @@ -161,8 +159,7 @@ namespace System.Text.Json } } - JsonEncodedText escapedKey = JsonEncodedText.Encode(key, options.Encoder); - writer.WritePropertyName(escapedKey); + writer.WritePropertyName(key); converter.Write(writer, value, options); } } diff --git a/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.KeyPolicy.cs b/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.KeyPolicy.cs index 707eab3..4aaeb18 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.KeyPolicy.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.KeyPolicy.cs @@ -131,6 +131,46 @@ namespace System.Text.Json.Serialization.Tests } [Fact] + public static void CustomNameSerialize_NullableValue() + { + var options = new JsonSerializerOptions + { + DictionaryKeyPolicy = new UppercaseNamingPolicy() // e.g. myint -> MYINT. + }; + + Dictionary obj = new Dictionary { { "myint1", 1 }, { "myint2", 2 } }; + + const string Json = @"{""myint1"":1,""myint2"":2}"; + const string JsonCustomKey = @"{""MYINT1"":1,""MYINT2"":2}"; + + // Without key policy option, serialize keys as they are. + string json = JsonSerializer.Serialize(obj); + Assert.Equal(Json, json); + + // With key policy option, serialize keys honoring the custom key policy. + json = JsonSerializer.Serialize(obj, options); + Assert.Equal(JsonCustomKey, json); + } + + [Fact] + public static void NullNamePolicy_NullableValue() + { + var options = new JsonSerializerOptions + { + DictionaryKeyPolicy = new NullNamingPolicy() + }; + + // A naming policy that returns null is not allowed. + Assert.Throws(() => JsonSerializer.Serialize(new Dictionary { { "onlyKey", 1 } }, options)); + + // We don't use policy on deserialize, so we populate dictionary. + Dictionary obj = JsonSerializer.Deserialize>(@"{""onlyKey"": 1}", options); + + Assert.Equal(1, obj.Count); + Assert.Equal(1, obj["onlyKey"]); + } + + [Fact] public static void KeyConflict_Serialize_WriteAll() { var options = new JsonSerializerOptions diff --git a/src/libraries/System.Text.Json/tests/Serialization/Null.ReadTests.cs b/src/libraries/System.Text.Json/tests/Serialization/Null.ReadTests.cs index 9beff45..d0614cf 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/Null.ReadTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/Null.ReadTests.cs @@ -71,12 +71,15 @@ namespace System.Text.Json.Serialization.Tests TestClassWithInitializedProperties obj = JsonSerializer.Deserialize(TestClassWithInitializedProperties.s_null_json); Assert.Null(obj.MyString); Assert.Null(obj.MyInt); + Assert.Null(obj.MyDateTime); Assert.Null(obj.MyIntArray); Assert.Null(obj.MyIntList); + Assert.Null(obj.MyNullableIntList); Assert.Null(obj.MyObjectList[0]); Assert.Null(obj.MyListList[0][0]); Assert.Null(obj.MyDictionaryList[0]["key"]); Assert.Null(obj.MyStringDictionary["key"]); + Assert.Null(obj.MyNullableDateTimeDictionary["key"]); Assert.Null(obj.MyObjectDictionary["key"]); Assert.Null(obj.MyStringDictionaryDictionary["key"]["key"]); Assert.Null(obj.MyListDictionary["key"][0]); @@ -93,13 +96,16 @@ namespace System.Text.Json.Serialization.Tests Assert.Equal("Hello", obj.MyString); Assert.Equal(1, obj.MyInt); + Assert.Equal(new DateTime(1995, 4, 16), obj.MyDateTime); Assert.Equal(1, obj.MyIntArray[0]); Assert.Equal(1, obj.MyIntList[0]); + Assert.Equal(1, obj.MyNullableIntList[0]); Assert.Null(obj.MyObjectList[0]); Assert.Null(obj.MyObjectList[0]); Assert.Null(obj.MyListList[0][0]); Assert.Null(obj.MyDictionaryList[0]["key"]); + Assert.Null(obj.MyNullableDateTimeDictionary["key"]); Assert.Null(obj.MyStringDictionary["key"]); Assert.Null(obj.MyObjectDictionary["key"]); Assert.Null(obj.MyStringDictionaryDictionary["key"]["key"]); diff --git a/src/libraries/System.Text.Json/tests/Serialization/Null.WriteTests.cs b/src/libraries/System.Text.Json/tests/Serialization/Null.WriteTests.cs index 7ae2aa7..c9ed945 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/Null.WriteTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/Null.WriteTests.cs @@ -16,12 +16,15 @@ namespace System.Text.Json.Serialization.Tests { MyString = null, MyInt = null, + MyDateTime = null, MyIntArray = null, MyIntList = null, + MyNullableIntList = null, MyObjectList = new List { null }, MyListList = new List> { new List { null } }, MyDictionaryList = new List> { new Dictionary() { ["key"] = null } }, MyStringDictionary = new Dictionary() { ["key"] = null }, + MyNullableDateTimeDictionary = new Dictionary() { ["key"] = null }, MyObjectDictionary = new Dictionary() { ["key"] = null }, MyStringDictionaryDictionary = new Dictionary>() { ["key"] = null }, MyListDictionary = new Dictionary>() { ["key"] = null }, @@ -31,12 +34,15 @@ namespace System.Text.Json.Serialization.Tests string json = JsonSerializer.Serialize(obj); Assert.Contains(@"""MyString"":null", json); Assert.Contains(@"""MyInt"":null", json); + Assert.Contains(@"""MyDateTime"":null", json); Assert.Contains(@"""MyIntArray"":null", json); Assert.Contains(@"""MyIntList"":null", json); + Assert.Contains(@"""MyNullableIntList"":null", json); Assert.Contains(@"""MyObjectList"":[null],", json); Assert.Contains(@"""MyListList"":[[null]],", json); Assert.Contains(@"""MyDictionaryList"":[{""key"":null}],", json); Assert.Contains(@"""MyStringDictionary"":{""key"":null},", json); + Assert.Contains(@"""MyNullableDateTimeDictionary"":{""key"":null},", json); Assert.Contains(@"""MyObjectDictionary"":{""key"":null},", json); Assert.Contains(@"""MyStringDictionaryDictionary"":{""key"":null},", json); Assert.Contains(@"""MyListDictionary"":{""key"":null},", json); @@ -53,12 +59,15 @@ namespace System.Text.Json.Serialization.Tests { MyString = null, MyInt = null, + MyDateTime = null, MyIntArray = null, MyIntList = null, + MyNullableIntList = null, MyObjectList = new List { null }, MyListList = new List> { new List { null } }, MyDictionaryList = new List> { new Dictionary() { ["key"] = null } }, MyStringDictionary = new Dictionary() { ["key"] = null }, + MyNullableDateTimeDictionary = new Dictionary() { ["key"] = null }, MyObjectDictionary = new Dictionary() { ["key"] = null }, MyStringDictionaryDictionary = new Dictionary>() { ["key"] = new Dictionary() { ["key"] = null } }, MyListDictionary = new Dictionary>() { ["key"] = new List { null } }, @@ -71,14 +80,17 @@ namespace System.Text.Json.Serialization.Tests TestClassWithInitializedProperties newObj = JsonSerializer.Deserialize(json); Assert.Equal("Hello", newObj.MyString); Assert.Equal(1, newObj.MyInt); + Assert.Equal(new DateTime(1995, 4, 16), newObj.MyDateTime); Assert.Equal(1, newObj.MyIntArray[0]); Assert.Equal(1, newObj.MyIntList[0]); + Assert.Equal(1, newObj.MyNullableIntList[0]); Assert.Null(newObj.MyObjectList[0]); Assert.Null(newObj.MyObjectList[0]); Assert.Null(newObj.MyListList[0][0]); Assert.Null(newObj.MyDictionaryList[0]["key"]); Assert.Null(newObj.MyStringDictionary["key"]); + Assert.Null(newObj.MyNullableDateTimeDictionary["key"]); Assert.Null(newObj.MyObjectDictionary["key"]); Assert.Null(newObj.MyStringDictionaryDictionary["key"]["key"]); Assert.Null(newObj.MyListDictionary["key"][0]); @@ -95,14 +107,17 @@ namespace System.Text.Json.Serialization.Tests TestClassWithInitializedProperties nestedObj = newParentObj.MyClass; Assert.Equal("Hello", nestedObj.MyString); Assert.Equal(1, nestedObj.MyInt); + Assert.Equal(new DateTime(1995, 4, 16), nestedObj.MyDateTime); Assert.Equal(1, nestedObj.MyIntArray[0]); Assert.Equal(1, nestedObj.MyIntList[0]); + Assert.Equal(1, nestedObj.MyNullableIntList[0]); Assert.Null(nestedObj.MyObjectList[0]); Assert.Null(nestedObj.MyObjectList[0]); Assert.Null(nestedObj.MyListList[0][0]); Assert.Null(nestedObj.MyDictionaryList[0]["key"]); Assert.Null(nestedObj.MyStringDictionary["key"]); + Assert.Null(nestedObj.MyNullableDateTimeDictionary["key"]); Assert.Null(nestedObj.MyObjectDictionary["key"]); Assert.Null(nestedObj.MyStringDictionaryDictionary["key"]["key"]); Assert.Null(nestedObj.MyListDictionary["key"][0]); diff --git a/src/libraries/System.Text.Json/tests/Serialization/NullableTests.cs b/src/libraries/System.Text.Json/tests/Serialization/NullableTests.cs new file mode 100644 index 0000000..cfc9942 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Serialization/NullableTests.cs @@ -0,0 +1,398 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public static partial class NullableTests + { + [Fact] + public static void DictionaryWithNullableValue() + { + Dictionary dictWithFloatValue = new Dictionary { { "key", 42.0f } }; + Dictionary dictWithFloatNull = new Dictionary { { "key", null } }; + TestDictionaryWithNullableValue, Dictionary>, float?>( + dictWithFloatValue, + dictWithFloatNull, + dictOfDictWithValue: new Dictionary> { { "key", dictWithFloatValue } }, + dictOfDictWithNull: new Dictionary> { { "key", dictWithFloatNull } }, + 42.0f); + + DateTime now = DateTime.Now; + Dictionary dictWithDateTimeValue = new Dictionary { { "key", now } }; + Dictionary dictWithDateTimeNull = new Dictionary { { "key", null } }; + TestDictionaryWithNullableValue, Dictionary>, DateTime?>( + dictWithDateTimeValue, + dictWithDateTimeNull, + dictOfDictWithValue: new Dictionary> { { "key", dictWithDateTimeValue } }, + dictOfDictWithNull: new Dictionary> { { "key", dictWithDateTimeNull } }, + now); + + MyDictionaryWrapper dictWrapperWithFloatValue = new MyDictionaryWrapper() { { "key", 42.0f } }; + MyDictionaryWrapper dictWrapperWithFloatNull = new MyDictionaryWrapper() { { "key", null } }; + TestDictionaryWithNullableValue, MyDictionaryWrapper>, float?>( + dictWrapperWithFloatValue, + dictWrapperWithFloatNull, + dictOfDictWithValue: new MyDictionaryWrapper> { { "key", dictWrapperWithFloatValue } }, + dictOfDictWithNull: new MyDictionaryWrapper> { { "key", dictWrapperWithFloatNull } }, + 42.0f); + + MyIDictionaryWrapper idictWrapperWithFloatValue = new MyIDictionaryWrapper() { { "key", 42.0f } }; + MyIDictionaryWrapper idictWrapperWithFloatNull = new MyIDictionaryWrapper() { { "key", null } }; + TestDictionaryWithNullableValue, MyIDictionaryWrapper>, float?>( + idictWrapperWithFloatValue, + idictWrapperWithFloatNull, + dictOfDictWithValue: new MyIDictionaryWrapper> { { "key", idictWrapperWithFloatValue } }, + dictOfDictWithNull: new MyIDictionaryWrapper> { { "key", idictWrapperWithFloatNull } }, + 42.0f); + + IDictionary idictWithDateTimeValue = new Dictionary { { "key", now } }; + IDictionary idictWithDateTimeNull = new Dictionary { { "key", null } }; + TestDictionaryWithNullableValue, IDictionary>, DateTime?>( + idictWithDateTimeValue, + idictWithDateTimeNull, + dictOfDictWithValue: new Dictionary> { { "key", idictWithDateTimeValue } }, + dictOfDictWithNull: new Dictionary> { { "key", idictWithDateTimeNull } }, + now); + + ImmutableDictionary immutableDictWithDateTimeValue = ImmutableDictionary.CreateRange(new Dictionary { { "key", now } }); + ImmutableDictionary immutableDictWithDateTimeNull = ImmutableDictionary.CreateRange(new Dictionary { { "key", null } }); + TestDictionaryWithNullableValue, ImmutableDictionary>, DateTime?>( + immutableDictWithDateTimeValue, + immutableDictWithDateTimeNull, + dictOfDictWithValue: ImmutableDictionary.CreateRange(new Dictionary> { { "key", immutableDictWithDateTimeValue } }), + dictOfDictWithNull: ImmutableDictionary.CreateRange(new Dictionary> { { "key", immutableDictWithDateTimeNull } }), + now); + } + + public class MyOverflowWrapper + { + [JsonExtensionData] + public Dictionary MyOverflow { get; set; } + } + + public class MyMultipleOverflowWrapper + { + [JsonExtensionData] + public Dictionary MyValidOverflow { get; set; } + + [JsonExtensionData] + public Dictionary MyInvalidOverflow { get; set; } + } + + public class AnotherMultipleOverflowWrapper + { + [JsonExtensionData] + public Dictionary MyInvalidOverflow { get; set; } + + [JsonExtensionData] + public Dictionary MyValidOverflow { get; set; } + } + + public class AnotherOverflowWrapper + { + public MyOverflowWrapper Wrapper { get; set; } + } + + [Fact] + public static void ExtensionDataWithNullableJsonElement_Throws() + { + Assert.Throws(() => JsonSerializer.Deserialize(@"{""key"":""value""}")); + Assert.Throws(() => JsonSerializer.Deserialize(@"{""Wrapper"": {""key"":""value""}}")); + + // Having multiple extension properties is not allowed. + Assert.Throws(() => JsonSerializer.Deserialize(@"{""key"":""value""}")); + Assert.Throws(() => JsonSerializer.Deserialize(@"{""key"":""value""}")); + } + + private static void TestDictionaryWithNullableValue( + TDict dictWithValue, + TDict dictWithNull, + TDictOfDict dictOfDictWithValue, + TDictOfDict dictOfDictWithNull, + TValue value) + { + string valueSerialized = JsonSerializer.Serialize(value); + + static void ValidateDict(TDict dict, TValue expectedValue) + { + IDictionary genericIDict = (IDictionary)dict; + Assert.Equal(1, genericIDict.Count); + Assert.Equal(expectedValue, genericIDict["key"]); + } + + static void ValidateDictOfDict(TDictOfDict dictOfDict, TValue expectedValue) + { + IDictionary genericIDict = (IDictionary)dictOfDict; + Assert.Equal(1, genericIDict.Count); + + IDictionary nestedDict = (IDictionary)genericIDict["key"]; + Assert.Equal(1, nestedDict.Count); + Assert.Equal(expectedValue, nestedDict["key"]); + } + + string json = JsonSerializer.Serialize(dictWithValue); + Assert.Equal(@"{""key"":" + valueSerialized + "}", json); + + TDict parsedDictWithValue = JsonSerializer.Deserialize(json); + ValidateDict(parsedDictWithValue, value); + + json = JsonSerializer.Serialize(dictWithNull); + Assert.Equal(@"{""key"":null}", json); + + TDict parsedDictWithNull = JsonSerializer.Deserialize(json); + ValidateDict(parsedDictWithNull, default); + + // Test nested dicts with nullable values. + json = JsonSerializer.Serialize(dictOfDictWithValue); + Assert.Equal(@"{""key"":{""key"":" + valueSerialized + "}}", json); + + TDictOfDict parsedDictOfDictWithValue = JsonSerializer.Deserialize(json); + ValidateDictOfDict(parsedDictOfDictWithValue, value); + + json = JsonSerializer.Serialize(dictOfDictWithNull); + Assert.Equal(@"{""key"":{""key"":null}}", json); + + TDictOfDict parsedDictOfDictWithNull = JsonSerializer.Deserialize(json); + ValidateDictOfDict(parsedDictOfDictWithNull, default); + } + + public class SimpleClassWithDictionariesWithNullableValues + { + public Dictionary Dict { get; set; } + public IDictionary IDict { get; set; } + public ImmutableDictionary ImmutableDict { get; set; } + public ImmutableSortedDictionary ImmutableSortedDict { get; set; } + } + + [Fact] + public static void ClassWithDictionariesWithNullableValues() + { + string json = + @"{ + ""Dict"": {""key"": ""1995-04-16""}, + ""IDict"": {""key"": null}, + ""ImmutableDict"": {""key"": ""1997-03-22""}, + ""ImmutableSortedDict"": { ""key"": null} + }"; + + SimpleClassWithDictionariesWithNullableValues obj = JsonSerializer.Deserialize(json); + Assert.Equal(new DateTime(1995, 4, 16), obj.Dict["key"]); + Assert.Null(obj.IDict["key"]); + Assert.Equal(new DateTime(1997, 3, 22), obj.ImmutableDict["key"]); + Assert.Null(obj.ImmutableSortedDict["key"]); + + string serialized = JsonSerializer.Serialize(obj); + Assert.Contains(@"""Dict"":{""key"":""1995-04-16T00:00:00""}", serialized); + Assert.Contains(@"""IDict"":{""key"":null}", serialized); + Assert.Contains(@"""ImmutableDict"":{""key"":""1997-03-22T00:00:00""}", serialized); + Assert.Contains(@"""ImmutableSortedDict"":{""key"":null}", serialized); + } + + [Fact] + public static void EnumerableWithNullableValue() + { + IEnumerable ieWithFloatValue = new List { 42.0f }; + IEnumerable ieWithFloatNull = new List { null }; + TestEnumerableWithNullableValue, IEnumerable>, float?>( + ieWithFloatValue, + ieWithFloatNull, + enumerableOfEnumerableWithValue: new List> { ieWithFloatValue }, + enumerableOfEnumerableWithNull: new List> { ieWithFloatNull }, + 42.0f); + + DateTime now = DateTime.Now; + IEnumerable ieWithDateTimeValue = new List { now }; + IEnumerable ieWithDateTimeNull = new List { null }; + TestEnumerableWithNullableValue, IEnumerable>, DateTime?>( + ieWithDateTimeValue, + ieWithDateTimeNull, + enumerableOfEnumerableWithValue: new List> { ieWithDateTimeValue }, + enumerableOfEnumerableWithNull: new List> { ieWithDateTimeNull }, + now); + + IReadOnlyList irlWithDateTimeValue = new List { now }; + IReadOnlyList irlWithDateTimeNull = new List { null }; + TestEnumerableWithNullableValue, IReadOnlyList>, DateTime?>( + irlWithDateTimeValue, + irlWithDateTimeNull, + enumerableOfEnumerableWithValue: new List> { irlWithDateTimeValue }, + enumerableOfEnumerableWithNull: new List> { irlWithDateTimeNull }, + now); + + Stack stWithDateTimeValue = new Stack(); + stWithDateTimeValue.Push(now); + + Stack stWithDateTimeNull = new Stack(); + stWithDateTimeNull.Push(null); + + Stack> enumerableOfEnumerableWithValue = new Stack>(); + enumerableOfEnumerableWithValue.Push(stWithDateTimeValue); + + Stack> enumerableOfEnumerableWithNull = new Stack>(); + enumerableOfEnumerableWithNull.Push(stWithDateTimeNull); + + TestEnumerableWithNullableValue, Stack>, DateTime?>( + stWithDateTimeValue, + stWithDateTimeNull, + enumerableOfEnumerableWithValue, + enumerableOfEnumerableWithNull, + now); + + IImmutableList imlWithDateTimeValue = ImmutableList.CreateRange(new List { now }); + IImmutableList imlWithDateTimeNull = ImmutableList.CreateRange(new List { null }); + TestEnumerableWithNullableValue, IImmutableList>, DateTime?>( + imlWithDateTimeValue, + imlWithDateTimeNull, + enumerableOfEnumerableWithValue: ImmutableList.CreateRange(new List> { imlWithDateTimeValue }), + enumerableOfEnumerableWithNull: ImmutableList.CreateRange(new List> { imlWithDateTimeNull }), + now); + } + + private static void TestEnumerableWithNullableValue( + TEnumerable enumerableWithValue, + TEnumerable enumerableWithNull, + TEnumerableOfEnumerable enumerableOfEnumerableWithValue, + TEnumerableOfEnumerable enumerableOfEnumerableWithNull, + TValue value) + { + string valueSerialized = JsonSerializer.Serialize(value); + + static void ValidateEnumerable(TEnumerable enumerable, TValue expectedValue) + { + IEnumerable ienumerable = (IEnumerable)enumerable; + int count = 0; + foreach (TValue val in ienumerable) + { + Assert.Equal(expectedValue, val); + count += 1; + } + Assert.Equal(1, count); + } + + static void ValidateEnumerableOfEnumerable(TEnumerableOfEnumerable dictOfDict, TValue expectedValue) + { + IEnumerable ienumerable = (IEnumerable)dictOfDict; + int ienumerableCount = 0; + int nestedIEnumerableCount = 0; + + foreach (IEnumerable nestedIEnumerable in ienumerable) + { + foreach (TValue val in nestedIEnumerable) + { + Assert.Equal(expectedValue, val); + nestedIEnumerableCount += 1; + } + ienumerableCount += 1; + } + Assert.Equal(1, ienumerableCount); + Assert.Equal(1, nestedIEnumerableCount); + } + + string json = JsonSerializer.Serialize(enumerableWithValue); + Assert.Equal($"[{valueSerialized}]", json); + + TEnumerable parsedEnumerableWithValue = JsonSerializer.Deserialize(json); + ValidateEnumerable(parsedEnumerableWithValue, value); + + json = JsonSerializer.Serialize(enumerableWithNull); + Assert.Equal("[null]", json); + + TEnumerable parsedEnumerableWithNull = JsonSerializer.Deserialize(json); + ValidateEnumerable(parsedEnumerableWithNull, default); + + // Test nested dicts with nullable values. + json = JsonSerializer.Serialize(enumerableOfEnumerableWithValue); + Assert.Equal($"[[{valueSerialized}]]", json); + + TEnumerableOfEnumerable parsedEnumerableOfEnumerableWithValue = JsonSerializer.Deserialize(json); + ValidateEnumerableOfEnumerable(parsedEnumerableOfEnumerableWithValue, value); + + json = JsonSerializer.Serialize(enumerableOfEnumerableWithNull); + Assert.Equal("[[null]]", json); + + TEnumerableOfEnumerable parsedEnumerableOfEnumerableWithNull = JsonSerializer.Deserialize(json); + ValidateEnumerableOfEnumerable(parsedEnumerableOfEnumerableWithNull, default); + } + + public class MyDictionaryWrapper : Dictionary { } + + public class MyIDictionaryWrapper : IDictionary + { + Dictionary dict = new Dictionary(); + + // Derived types need default constructors to be supported. + public MyIDictionaryWrapper() { } + + public TValue this[string key] { get => ((IDictionary)dict)[key]; set => ((IDictionary)dict)[key] = value; } + + public ICollection Keys => ((IDictionary)dict).Keys; + + public ICollection Values => ((IDictionary)dict).Values; + + public int Count => ((IDictionary)dict).Count; + + public bool IsReadOnly => ((IDictionary)dict).IsReadOnly; + + public void Add(string key, TValue value) + { + ((IDictionary)dict).Add(key, value); + } + + public void Add(KeyValuePair item) + { + ((IDictionary)dict).Add(item); + } + + public void Clear() + { + ((IDictionary)dict).Clear(); + } + + public bool Contains(KeyValuePair item) + { + return ((IDictionary)dict).Contains(item); + } + + public bool ContainsKey(string key) + { + return ((IDictionary)dict).ContainsKey(key); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + ((IDictionary)dict).CopyTo(array, arrayIndex); + } + + public IEnumerator> GetEnumerator() + { + return ((IDictionary)dict).GetEnumerator(); + } + + public bool Remove(string key) + { + return ((IDictionary)dict).Remove(key); + } + + public bool Remove(KeyValuePair item) + { + return ((IDictionary)dict).Remove(item); + } + + public bool TryGetValue(string key, out TValue value) + { + return ((IDictionary)dict).TryGetValue(key, out value); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IDictionary)dict).GetEnumerator(); + } + } + } +} diff --git a/src/libraries/System.Text.Json/tests/Serialization/TestClasses.cs b/src/libraries/System.Text.Json/tests/Serialization/TestClasses.cs index c188565..7d64e12 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/TestClasses.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/TestClasses.cs @@ -145,14 +145,17 @@ namespace System.Text.Json.Serialization.Tests { public string MyString { get; set; } = "Hello"; public int? MyInt { get; set; } = 1; + public DateTime? MyDateTime { get; set; } = new DateTime(1995, 4, 16); public int[] MyIntArray { get; set; } = new int[] { 1 }; public List MyIntList { get; set; } = new List { 1 }; + public List MyNullableIntList { get; set; } = new List { 1 }; public List MyObjectList { get; set; } = new List { 1 }; public List> MyListList { get; set; } = new List> { new List { 1 } }; public List> MyDictionaryList { get; set; } = new List> { new Dictionary { ["key"] = "value" } }; public Dictionary MyStringDictionary { get; set; } = new Dictionary { ["key"] = "value" }; + public Dictionary MyNullableDateTimeDictionary { get; set; } = new Dictionary { ["key"] = new DateTime(1995, 04, 16) }; public Dictionary MyObjectDictionary { get; set; } = new Dictionary { ["key"] = "value" }; public Dictionary> MyStringDictionaryDictionary { get; set; } = new Dictionary> { @@ -176,12 +179,15 @@ namespace System.Text.Json.Serialization.Tests @"{" + @"""MyString"" : null," + @"""MyInt"" : null," + + @"""MyDateTime"" : null," + @"""MyIntArray"" : null," + @"""MyIntList"" : null," + + @"""MyNullableIntList"" : null," + @"""MyObjectList"" : [null]," + @"""MyListList"" : [[null]]," + @"""MyDictionaryList"" : [{""key"" : null}]," + @"""MyStringDictionary"" : {""key"" : null}," + + @"""MyNullableDateTimeDictionary"" : {""key"" : null}," + @"""MyObjectDictionary"" : {""key"" : null}," + @"""MyStringDictionaryDictionary"" : {""key"" : {""key"" : null}}," + @"""MyListDictionary"" : {""key"" : [null]}," + 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 b1d02c5..a2b5510 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 @@ -61,6 +61,7 @@ + -- 2.7.4