From cf7d20adc403b4f98690e00093598fe056670f30 Mon Sep 17 00:00:00 2001 From: Steve Harter Date: Fri, 17 May 2019 07:56:47 -0700 Subject: [PATCH] Add extension data support to Json serializer (dotnet/corefx#37690) Commit migrated from https://github.com/dotnet/corefx/commit/71487a4a5bf7c0043cca4672ff3352c098dabd8c --- .../System.Text.Json/ref/System.Text.Json.cs | 5 + .../System.Text.Json/src/Resources/Strings.resx | 15 ++ .../System.Text.Json/src/System.Text.Json.csproj | 13 +- .../src/System/Text/Json/JsonHelpers.cs | 14 ++ .../Serialization/JsonClassInfo.AddProperty.cs | 14 +- .../Text/Json/Serialization/JsonClassInfo.cs | 118 ++++++--- .../Serialization/JsonExtensionDataAttribute.cs | 20 ++ .../Text/Json/Serialization/JsonPropertyInfo.cs | 182 +++++++------- .../Json/Serialization/JsonPropertyInfoCommon.cs | 15 +- .../Serialization/JsonPropertyInfoNotNullable.cs | 101 ++++---- .../Json/Serialization/JsonPropertyInfoNullable.cs | 59 ++--- .../JsonSerializer.Read.HandleArray.cs | 56 ++--- .../JsonSerializer.Read.HandleDictionary.cs | 95 ++++++++ .../JsonSerializer.Read.HandleNull.cs | 11 +- .../JsonSerializer.Read.HandleObject.cs | 72 ++---- .../JsonSerializer.Read.HandlePropertyName.cs | 131 ++++++++++ .../JsonSerializer.Read.HandleValue.cs | 2 +- .../Text/Json/Serialization/JsonSerializer.Read.cs | 74 +++--- .../JsonSerializer.Write.HandleDictionary.cs | 76 +++--- .../JsonSerializer.Write.HandleEnumerable.cs | 9 +- .../JsonSerializer.Write.HandleObject.cs | 10 +- .../System/Text/Json/Serialization/ReadStack.cs | 4 +- .../Text/Json/Serialization/ReadStackFrame.cs | 58 +++-- .../Text/Json/Serialization/WriteStackFrame.cs | 4 +- .../System/Text/Json/ThrowHelper.Serialization.cs | 42 +++- .../src/System/Text/Json/ThrowHelper.cs | 2 +- .../tests/Serialization/Array.ReadTests.cs | 36 ++- .../tests/Serialization/Array.WriteTests.cs | 2 - .../tests/Serialization/DictionaryTests.cs | 70 +++++- .../tests/Serialization/ExtensionDataTests.cs | 266 +++++++++++++++++++++ .../tests/Serialization/OptionsTests.cs | 48 ++++ .../tests/Serialization/PropertyVisibilityTests.cs | 10 +- .../tests/Serialization/TestClasses.cs | 9 + .../tests/System.Text.Json.Tests.csproj | 1 + 34 files changed, 1172 insertions(+), 472 deletions(-) create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonExtensionDataAttribute.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleDictionary.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs create mode 100644 src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs index fe1345d..47fbbcb 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -351,6 +351,11 @@ namespace System.Text.Json.Serialization protected JsonAttribute() { } } [System.AttributeUsageAttribute(System.AttributeTargets.Property, AllowMultiple=false)] + public sealed partial class JsonExtensionDataAttribute : System.Text.Json.Serialization.JsonAttribute + { + public JsonExtensionDataAttribute() { } + } + [System.AttributeUsageAttribute(System.AttributeTargets.Property, AllowMultiple=false)] public sealed partial class JsonIgnoreAttribute : System.Text.Json.Serialization.JsonAttribute { public JsonIgnoreAttribute() { } diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index dc3eb4a..52c72c4 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -342,4 +342,19 @@ Deserialization of type {0} is not supported. + + The data extension property '{0}.{1}' does not match the required signature of IDictionary<string, JsonElement> or IDictionary<string, object>. + + + A class cannot have more than one property that has the attribute '{0}'. + + + The collection type '{0}' is not supported. + + + The data extension property '{0}.{1}' cannot contain dictionary values of type '{2}'. Dictionary values must be of type JsonElement. + + + The collection type '{0}' on '{1}' is not supported. + \ No newline at end of file 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 15106dc..04c2ed9 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -76,6 +76,7 @@ + @@ -84,21 +85,23 @@ + + + + - - - - - + + + diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs index 251fe29..4485067 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs @@ -77,5 +77,19 @@ namespace System.Text.Json /// Otherwise, returns . /// public static bool IsDigit(byte value) => (uint)(value - '0') <= '9' - '0'; + + /// + /// Calls Encoding.UTF8.GetString that supports netstandard. + /// + /// The utf8 bytes to convert. + /// + internal static string Utf8GetString(ReadOnlySpan bytes) + { + return Encoding.UTF8.GetString(bytes +#if netstandard + .ToArray() +#endif + ); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.AddProperty.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.AddProperty.cs index 05b9ca1..c757848 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.AddProperty.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.AddProperty.cs @@ -54,13 +54,19 @@ namespace System.Text.Json.Serialization internal static JsonPropertyInfo CreateProperty(Type declaredPropertyType, Type runtimePropertyType, PropertyInfo propertyInfo, Type parentClassType, JsonSerializerOptions options) { + bool hasIgnoreAttribute = (JsonPropertyInfo.GetAttribute(propertyInfo) != null); + if (hasIgnoreAttribute) + { + return JsonPropertyInfo.CreateIgnoredPropertyPlaceholder(propertyInfo, options); + } + Type collectionElementType = null; switch (GetClassType(runtimePropertyType)) { case ClassType.Enumerable: case ClassType.Dictionary: case ClassType.Unknown: - collectionElementType = GetElementType(runtimePropertyType); + collectionElementType = GetElementType(runtimePropertyType, parentClassType, propertyInfo); break; } @@ -79,10 +85,12 @@ namespace System.Text.Json.Serialization JsonPropertyInfo jsonInfo = (JsonPropertyInfo)Activator.CreateInstance( propertyInfoClassType, BindingFlags.Instance | BindingFlags.Public, - binder: null, - new object[] { parentClassType, declaredPropertyType, runtimePropertyType, propertyInfo, collectionElementType, options }, + binder: null, + args: null, culture: null); + jsonInfo.Initialize(parentClassType, declaredPropertyType, runtimePropertyType, propertyInfo, collectionElementType, options); + return jsonInfo; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs index 8b53fce..d993307 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs @@ -11,7 +11,7 @@ using System.Text.Json.Serialization.Converters; namespace System.Text.Json.Serialization { - [DebuggerDisplay("ClassType.{ClassType} {Type.Name}")] + [DebuggerDisplay("ClassType.{ClassType}, {Type.Name}")] internal sealed partial class JsonClassInfo { // The length of the property name embedded in the key (in bytes). @@ -27,6 +27,8 @@ namespace System.Text.Json.Serialization internal ClassType ClassType { get; private set; } + public JsonPropertyInfo DataExtensionProperty { get; private set; } + // If enumerable, the JsonClassInfo for the element type. internal JsonClassInfo ElementClassInfo { get; private set; } @@ -38,6 +40,8 @@ namespace System.Text.Json.Serialization // Todo: when using PropertyNameCaseInsensitive we also need to use the hashtable with case-insensitive // comparison to handle Turkish etc. cultures properly. + Debug.Assert(_propertyRefs != null); + // Set the sorted property cache. Overwrite any existing cache which can occur in multi-threaded cases. if (frame.PropertyRefCache != null) { @@ -51,9 +55,9 @@ namespace System.Text.Json.Serialization for (int iProperty = 0; iProperty < _propertyRefs.Count; iProperty++) { PropertyRef propertyRef = _propertyRefs[iProperty]; - bool found = false; int iCacheProperty = 0; + for (; iCacheProperty < cache.Count; iCacheProperty++) { if (IsPropertyRefEqual(ref propertyRef, cache[iCacheProperty])) @@ -99,10 +103,7 @@ namespace System.Text.Json.Serialization { JsonPropertyInfo jsonPropertyInfo = AddProperty(propertyInfo.PropertyType, propertyInfo, type, options); - if (jsonPropertyInfo.NameAsString == null) - { - ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNameNull(this, jsonPropertyInfo); - } + Debug.Assert(jsonPropertyInfo.NameUsedToCompareAsString != null); // If the JsonPropertyNameAttribute or naming policy results in collisions, throw an exception. if (!propertyNames.Add(jsonPropertyInfo.NameUsedToCompareAsString)) @@ -113,7 +114,10 @@ namespace System.Text.Json.Serialization jsonPropertyInfo.ClearUnusedValuesAfterAdd(); } } + + DetermineExtensionDataProperty(); break; + case ClassType.Enumerable: case ClassType.Dictionary: // Add a single property that maps to the class type so we can have policies applied. @@ -123,7 +127,7 @@ namespace System.Text.Json.Serialization CreateObject = options.ClassMaterializerStrategy.CreateConstructor(policyProperty.RuntimePropertyType); // Create a ClassInfo that maps to the element type which is used for (de)serialization and policies. - Type elementType = GetElementType(type); + Type elementType = GetElementType(type, parentType : null, memberInfo: null); ElementClassInfo = options.GetOrAddClass(elementType); break; case ClassType.Value: @@ -137,12 +141,53 @@ namespace System.Text.Json.Serialization } } + private void DetermineExtensionDataProperty() + { + JsonPropertyInfo jsonPropertyInfo = GetPropertyThatHasAttribute(typeof(JsonExtensionDataAttribute)); + if (jsonPropertyInfo != null) + { + Type declaredPropertyType = jsonPropertyInfo.DeclaredPropertyType; + if (!typeof(IDictionary).IsAssignableFrom(declaredPropertyType) && + !typeof(IDictionary).IsAssignableFrom(declaredPropertyType)) + { + ThrowHelper.ThrowInvalidOperationException_SerializationDataExtensionPropertyInvalid(this, jsonPropertyInfo); + } + + DataExtensionProperty = jsonPropertyInfo; + } + } + + private JsonPropertyInfo GetPropertyThatHasAttribute(Type attributeType) + { + Debug.Assert(_propertyRefs != null); + + JsonPropertyInfo property = null; + + for (int iProperty = 0; iProperty < _propertyRefs.Count; iProperty++) + { + PropertyRef propertyRef = _propertyRefs[iProperty]; + JsonPropertyInfo jsonPropertyInfo = propertyRef.Info; + Attribute attribute = jsonPropertyInfo.PropertyInfo.GetCustomAttribute(attributeType); + if (attribute != null) + { + if (property != null) + { + ThrowHelper.ThrowInvalidOperationException_SerializationDuplicateAttribute(attributeType); + } + + property = jsonPropertyInfo; + } + } + + return property; + } + internal JsonPropertyInfo GetProperty(JsonSerializerOptions options, ReadOnlySpan propertyName, ref ReadStackFrame frame) { // If we should compare with case-insensitive, normalize to an uppercase format since that is what is cached on the propertyInfo. if (options.PropertyNameCaseInsensitive) { - string utf16PropertyName = Encoding.UTF8.GetString(propertyName.ToArray()); + string utf16PropertyName = JsonHelpers.Utf8GetString(propertyName); string upper = utf16PropertyName.ToUpperInvariant(); propertyName = Encoding.UTF8.GetBytes(upper); } @@ -200,10 +245,9 @@ namespace System.Text.Json.Serialization if (!hasPropertyCache) { - if (propertyIndex == 0) + if (propertyIndex == 0 && frame.PropertyRefCache == null) { // Create the temporary list on first property access to prevent a partially filled List. - Debug.Assert(frame.PropertyRefCache == null); frame.PropertyRefCache = new List(); } @@ -258,7 +302,7 @@ namespace System.Text.Json.Serialization if (propertyRef.Key == other.Key) { if (propertyRef.Info.Name.Length <= PropertyNameKeyLength || - propertyRef.Info.Name.SequenceEqual(other.Info.Name)) + propertyRef.Info.Name.AsSpan().SequenceEqual(other.Info.Name.AsSpan())) { return true; } @@ -308,37 +352,41 @@ namespace System.Text.Json.Serialization return key; } - public static Type GetElementType(Type propertyType) + // Return the element type of the IEnumerable, or return null if not an IEnumerable. + public static Type GetElementType(Type propertyType, Type parentType, MemberInfo memberInfo) { - Type elementType = null; - if (typeof(IEnumerable).IsAssignableFrom(propertyType)) + if (!typeof(IEnumerable).IsAssignableFrom(propertyType)) + { + return null; + } + + // Check for Array. + Type elementType = propertyType.GetElementType(); + if (elementType != null) + { + return elementType; + } + + // Check for Dictionary or IEnumerable + if (propertyType.IsGenericType) { - elementType = propertyType.GetElementType(); - if (elementType == null) + Type[] args = propertyType.GetGenericArguments(); + ClassType classType = GetClassType(propertyType); + + if (classType == ClassType.Dictionary && + args.Length >= 2 && // It is >= 2 in case there is a IDictionary. + args[0].UnderlyingSystemType == typeof(string)) { - Type[] args = propertyType.GetGenericArguments(); + return args[1]; + } - if (propertyType.IsGenericType) - { - if (GetClassType(propertyType) == ClassType.Dictionary && - args.Length >= 2) // It is >= 2 in case there is a Dictionary. - { - elementType = args[1]; - } - else if (GetClassType(propertyType) == ClassType.Enumerable && args.Length >= 1) // It is >= 1 in case there is an IEnumerable. - { - elementType = args[0]; - } - } - else - { - // Unable to determine collection type; attempt to use object which will be used to create loosely-typed collection. - elementType = typeof(object); - } + if (classType == ClassType.Enumerable && args.Length >= 1) // It is >= 1 in case there is an IEnumerable. + { + return args[0]; } } - return elementType; + throw ThrowHelper.GetNotSupportedException_SerializationNotSupportedCollection(propertyType, parentType, memberInfo); } internal static ClassType GetClassType(Type type) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonExtensionDataAttribute.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonExtensionDataAttribute.cs new file mode 100644 index 0000000..9e6dcd8 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonExtensionDataAttribute.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. +// See the LICENSE file in the project root for more information. + +namespace System.Text.Json.Serialization +{ + /// + /// When placed on a property of type , any + /// properties that do not have a matching member are added to that Dictionary during deserialization and written during serialization. + /// + /// + /// The TKey value must be and TValue must be or . + /// If there is more than one extension property on a type, or it the property is not of the correct type, + /// an is thrown during the first serialization or deserialization of that type. + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] + public sealed class JsonExtensionDataAttribute : JsonAttribute + { + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs index 8c429bc..6160df0 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs @@ -11,7 +11,7 @@ using System.Text.Json.Serialization.Policies; namespace System.Text.Json.Serialization { - [DebuggerDisplay("{PropertyInfo}")] + [DebuggerDisplay("PropertyInfo={PropertyInfo}, Element={ElementClassInfo}")] internal abstract class JsonPropertyInfo { // Cache the converters so they don't get created for every enumerable property. @@ -20,35 +20,33 @@ namespace System.Text.Json.Serialization private static readonly JsonEnumerableConverter s_jsonIEnumerableConstuctibleConverter = new DefaultIEnumerableConstructibleConverter(); private static readonly JsonEnumerableConverter s_jsonImmutableConverter = new DefaultImmutableConverter(); + public static readonly JsonPropertyInfo s_missingProperty = new JsonPropertyInfoNotNullable(); + public ClassType ClassType; // The name of the property with any casing policy or the name specified from JsonPropertyNameAttribute. - private byte[] _name { get; set; } - public ReadOnlySpan Name => _name; + public byte[] Name { get; private set; } public string NameAsString { get; private set; } // Used to support case-insensitive comparison - private byte[] _nameUsedToCompare { get; set; } - public ReadOnlySpan NameUsedToCompare => _nameUsedToCompare; + public byte[] NameUsedToCompare { get; private set; } public string NameUsedToCompareAsString { get; private set; } // The escaped name passed to the writer. - public byte[] _escapedName { get; private set; } + public byte[] EscapedName { get; private set; } public bool HasGetter { get; set; } public bool HasSetter { get; set; } public bool ShouldSerialize { get; private set; } public bool ShouldDeserialize { get; private set; } + public bool IsPropertyPolicy {get; protected set;} public bool IgnoreNullValues { get; private set; } // todo: to minimize hashtable lookups, cache JsonClassInfo: //public JsonClassInfo ClassInfo; - // Constructor used for internal identifiers - public JsonPropertyInfo() { } - - public JsonPropertyInfo( + public virtual void Initialize( Type parentClassType, Type declaredPropertyType, Type runtimePropertyType, @@ -94,100 +92,95 @@ namespace System.Text.Json.Serialization private void DeterminePropertyName(JsonSerializerOptions options) { - if (PropertyInfo != null) + if (PropertyInfo == null) { - JsonPropertyNameAttribute nameAttribute = GetAttribute(); - if (nameAttribute != null) - { - NameAsString = nameAttribute.Name; + return; + } - // null is not valid; JsonClassInfo throws an InvalidOperationException after this return. - if (NameAsString == null) - { - return; - } - } - else if (options.PropertyNamingPolicy != null) + JsonPropertyNameAttribute nameAttribute = GetAttribute(PropertyInfo); + if (nameAttribute != null) + { + string name = nameAttribute.Name; + if (name == null) { - NameAsString = options.PropertyNamingPolicy.ConvertName(PropertyInfo.Name); - - // null is not valid; JsonClassInfo throws an InvalidOperationException after this return. - if (NameAsString == null) - { - return; - } + ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNameNull(ParentClassType, this); } - else + + NameAsString = name; + } + else if (options.PropertyNamingPolicy != null) + { + string name = options.PropertyNamingPolicy.ConvertName(PropertyInfo.Name); + if (name == null) { - NameAsString = PropertyInfo.Name; + ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNameNull(ParentClassType, this); } - // At this point propertyName is valid UTF16, so just call the simple UTF16->UTF8 encoder. - _name = Encoding.UTF8.GetBytes(NameAsString); + NameAsString = name; + } + else + { + NameAsString = PropertyInfo.Name; + } - // Set the compare name. - if (options.PropertyNameCaseInsensitive) - { - NameUsedToCompareAsString = NameAsString.ToUpperInvariant(); - _nameUsedToCompare = Encoding.UTF8.GetBytes(NameUsedToCompareAsString); - } - else - { - NameUsedToCompareAsString = NameAsString; - _nameUsedToCompare = _name; - } + Debug.Assert(NameAsString != null); + + // At this point propertyName is valid UTF16, so just call the simple UTF16->UTF8 encoder. + Name = Encoding.UTF8.GetBytes(NameAsString); - // Cache the escaped name. + // Set the compare name. + if (options.PropertyNameCaseInsensitive) + { + NameUsedToCompareAsString = NameAsString.ToUpperInvariant(); + NameUsedToCompare = Encoding.UTF8.GetBytes(NameUsedToCompareAsString); + } + else + { + NameUsedToCompareAsString = NameAsString; + NameUsedToCompare = Name; + } + + // Cache the escaped name. #if true - // temporary behavior until the writer can accept escaped string. - _escapedName = _name; + // temporary behavior until the writer can accept escaped string. + EscapedName = Name; #else - - int valueIdx = JsonWriterHelper.NeedsEscaping(_name); - if (valueIdx == -1) - { - _escapedName = _name; - } - else - { - byte[] pooledName = null; - int length = JsonWriterHelper.GetMaxEscapedLength(_name.Length, valueIdx); + int valueIdx = JsonWriterHelper.NeedsEscaping(_name); + if (valueIdx == -1) + { + _escapedName = _name; + } + else + { + byte[] pooledName = null; + int length = JsonWriterHelper.GetMaxEscapedLength(_name.Length, valueIdx); - Span escapedName = length <= JsonConstants.StackallocThreshold ? - stackalloc byte[length] : - (pooledName = ArrayPool.Shared.Rent(length)); + Span escapedName = length <= JsonConstants.StackallocThreshold ? + stackalloc byte[length] : + (pooledName = ArrayPool.Shared.Rent(length)); - JsonWriterHelper.EscapeString(_name, escapedName, 0, out int written); + JsonWriterHelper.EscapeString(_name, escapedName, 0, out int written); - _escapedName = escapedName.Slice(0, written).ToArray(); + _escapedName = escapedName.Slice(0, written).ToArray(); - if (pooledName != null) - { - // We clear the array because it is "user data" (although a property name). - new Span(pooledName, 0, written).Clear(); - ArrayPool.Shared.Return(pooledName); - } + if (pooledName != null) + { + // We clear the array because it is "user data" (although a property name). + new Span(pooledName, 0, written).Clear(); + ArrayPool.Shared.Return(pooledName); } -#endif } +#endif } private void DetermineSerializationCapabilities(JsonSerializerOptions options) { - bool hasIgnoreAttribute = (GetAttribute() != null); - - if (hasIgnoreAttribute) - { - // We don't serialize or deserialize. - return; - } - - if (ClassType != ClassType.Enumerable) + if (ClassType != ClassType.Enumerable && ClassType != ClassType.Dictionary) { - // We serialize if there is a getter + no [Ignore] attribute + not ignoring readonly properties. + // We serialize if there is a getter + not ignoring readonly properties. ShouldSerialize = HasGetter && (HasSetter || !options.IgnoreReadOnlyProperties); - // We deserialize if there is a setter + no [Ignore] attribute. + // We deserialize if there is a setter. ShouldDeserialize = HasSetter; } else @@ -198,7 +191,8 @@ namespace System.Text.Json.Serialization { ShouldDeserialize = true; } - else if (RuntimePropertyType.IsAssignableFrom(typeof(IList))) + else if (!RuntimePropertyType.IsArray && + (typeof(IList).IsAssignableFrom(RuntimePropertyType) || typeof(IDictionary).IsAssignableFrom(RuntimePropertyType))) { ShouldDeserialize = true; } @@ -218,7 +212,7 @@ namespace System.Text.Json.Serialization } else if (typeof(IEnumerable).IsAssignableFrom(RuntimePropertyType)) { - Type elementType = JsonClassInfo.GetElementType(RuntimePropertyType); + Type elementType = JsonClassInfo.GetElementType(RuntimePropertyType, ParentClassType, PropertyInfo); // If the property type only has interface(s) exposed by JsonEnumerableT then use JsonEnumerableT as the converter. if (RuntimePropertyType.IsAssignableFrom(typeof(JsonEnumerableT<>).MakeGenericType(elementType))) @@ -260,16 +254,30 @@ namespace System.Text.Json.Serialization // Copy any settings defined at run-time to the new property. public void CopyRuntimeSettingsTo(JsonPropertyInfo other) { - other._name = _name; - other._nameUsedToCompare = _nameUsedToCompare; - other._escapedName = _escapedName; + other.Name = Name; + other.NameUsedToCompare = NameUsedToCompare; + other.EscapedName = EscapedName; + } + + // Create a property that is either ignored at run-time. It uses typeof(int) in order to prevent + // issues with unsupported types and helps ensure we don't accidently (de)serialize it. + public static JsonPropertyInfo CreateIgnoredPropertyPlaceholder(PropertyInfo propertyInfo, JsonSerializerOptions options) + { + JsonPropertyInfo jsonPropertyInfo = new JsonPropertyInfoNotNullable(); + jsonPropertyInfo.PropertyInfo = propertyInfo; + jsonPropertyInfo.DeterminePropertyName(options); + + Debug.Assert(!jsonPropertyInfo.ShouldDeserialize); + Debug.Assert(!jsonPropertyInfo.ShouldSerialize); + + return jsonPropertyInfo; } public abstract object GetValueAsObject(object obj); - public TAttribute GetAttribute() where TAttribute : Attribute + public static TAttribute GetAttribute(PropertyInfo propertyInfo) where TAttribute : Attribute { - return (TAttribute)PropertyInfo?.GetCustomAttribute(typeof(TAttribute), inherit: false); + return (TAttribute)propertyInfo?.GetCustomAttribute(typeof(TAttribute), inherit: false); } public abstract IEnumerable CreateImmutableCollectionFromList(string delegateKey, IList sourceList); 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 708f2d9..ba67427 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 @@ -16,24 +16,21 @@ namespace System.Text.Json.Serialization /// internal abstract class JsonPropertyInfoCommon : JsonPropertyInfo { - public bool _isPropertyPolicy; public Func Get { get; private set; } public Action Set { get; private set; } public JsonValueConverter ValueConverter { get; internal set; } - // Constructor used for internal identifiers - public JsonPropertyInfoCommon() { } - - public JsonPropertyInfoCommon( + public override void Initialize( Type parentClassType, Type declaredPropertyType, Type runtimePropertyType, PropertyInfo propertyInfo, Type elementType, - JsonSerializerOptions options) : - base(parentClassType, declaredPropertyType, runtimePropertyType, propertyInfo, elementType, options) + JsonSerializerOptions options) { + base.Initialize(parentClassType, declaredPropertyType, runtimePropertyType, propertyInfo, elementType, options); + if (propertyInfo != null) { if (propertyInfo.GetMethod?.IsPublic == true) @@ -50,7 +47,7 @@ namespace System.Text.Json.Serialization } else { - _isPropertyPolicy = true; + IsPropertyPolicy = true; HasGetter = true; HasSetter = true; ValueConverter = DefaultConverters.s_converter; @@ -67,7 +64,7 @@ namespace System.Text.Json.Serialization public override object GetValueAsObject(object obj) { - if (_isPropertyPolicy) + if (IsPropertyPolicy) { return obj; } 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 9c95b56..fa326d0 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 @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Diagnostics; -using System.Reflection; namespace System.Text.Json.Serialization { @@ -15,48 +14,33 @@ namespace System.Text.Json.Serialization JsonPropertyInfoCommon where TRuntimeProperty : TDeclaredProperty { - // Constructor used for internal identifiers - public JsonPropertyInfoNotNullable() { } - - public JsonPropertyInfoNotNullable( - Type parentClassType, - Type declaredPropertyType, - Type runtimePropertyType, - PropertyInfo propertyInfo, - Type elementType, - JsonSerializerOptions options) : - base(parentClassType, declaredPropertyType, runtimePropertyType, propertyInfo, elementType, options) - { - } - public override void Read(JsonTokenType tokenType, JsonSerializerOptions options, ref ReadStack state, ref Utf8JsonReader reader) { + Debug.Assert(ShouldDeserialize); + if (ElementClassInfo != null) { // Forward the setter to the value-based JsonPropertyInfo. JsonPropertyInfo propertyInfo = ElementClassInfo.GetPolicyProperty(); propertyInfo.ReadEnumerable(tokenType, options, ref state, ref reader); } - else if (ShouldDeserialize) + else { - if (ValueConverter != null) + if (ValueConverter != null && ValueConverter.TryRead(RuntimePropertyType, ref reader, out TRuntimeProperty value)) { - if (ValueConverter.TryRead(RuntimePropertyType, ref reader, out TRuntimeProperty value)) + if (state.Current.ReturnValue == null) { - if (state.Current.ReturnValue == null) - { - state.Current.ReturnValue = value; - } - else - { - // Null values were already handled. - Debug.Assert(value != null); - - Set((TClass)state.Current.ReturnValue, value); - } - - return; + state.Current.ReturnValue = value; } + else + { + // Null values were already handled. + Debug.Assert(value != null); + + Set((TClass)state.Current.ReturnValue, value); + } + + return; } ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(RuntimePropertyType, reader, state.PropertyPath); @@ -66,61 +50,64 @@ namespace System.Text.Json.Serialization // If this method is changed, also change JsonPropertyInfoNullable.ReadEnumerable and JsonSerializer.ApplyObjectToEnumerable public override void ReadEnumerable(JsonTokenType tokenType, JsonSerializerOptions options, ref ReadStack state, ref Utf8JsonReader reader) { + Debug.Assert(ShouldDeserialize); + if (ValueConverter == null || !ValueConverter.TryRead(RuntimePropertyType, ref reader, out TRuntimeProperty value)) { ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(RuntimePropertyType, reader, state.PropertyPath); return; } - JsonSerializer.ApplyValueToEnumerable(ref value, options, ref state, ref reader); + JsonSerializer.ApplyValueToEnumerable(ref value, ref state, ref reader); } public override void Write(JsonSerializerOptions options, ref WriteStackFrame current, Utf8JsonWriter writer) { Debug.Assert(current.Enumerator == null); + Debug.Assert(ShouldSerialize); - if (ShouldSerialize) + TRuntimeProperty value; + if (IsPropertyPolicy) { - TRuntimeProperty value; - if (_isPropertyPolicy) - { - value = (TRuntimeProperty)current.CurrentValue; - } - else + value = (TRuntimeProperty)current.CurrentValue; + } + else + { + value = (TRuntimeProperty)Get((TClass)current.CurrentValue); + } + + if (value == null) + { + Debug.Assert(EscapedName != null); + + if (!IgnoreNullValues) { - value = (TRuntimeProperty)Get((TClass)current.CurrentValue); + writer.WriteNull(EscapedName); } - - if (value == null) + } + else if (ValueConverter != null) + { + if (EscapedName != null) { - Debug.Assert(_escapedName != null); - - if (!IgnoreNullValues) - { - writer.WriteNull(_escapedName); - } + ValueConverter.Write(EscapedName, value, writer); } - else if (ValueConverter != null) + else { - if (_escapedName != null) - { - ValueConverter.Write(_escapedName, value, writer); - } - else - { - ValueConverter.Write(value, writer); - } + ValueConverter.Write(value, writer); } } } public override void WriteDictionary(JsonSerializerOptions options, ref WriteStackFrame current, Utf8JsonWriter writer) { + Debug.Assert(ShouldSerialize); JsonSerializer.WriteDictionary(ValueConverter, options, ref current, writer); } public override void WriteEnumerable(JsonSerializerOptions options, ref WriteStackFrame current, Utf8JsonWriter writer) { + Debug.Assert(ShouldSerialize); + if (ValueConverter != null) { Debug.Assert(current.Enumerator != null); 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 e276e29..0a476b1 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 @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Diagnostics; -using System.Reflection; namespace System.Text.Json.Serialization { @@ -18,46 +17,32 @@ namespace System.Text.Json.Serialization // should this be cached somewhere else so that it's not populated per TClass as well as TProperty? private static readonly Type s_underlyingType = typeof(TProperty); - public JsonPropertyInfoNullable( - Type parentClassType, - Type declaredPropertyType, - Type runtimePropertyType, - PropertyInfo propertyInfo, - Type elementType, - JsonSerializerOptions options) : - base(parentClassType, declaredPropertyType, runtimePropertyType, propertyInfo, elementType, options) - { - } - public override void Read(JsonTokenType tokenType, JsonSerializerOptions options, ref ReadStack state, ref Utf8JsonReader reader) { Debug.Assert(ElementClassInfo == null); + Debug.Assert(ShouldDeserialize); - if (ShouldDeserialize) + if (ValueConverter != null && ValueConverter.TryRead(s_underlyingType, ref reader, out TProperty value)) { - if (ValueConverter != null) + if (state.Current.ReturnValue == null) { - if (ValueConverter.TryRead(s_underlyingType, ref reader, out TProperty value)) - { - if (state.Current.ReturnValue == null) - { - state.Current.ReturnValue = value; - } - else - { - Set((TClass)state.Current.ReturnValue, value); - } - - return; - } + state.Current.ReturnValue = value; + } + else + { + Set((TClass)state.Current.ReturnValue, value); } - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(RuntimePropertyType, reader, state.PropertyPath); + return; } + + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(RuntimePropertyType, reader, state.PropertyPath); } public override void ReadEnumerable(JsonTokenType tokenType, JsonSerializerOptions options, ref ReadStack state, ref Utf8JsonReader reader) { + Debug.Assert(ShouldDeserialize); + if (ValueConverter == null || !ValueConverter.TryRead(typeof(TProperty), ref reader, out TProperty value)) { ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(RuntimePropertyType, reader, state.PropertyPath); @@ -65,21 +50,23 @@ namespace System.Text.Json.Serialization } TProperty? nullableValue = new TProperty?(value); - JsonSerializer.ApplyValueToEnumerable(ref nullableValue, options, ref state, ref reader); + JsonSerializer.ApplyValueToEnumerable(ref nullableValue, ref state, ref reader); } public override void Write(JsonSerializerOptions options, ref WriteStackFrame current, Utf8JsonWriter writer) { + Debug.Assert(ShouldSerialize); + if (current.Enumerator != null) { // Forward the setter to the value-based JsonPropertyInfo. JsonPropertyInfo propertyInfo = ElementClassInfo.GetPolicyProperty(); propertyInfo.WriteEnumerable(options, ref current, writer); } - else if (ShouldSerialize) + else { TProperty? value; - if (_isPropertyPolicy) + if (IsPropertyPolicy) { value = (TProperty?)current.CurrentValue; } @@ -90,18 +77,18 @@ namespace System.Text.Json.Serialization if (value == null) { - Debug.Assert(_escapedName != null); + Debug.Assert(EscapedName != null); if (!IgnoreNullValues) { - writer.WriteNull(_escapedName); + writer.WriteNull(EscapedName); } } else if (ValueConverter != null) { - if (_escapedName != null) + if (EscapedName != null) { - ValueConverter.Write(_escapedName, value.GetValueOrDefault(), writer); + ValueConverter.Write(EscapedName, value.GetValueOrDefault(), writer); } else { @@ -113,6 +100,8 @@ namespace System.Text.Json.Serialization public override void WriteEnumerable(JsonSerializerOptions options, ref WriteStackFrame current, Utf8JsonWriter writer) { + Debug.Assert(ShouldSerialize); + if (ValueConverter != null) { Debug.Assert(current.Enumerator != null); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleArray.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleArray.cs index b36dbdf..8ff408c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleArray.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleArray.cs @@ -16,12 +16,9 @@ namespace System.Text.Json.Serialization ref Utf8JsonReader reader, ref ReadStack state) { - JsonPropertyInfo jsonPropertyInfo; + JsonPropertyInfo jsonPropertyInfo = state.Current.JsonPropertyInfo; - jsonPropertyInfo = state.Current.JsonPropertyInfo; - - bool skip = jsonPropertyInfo != null && !jsonPropertyInfo.ShouldDeserialize; - if (skip || state.Current.Skip()) + if (state.Current.SkipProperty) { // The array is not being applied to the object. state.Push(); @@ -38,27 +35,26 @@ namespace System.Text.Json.Serialization jsonPropertyInfo = state.Current.JsonClassInfo.CreatePolymorphicProperty(jsonPropertyInfo, typeof(object), options); } + // Verify that we don't have a multidimensional array. Type arrayType = jsonPropertyInfo.RuntimePropertyType; if (!typeof(IEnumerable).IsAssignableFrom(arrayType) || (arrayType.IsArray && arrayType.GetArrayRank() > 1)) { ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(arrayType, reader, state.PropertyPath); } - Debug.Assert(state.Current.IsPropertyEnumerable || state.Current.IsDictionary); + Debug.Assert(state.Current.IsProcessingEnumerableOrDictionary); - if (state.Current.EnumerableCreated) + if (state.Current.PropertyInitialized) { // A nested json array so push a new stack frame. Type elementType = state.Current.JsonClassInfo.ElementClassInfo.GetPolicyProperty().RuntimePropertyType; state.Push(); - state.Current.Initialize(elementType, options); - state.Current.PopStackOnEnd = true; } else { - state.Current.EnumerableCreated = true; + state.Current.PropertyInitialized = true; } jsonPropertyInfo = state.Current.JsonPropertyInfo; @@ -85,8 +81,8 @@ namespace System.Text.Json.Serialization private static bool HandleEndArray( JsonSerializerOptions options, - ref ReadStack state, - ref Utf8JsonReader reader) + ref Utf8JsonReader reader, + ref ReadStack state) { bool lastFrame = state.IsLastFrame; @@ -99,7 +95,6 @@ namespace System.Text.Json.Serialization IEnumerable value = ReadStackFrame.GetEnumerableValue(state.Current); bool setPropertyDirectly; - bool popStackOnEnd = state.Current.PopStackOnEnd; if (state.Current.TempEnumerableValues != null) { @@ -109,11 +104,9 @@ namespace System.Text.Json.Serialization value = converter.CreateFromList(ref state, (IList)value, options); setPropertyDirectly = true; } - else if (!popStackOnEnd) + else if (state.Current.IsEnumerableProperty) { - Debug.Assert(state.Current.IsPropertyEnumerable); - - // We added the items to the list property already. + // We added the items to the list already. state.Current.ResetProperty(); return false; } @@ -122,11 +115,6 @@ namespace System.Text.Json.Serialization setPropertyDirectly = false; } - if (popStackOnEnd) - { - state.Pop(); - } - if (lastFrame) { if (state.Current.ReturnValue == null) @@ -143,12 +131,15 @@ namespace System.Text.Json.Serialization } // else there must be an outer object, so we'll return false here. } + else if (state.Current.IsEnumerable) + { + state.Pop(); + } - ApplyObjectToEnumerable(value, options, ref state, ref reader, setPropertyDirectly: setPropertyDirectly); + ApplyObjectToEnumerable(value, ref state, ref reader, setPropertyDirectly: setPropertyDirectly); - if (!popStackOnEnd) + if (state.Current.IsEnumerableProperty) { - Debug.Assert(state.Current.IsPropertyEnumerable); state.Current.ResetProperty(); } @@ -158,11 +149,12 @@ namespace System.Text.Json.Serialization // If this method is changed, also change ApplyValueToEnumerable. internal static void ApplyObjectToEnumerable( object value, - JsonSerializerOptions options, ref ReadStack state, ref Utf8JsonReader reader, bool setPropertyDirectly = false) { + Debug.Assert(!state.Current.SkipProperty); + if (state.Current.IsEnumerable) { if (state.Current.TempEnumerableValues != null) @@ -174,7 +166,7 @@ namespace System.Text.Json.Serialization ((IList)state.Current.ReturnValue).Add(value); } } - else if (!setPropertyDirectly && state.Current.IsPropertyEnumerable) + else if (!setPropertyDirectly && state.Current.IsEnumerableProperty) { Debug.Assert(state.Current.JsonPropertyInfo != null); Debug.Assert(state.Current.ReturnValue != null); @@ -189,10 +181,11 @@ namespace System.Text.Json.Serialization list.Add(value); } } - else if (state.Current.IsDictionary) + else if (state.Current.IsDictionary || (state.Current.IsDictionaryProperty && !setPropertyDirectly)) { Debug.Assert(state.Current.ReturnValue != null); IDictionary dictionary = (IDictionary)state.Current.JsonPropertyInfo.GetValueAsObject(state.Current.ReturnValue); + string key = state.Current.KeyName; Debug.Assert(!string.IsNullOrEmpty(key)); if (!dictionary.Contains(key)) @@ -214,10 +207,11 @@ namespace System.Text.Json.Serialization // If this method is changed, also change ApplyObjectToEnumerable. internal static void ApplyValueToEnumerable( ref TProperty value, - JsonSerializerOptions options, ref ReadStack state, ref Utf8JsonReader reader) { + Debug.Assert(!state.Current.SkipProperty); + if (state.Current.IsEnumerable) { if (state.Current.TempEnumerableValues != null) @@ -229,7 +223,7 @@ namespace System.Text.Json.Serialization ((IList)state.Current.ReturnValue).Add(value); } } - else if (state.Current.IsPropertyEnumerable) + else if (state.Current.IsEnumerableProperty) { Debug.Assert(state.Current.JsonPropertyInfo != null); Debug.Assert(state.Current.ReturnValue != null); @@ -244,7 +238,7 @@ namespace System.Text.Json.Serialization list.Add(value); } } - else if (state.Current.IsDictionary) + else if (state.Current.IsProcessingDictionary) { Debug.Assert(state.Current.ReturnValue != null); IDictionary dictionary = (IDictionary)state.Current.JsonPropertyInfo.GetValueAsObject(state.Current.ReturnValue); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleDictionary.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleDictionary.cs new file mode 100644 index 0000000..97540f5 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleDictionary.cs @@ -0,0 +1,95 @@ +// 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.Diagnostics; + +namespace System.Text.Json.Serialization +{ + public static partial class JsonSerializer + { + private static void HandleStartDictionary(JsonSerializerOptions options, ref Utf8JsonReader reader, ref ReadStack state) + { + Debug.Assert(!state.Current.IsProcessingEnumerable); + + JsonPropertyInfo jsonPropertyInfo = state.Current.JsonPropertyInfo; + if (jsonPropertyInfo == null) + { + jsonPropertyInfo = state.Current.JsonClassInfo.CreateRootObject(options); + } + + Debug.Assert(jsonPropertyInfo != null); + + // A nested object or dictionary so push new frame. + if (state.Current.PropertyInitialized) + { + Debug.Assert(state.Current.IsDictionary); + + JsonClassInfo classInfoTemp = state.Current.JsonClassInfo; + state.Push(); + state.Current.JsonClassInfo = classInfoTemp.ElementClassInfo; + state.Current.InitializeJsonPropertyInfo(); + + ClassType classType = state.Current.JsonClassInfo.ClassType; + if (classType == ClassType.Value && + jsonPropertyInfo.ElementClassInfo.Type != typeof(object) && + jsonPropertyInfo.ElementClassInfo.Type != typeof(JsonElement)) + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(state.Current.JsonClassInfo.Type, reader, state.PropertyPath); + } + + JsonClassInfo classInfo = state.Current.JsonClassInfo; + state.Current.ReturnValue = classInfo.CreateObject(); + return; + } + + state.Current.PropertyInitialized = true; + + // If current property is already set (from a constructor, for example) leave as-is. + if (jsonPropertyInfo.GetValueAsObject(state.Current.ReturnValue) == null) + { + // Create the dictionary. + JsonClassInfo dictionaryClassInfo = options.GetOrAddClass(jsonPropertyInfo.RuntimePropertyType); + IDictionary value = (IDictionary)dictionaryClassInfo.CreateObject(); + if (value != null) + { + if (state.Current.ReturnValue != null) + { + state.Current.JsonPropertyInfo.SetValueAsObject(state.Current.ReturnValue, value); + } + else + { + // A dictionary is being returned directly, or a nested dictionary. + state.Current.SetReturnValue(value); + } + } + } + } + + private static void HandleEndDictionary(JsonSerializerOptions options, ref Utf8JsonReader reader, ref ReadStack state) + { + if (state.Current.IsDictionaryProperty) + { + // We added the items to the dictionary already. + state.Current.ResetProperty(); + } + else + { + object value = state.Current.ReturnValue; + + if (state.IsLastFrame) + { + // Set the return value directly since this will be returned to the user. + state.Current.Reset(); + state.Current.ReturnValue = value; + } + else + { + state.Pop(); + ApplyObjectToEnumerable(value, ref state, ref reader); + } + } + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleNull.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleNull.cs index 7f7ecb1..b5b9e5f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleNull.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleNull.cs @@ -2,7 +2,6 @@ // 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.Diagnostics; namespace System.Text.Json.Serialization @@ -11,7 +10,7 @@ namespace System.Text.Json.Serialization { private static bool HandleNull(ref Utf8JsonReader reader, ref ReadStack state, JsonSerializerOptions options) { - if (state.Current.Skip()) + if (state.Current.SkipProperty) { return false; } @@ -32,14 +31,14 @@ namespace System.Text.Json.Serialization if (state.Current.IsEnumerable || state.Current.IsDictionary) { - ApplyObjectToEnumerable(null, options, ref state, ref reader); + ApplyObjectToEnumerable(null, ref state, ref reader); return false; } - if (state.Current.IsPropertyEnumerable) + if (state.Current.IsEnumerableProperty || state.Current.IsDictionaryProperty) { - bool setPropertyToNull = !state.Current.EnumerableCreated; - ApplyObjectToEnumerable(null, options, ref state, ref reader, setPropertyDirectly: setPropertyToNull); + bool setPropertyToNull = !state.Current.PropertyInitialized; + ApplyObjectToEnumerable(null, ref state, ref reader, setPropertyDirectly: setPropertyToNull); return false; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleObject.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleObject.cs index 425c4d7..b561568 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleObject.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleObject.cs @@ -10,89 +10,45 @@ namespace System.Text.Json.Serialization { private static void HandleStartObject(JsonSerializerOptions options, ref Utf8JsonReader reader, ref ReadStack state) { - if (state.Current.Skip()) - { - state.Push(); - state.Current.Drain = true; - return; - } + Debug.Assert(!state.Current.IsProcessingDictionary); if (state.Current.IsProcessingEnumerable) { + // A nested object within an enumerable. Type objType = state.Current.GetElementType(); state.Push(); state.Current.Initialize(objType, options); } else if (state.Current.JsonPropertyInfo != null) { - if (state.Current.IsDictionary) - { - // Verify that the Dictionary can be deserialized by having as first generic argument. - Type[] args = state.Current.JsonClassInfo.Type.GetGenericArguments(); - if (args.Length == 0 || args[0].UnderlyingSystemType != typeof(string)) - { - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(state.Current.JsonClassInfo.Type, reader, state.PropertyPath); - } - - if (state.Current.ReturnValue == null) - { - // The Dictionary created below will be returned to corresponding Parse() etc method. - // Ensure any nested array creates a new frame. - state.Current.EnumerableCreated = true; - } - else - { - ClassType classType = state.Current.JsonClassInfo.ElementClassInfo.ClassType; - - // Verify that the second parameter is not a value. - if (state.Current.JsonClassInfo.ElementClassInfo.ClassType == ClassType.Value) - { - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(state.Current.JsonClassInfo.Type, reader, state.PropertyPath); - } - - // A nested object, dictionary or enumerable. - JsonClassInfo classInfoTemp = state.Current.JsonClassInfo; - state.Push(); - state.Current.JsonClassInfo = classInfoTemp.ElementClassInfo; - state.Current.InitializeJsonPropertyInfo(); - } - } - else - { - // Nested object. - Type objType = state.Current.JsonPropertyInfo.RuntimePropertyType; - state.Push(); - state.Current.Initialize(objType, options); - } + // Nested object. + Type objType = state.Current.JsonPropertyInfo.RuntimePropertyType; + state.Push(); + state.Current.Initialize(objType, options); } JsonClassInfo classInfo = state.Current.JsonClassInfo; state.Current.ReturnValue = classInfo.CreateObject(); } - private static bool HandleEndObject(JsonSerializerOptions options, ref ReadStack state, ref Utf8JsonReader reader) + private static void HandleEndObject(JsonSerializerOptions options, ref Utf8JsonReader reader, ref ReadStack state) { - bool isLastFrame = state.IsLastFrame; - if (state.Current.Drain) - { - state.Pop(); - return isLastFrame; - } + Debug.Assert(!state.Current.IsProcessingDictionary); state.Current.JsonClassInfo.UpdateSortedPropertyCache(ref state.Current); object value = state.Current.ReturnValue; - if (isLastFrame) + if (state.IsLastFrame) { state.Current.Reset(); state.Current.ReturnValue = value; - return true; } - - state.Pop(); - ApplyObjectToEnumerable(value, options, ref state, ref reader); - return false; + else + { + state.Pop(); + ApplyObjectToEnumerable(value, ref state, ref reader); + } } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs new file mode 100644 index 0000000..e7b0ff2 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs @@ -0,0 +1,131 @@ +// 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.Buffers; +using System.Collections; +using System.Diagnostics; + +namespace System.Text.Json.Serialization +{ + public static partial class JsonSerializer + { + private static void HandlePropertyName( + JsonSerializerOptions options, + ref Utf8JsonReader reader, + ref ReadStack state) + { + if (state.Current.Drain) + { + return; + } + + Debug.Assert(state.Current.ReturnValue != default); + Debug.Assert(state.Current.JsonClassInfo != default); + + if (state.Current.IsProcessingDictionary) + { + if (ReferenceEquals(state.Current.JsonClassInfo.DataExtensionProperty, state.Current.JsonPropertyInfo)) + { + ReadOnlySpan propertyName = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; + + // todo: use a cleaner call to get the unescaped string once https://github.com/dotnet/corefx/issues/35386 is implemented. + if (reader._stringHasEscaping) + { + int idx = propertyName.IndexOf(JsonConstants.BackSlash); + Debug.Assert(idx != -1); + propertyName = GetUnescapedString(propertyName, idx); + } + + ProcessMissingProperty(propertyName, options, ref reader, ref state); + } + else + { + string keyName = reader.GetString(); + if (options.DictionaryKeyPolicy != null) + { + keyName = options.DictionaryKeyPolicy.ConvertName(keyName); + } + + if (state.Current.IsDictionary) + { + state.Current.JsonPropertyInfo = state.Current.JsonClassInfo.GetPolicyProperty(); + } + + Debug.Assert(state.Current.IsDictionary || + (state.Current.IsDictionaryProperty && state.Current.JsonPropertyInfo != null)); + + state.Current.KeyName = keyName; + } + } + else + { + ReadOnlySpan propertyName = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; + if (reader._stringHasEscaping) + { + int idx = propertyName.IndexOf(JsonConstants.BackSlash); + Debug.Assert(idx != -1); + propertyName = GetUnescapedString(propertyName, idx); + } + + state.Current.JsonPropertyInfo = state.Current.JsonClassInfo.GetProperty(options, propertyName, ref state.Current); + if (state.Current.JsonPropertyInfo == null) + { + if (state.Current.JsonClassInfo.DataExtensionProperty == null) + { + state.Current.JsonPropertyInfo = JsonPropertyInfo.s_missingProperty; + } + else + { + ProcessMissingProperty(propertyName, options, ref reader, ref state); + } + } + else + { + state.Current.PropertyIndex++; + } + } + } + + private static void ProcessMissingProperty( + ReadOnlySpan unescapedPropertyName, + JsonSerializerOptions options, + ref Utf8JsonReader reader, + ref ReadStack state) + { + JsonPropertyInfo jsonPropertyInfo = state.Current.JsonClassInfo.DataExtensionProperty; + + Debug.Assert(jsonPropertyInfo != null); + Debug.Assert(state.Current.ReturnValue != null); + + IDictionary extensionData = (IDictionary)jsonPropertyInfo.GetValueAsObject(state.Current.ReturnValue); + if (extensionData == null) + { + Type type = jsonPropertyInfo.DeclaredPropertyType; + + // Create the appropriate dictionary type. We already verified the types. + Debug.Assert(type.IsGenericType); + Debug.Assert(type.GetGenericArguments().Length == 2); + Debug.Assert(type.GetGenericArguments()[0].UnderlyingSystemType == typeof(string)); + Debug.Assert( + type.GetGenericArguments()[1].UnderlyingSystemType == typeof(object) || + type.GetGenericArguments()[1].UnderlyingSystemType == typeof(JsonElement)); + + extensionData = (IDictionary)options.GetOrAddClass(type).CreateObject(); + jsonPropertyInfo.SetValueAsObject(state.Current.ReturnValue, extensionData); + } + + JsonElement jsonElement; + using (JsonDocument jsonDocument = JsonDocument.ParseValue(ref reader)) + { + jsonElement = jsonDocument.RootElement.Clone(); + } + + string keyName = JsonHelpers.Utf8GetString(unescapedPropertyName); + + // Currently we don't apply any naming policy. If we do, we'd have to pass it onto the JsonDocument. + + extensionData.Add(keyName, jsonElement); + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleValue.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleValue.cs index 15e9d98..a113129 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleValue.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleValue.cs @@ -8,7 +8,7 @@ namespace System.Text.Json.Serialization { private static bool HandleValue(JsonTokenType tokenType, JsonSerializerOptions options, ref Utf8JsonReader reader, ref ReadStack state) { - if (state.Current.Skip()) + if (state.Current.SkipProperty) { return false; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.cs index 59aa8e6..f95f24b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Buffers; +using System.Collections; using System.Diagnostics; namespace System.Text.Json.Serialization @@ -13,9 +14,6 @@ namespace System.Text.Json.Serialization /// public static partial class JsonSerializer { - internal static readonly JsonPropertyInfo s_missingProperty = new JsonPropertyInfoNotNullable(); - - // todo: for readability, refactor this method to split by ClassType(Enumerable, Object, or Value) like Write() private static void ReadCore( JsonSerializerOptions options, ref Utf8JsonReader reader, @@ -38,63 +36,49 @@ namespace System.Text.Json.Serialization } else if (tokenType == JsonTokenType.PropertyName) { - if (!state.Current.Drain) + HandlePropertyName(options, ref reader, ref state); + } + else if (tokenType == JsonTokenType.StartObject) + { + if (state.Current.SkipProperty) { - Debug.Assert(state.Current.ReturnValue != default); - Debug.Assert(state.Current.JsonClassInfo != default); - - if (state.Current.IsDictionary) - { - string keyName = reader.GetString(); - if (options.DictionaryKeyPolicy != null) - { - keyName = options.DictionaryKeyPolicy.ConvertName(keyName); - } - - state.Current.JsonPropertyInfo = state.Current.JsonClassInfo.GetPolicyProperty(); - state.Current.KeyName = keyName; - } - else + state.Push(); + state.Current.Drain = true; + } + else if (state.Current.IsProcessingValue) + { + if (HandleValue(tokenType, options, ref reader, ref state)) { - ReadOnlySpan propertyName = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; - if (reader._stringHasEscaping) - { - int idx = propertyName.IndexOf(JsonConstants.BackSlash); - Debug.Assert(idx != -1); - propertyName = GetUnescapedString(propertyName, idx); - } - - state.Current.JsonPropertyInfo = state.Current.JsonClassInfo.GetProperty(options, propertyName, ref state.Current); - if (state.Current.JsonPropertyInfo == null) - { - state.Current.JsonPropertyInfo = s_missingProperty; - } - - state.Current.PropertyIndex++; + continue; } } - } - else if (tokenType == JsonTokenType.StartObject) - { - if (!state.Current.IsProcessingProperty) + else if (state.Current.IsProcessingDictionary) { - HandleStartObject(options, ref reader, ref state); + HandleStartDictionary(options, ref reader, ref state); } - else if (HandleValue(tokenType, options, ref reader, ref state)) + else { - continue; + HandleStartObject(options, ref reader, ref state); } } else if (tokenType == JsonTokenType.EndObject) { - if (HandleEndObject(options, ref state, ref reader)) + if (state.Current.Drain) { - continue; + state.Pop(); + } + else if (state.Current.IsProcessingDictionary) + { + HandleEndDictionary(options, ref reader, ref state); + } + else + { + HandleEndObject(options, ref reader, ref state); } } else if (tokenType == JsonTokenType.StartArray) { - if (!state.Current.IsProcessingProperty) + if (!state.Current.IsProcessingValue) { HandleStartArray(options, ref reader, ref state); } @@ -105,7 +89,7 @@ namespace System.Text.Json.Serialization } else if (tokenType == JsonTokenType.EndArray) { - if (HandleEndArray(options, ref state, ref reader)) + if (HandleEndArray(options, ref reader, ref state)) { continue; } 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 b9b2864..3ffe079 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 @@ -2,7 +2,6 @@ // 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.Buffers; using System.Collections; using System.Collections.Generic; using System.Diagnostics; @@ -19,22 +18,11 @@ namespace System.Text.Json.Serialization ref WriteStack state) { JsonPropertyInfo jsonPropertyInfo = state.Current.JsonPropertyInfo; - if (!jsonPropertyInfo.ShouldSerialize) - { - // Ignore writing this property. - return true; - } - if (state.Current.Enumerator == null) { - // Verify that the Dictionary can be serialized by having as first generic argument. - Type[] args = jsonPropertyInfo.RuntimePropertyType.GetGenericArguments(); - if (args.Length == 0 || args[0].UnderlyingSystemType != typeof(string)) - { - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(state.Current.JsonClassInfo.Type, state.PropertyPath); - } + IEnumerable enumerable; - IEnumerable enumerable = (IEnumerable)jsonPropertyInfo.GetValueAsObject(state.Current.CurrentValue); + enumerable = (IEnumerable)jsonPropertyInfo.GetValueAsObject(state.Current.CurrentValue); if (enumerable == null) { // Write a null object or enumerable. @@ -48,28 +36,36 @@ namespace System.Text.Json.Serialization if (state.Current.Enumerator.MoveNext()) { - // Check for polymorphism. - if (elementClassInfo.ClassType == ClassType.Unknown) - { - object currentValue = ((IDictionaryEnumerator)state.Current.Enumerator).Entry.Value; - GetRuntimeClassInfo(currentValue, ref elementClassInfo, options); - } - - if (elementClassInfo.ClassType == ClassType.Value) - { - elementClassInfo.GetPolicyProperty().WriteDictionary(options, ref state.Current, writer); - } - else if (state.Current.Enumerator.Current == null) + // Handle DataExtension. + if (ReferenceEquals(jsonPropertyInfo, state.Current.JsonClassInfo.DataExtensionProperty)) { - writer.WriteNull(jsonPropertyInfo.Name); + WriteExtensionData(writer, ref state.Current); } else { - // An object or another enumerator requires a new stack frame. - var enumerator = (IDictionaryEnumerator)state.Current.Enumerator; - object value = enumerator.Value; - state.Push(elementClassInfo, value); - state.Current.KeyName = (string)enumerator.Key; + // Check for polymorphism. + if (elementClassInfo.ClassType == ClassType.Unknown) + { + object currentValue = ((IDictionaryEnumerator)state.Current.Enumerator).Entry.Value; + GetRuntimeClassInfo(currentValue, ref elementClassInfo, options); + } + + if (elementClassInfo.ClassType == ClassType.Value) + { + elementClassInfo.GetPolicyProperty().WriteDictionary(options, ref state.Current, writer); + } + else if (state.Current.Enumerator.Current == null) + { + writer.WriteNull(jsonPropertyInfo.Name); + } + else + { + // An object or another enumerator requires a new stack frame. + var enumerator = (IDictionaryEnumerator)state.Current.Enumerator; + object value = enumerator.Value; + state.Push(elementClassInfo, value); + state.Current.KeyName = (string)enumerator.Key; + } } return false; @@ -161,5 +157,21 @@ namespace System.Text.Json.Serialization #endif } } + + private static void WriteExtensionData(Utf8JsonWriter writer, ref WriteStackFrame frame) + { + DictionaryEntry entry = ((IDictionaryEnumerator)frame.Enumerator).Entry; + if (entry.Value is JsonElement element) + { + Debug.Assert(entry.Key is string); + + string propertyName = (string)entry.Key; + element.WriteAsProperty(propertyName.AsSpan(), writer); + } + else + { + ThrowHelper.ThrowInvalidOperationException_SerializationDataExtensionPropertyInvalid(frame.JsonClassInfo, entry.Value.GetType()); + } + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleEnumerable.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleEnumerable.cs index 6f51097..4be8b9f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleEnumerable.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleEnumerable.cs @@ -17,16 +17,9 @@ namespace System.Text.Json.Serialization { Debug.Assert(state.Current.JsonPropertyInfo.ClassType == ClassType.Enumerable); - JsonPropertyInfo jsonPropertyInfo = state.Current.JsonPropertyInfo; - if (!jsonPropertyInfo.ShouldSerialize) - { - // Ignore writing this property. - return true; - } - if (state.Current.Enumerator == null) { - IEnumerable enumerable = (IEnumerable)jsonPropertyInfo.GetValueAsObject(state.Current.CurrentValue); + IEnumerable enumerable = (IEnumerable)state.Current.JsonPropertyInfo.GetValueAsObject(state.Current.CurrentValue); if (enumerable == null) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleObject.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleObject.cs index 1daa62d..f60932e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleObject.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleObject.cs @@ -13,8 +13,6 @@ namespace System.Text.Json.Serialization Utf8JsonWriter writer, ref WriteStack state) { - JsonClassInfo classInfo = state.Current.JsonClassInfo; - // Write the start. if (!state.Current.StartObjectWritten) { @@ -24,6 +22,7 @@ namespace System.Text.Json.Serialization // Determine if we are done enumerating properties. // If the ClassType is unknown, there will be a policy property applied. There is probably // a better way to identify policy properties- maybe not put them in the normal property bag? + JsonClassInfo classInfo = state.Current.JsonClassInfo; if (classInfo.ClassType != ClassType.Unknown && state.Current.PropertyIndex != classInfo.PropertyCount) { HandleObject(options, writer, ref state); @@ -54,6 +53,11 @@ namespace System.Text.Json.Serialization state.Current.JsonClassInfo.ClassType == ClassType.Unknown); JsonPropertyInfo jsonPropertyInfo = state.Current.JsonClassInfo.GetProperty(state.Current.PropertyIndex); + if (!jsonPropertyInfo.ShouldSerialize) + { + state.Current.NextProperty(); + return true; + } bool obtainedValue = false; object currentValue = null; @@ -122,7 +126,7 @@ namespace System.Text.Json.Serialization { if (!jsonPropertyInfo.IgnoreNullValues) { - writer.WriteNull(jsonPropertyInfo._escapedName); + writer.WriteNull(jsonPropertyInfo.EscapedName); } state.Current.NextProperty(); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs index 650db0a..73c735e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs @@ -7,7 +7,7 @@ using System.Diagnostics; namespace System.Text.Json.Serialization { - [DebuggerDisplay("Current: ClassType.{Current.JsonClassInfo.ClassType} {Current.JsonClassInfo.Type.Name}")] + [DebuggerDisplay("Current: ClassType.{Current.JsonClassInfo.ClassType}, {Current.JsonClassInfo.Type.Name}")] internal struct ReadStack { // A fields is used instead of a property to avoid value semantics. @@ -83,7 +83,7 @@ namespace System.Text.Json.Serialization private string GetPropertyName(in ReadStackFrame frame) { - if (frame.JsonPropertyInfo != null && frame.JsonClassInfo.ClassType == ClassType.Object) + if (frame.JsonPropertyInfo?.PropertyInfo != null && frame.JsonClassInfo.ClassType == ClassType.Object) { return $".{frame.JsonPropertyInfo.PropertyInfo.Name}"; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs index bab6bcb..7f3e4d3 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs @@ -9,7 +9,7 @@ using System.Runtime.CompilerServices; namespace System.Text.Json.Serialization { - [DebuggerDisplay("ClassType.{JsonClassInfo.ClassType} {JsonClassInfo.Type.Name}")] + [DebuggerDisplay("ClassType.{JsonClassInfo.ClassType}, {JsonClassInfo.Type.Name}")] internal struct ReadStackFrame { // The object (POCO or IEnumerable) that is being populated @@ -22,12 +22,11 @@ namespace System.Text.Json.Serialization // Current property values. public JsonPropertyInfo JsonPropertyInfo; - // Pop the stack when the current array or dictionary is done. - public bool PopStackOnEnd; - // Support System.Array and other types that don't implement IList. public IList TempEnumerableValues; - public bool EnumerableCreated; + + // Has an array or dictionary property been initialized. + public bool PropertyInitialized; // For performance, we order the properties by the first deserialize and PropertyIndex helps find the right slot quicker. public int PropertyIndex; @@ -37,17 +36,28 @@ namespace System.Text.Json.Serialization public bool Drain; public bool IsDictionary => JsonClassInfo.ClassType == ClassType.Dictionary; + + public bool IsDictionaryProperty => JsonPropertyInfo != null && + !JsonPropertyInfo.IsPropertyPolicy && + JsonPropertyInfo.ClassType == ClassType.Dictionary; + public bool IsEnumerable => JsonClassInfo.ClassType == ClassType.Enumerable; - public bool IsProcessingEnumerableOrDictionary => IsProcessingEnumerable || IsDictionary; - public bool IsProcessingEnumerable => IsEnumerable || IsPropertyEnumerable; - public bool IsPropertyEnumerable => JsonPropertyInfo != null ? JsonPropertyInfo.ClassType == ClassType.Enumerable : false; - public bool IsProcessingProperty + public bool IsEnumerableProperty => + JsonPropertyInfo != null && + !JsonPropertyInfo.IsPropertyPolicy && + JsonPropertyInfo.ClassType == ClassType.Enumerable; + + public bool IsProcessingEnumerableOrDictionary => IsProcessingEnumerable || IsProcessingDictionary; + public bool IsProcessingDictionary => IsDictionary || IsDictionaryProperty; + public bool IsProcessingEnumerable => IsEnumerable || IsEnumerableProperty; + + public bool IsProcessingValue { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - if (JsonPropertyInfo == null || Skip()) + if (JsonPropertyInfo == null || SkipProperty) { return false; } @@ -55,8 +65,11 @@ namespace System.Text.Json.Serialization // We've got a property info. If we're a Value or polymorphic Value // (ClassType.Unknown), return true. ClassType type = JsonPropertyInfo.ClassType; - return type == ClassType.Value || type == ClassType.Unknown - || (type == ClassType.Dictionary && KeyName != null && JsonClassInfo.ElementClassInfo.ClassType == ClassType.Unknown); + return type == ClassType.Value || type == ClassType.Unknown || + KeyName != null && ( + (IsDictionary && JsonClassInfo.ElementClassInfo.ClassType == ClassType.Unknown) || + (IsDictionaryProperty && JsonPropertyInfo.ElementClassInfo.ClassType == ClassType.Unknown) + ); } } @@ -86,9 +99,8 @@ namespace System.Text.Json.Serialization public void ResetProperty() { - EnumerableCreated = false; + PropertyInitialized = false; JsonPropertyInfo = null; - PopStackOnEnd = false; TempEnumerableValues = null; } @@ -112,7 +124,7 @@ namespace System.Text.Json.Serialization } else { - converterList = new List(); + converterList = new List(); } state.Current.TempEnumerableValues = converterList; @@ -137,17 +149,12 @@ namespace System.Text.Json.Serialization public Type GetElementType() { - if (IsPropertyEnumerable) + if (IsEnumerableProperty || IsDictionaryProperty) { return JsonPropertyInfo.ElementClassInfo.Type; } - if (IsEnumerable) - { - return JsonClassInfo.ElementClassInfo.Type; - } - - if (IsDictionary) + if (IsEnumerable || IsDictionary) { return JsonClassInfo.ElementClassInfo.Type; } @@ -175,9 +182,8 @@ namespace System.Text.Json.Serialization ReturnValue = value; } - public bool Skip() - { - return Drain || ReferenceEquals(JsonPropertyInfo, JsonSerializer.s_missingProperty); - } + public bool SkipProperty => Drain || + ReferenceEquals(JsonPropertyInfo, JsonPropertyInfo.s_missingProperty) || + (JsonPropertyInfo?.IsPropertyPolicy == false && JsonPropertyInfo?.ShouldDeserialize == false); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs index 82f823b..660bb44 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs @@ -46,9 +46,9 @@ namespace System.Text.Json.Serialization public void WriteObjectOrArrayStart(ClassType classType, Utf8JsonWriter writer, bool writeNull = false) { - if (JsonPropertyInfo?._escapedName != null) + if (JsonPropertyInfo?.EscapedName != null) { - WriteObjectOrArrayStart(classType, JsonPropertyInfo?._escapedName, writer, writeNull); + WriteObjectOrArrayStart(classType, JsonPropertyInfo?.EscapedName, writer, writeNull); } else if (KeyName != null) { 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 6bf5b59..2f69736 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 @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Diagnostics; +using System.Reflection; using System.Runtime.CompilerServices; using System.Text.Json.Serialization; @@ -13,19 +14,30 @@ namespace System.Text.Json [MethodImpl(MethodImplOptions.NoInlining)] public static void ThrowArgumentException_DeserializeWrongType(Type type, object value) { - throw new ArgumentException(SR.Format(SR.DeserializeWrongType, type.FullName, value.GetType().FullName)); + throw new ArgumentException(SR.Format(SR.DeserializeWrongType, type, value.GetType())); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static NotSupportedException GetNotSupportedException_SerializationNotSupportedCollection(Type propertyType, Type parentType, MemberInfo memberInfo) + { + if (parentType != null && parentType != typeof(object) && memberInfo != null) + { + return new NotSupportedException(SR.Format(SR.SerializationNotSupportedCollection, propertyType, $"{parentType}.{memberInfo.Name}")); + } + + return new NotSupportedException(SR.Format(SR.SerializationNotSupportedCollectionType, propertyType)); } [MethodImpl(MethodImplOptions.NoInlining)] public static void ThrowJsonException_DeserializeUnableToConvertValue(Type propertyType, in Utf8JsonReader reader, string path) { - ThowJsonException(SR.Format(SR.DeserializeUnableToConvertValue, propertyType.FullName), in reader, path); + ThowJsonException(SR.Format(SR.DeserializeUnableToConvertValue, propertyType), in reader, path); } [MethodImpl(MethodImplOptions.NoInlining)] public static void ThrowJsonException_DeserializeUnableToConvertValue(Type propertyType, string path) { - string message = SR.Format(SR.DeserializeUnableToConvertValue, propertyType.FullName) + $" Path: {path}."; + string message = SR.Format(SR.DeserializeUnableToConvertValue, propertyType) + $" Path: {path}."; throw new JsonException(message, path, null, null); } @@ -44,13 +56,13 @@ namespace System.Text.Json [MethodImpl(MethodImplOptions.NoInlining)] public static void ThrowInvalidOperationException_SerializerPropertyNameConflict(JsonClassInfo jsonClassInfo, JsonPropertyInfo jsonPropertyInfo) { - throw new InvalidOperationException(SR.Format(SR.SerializerPropertyNameConflict, jsonClassInfo.Type.FullName, jsonPropertyInfo.PropertyInfo.Name)); + throw new InvalidOperationException(SR.Format(SR.SerializerPropertyNameConflict, jsonClassInfo.Type, jsonPropertyInfo.PropertyInfo.Name)); } [MethodImpl(MethodImplOptions.NoInlining)] - public static void ThrowInvalidOperationException_SerializerPropertyNameNull(JsonClassInfo jsonClassInfo, JsonPropertyInfo jsonPropertyInfo) + public static void ThrowInvalidOperationException_SerializerPropertyNameNull(Type parentType, JsonPropertyInfo jsonPropertyInfo) { - throw new InvalidOperationException(SR.Format(SR.SerializerPropertyNameNull, jsonClassInfo.Type.FullName, jsonPropertyInfo.PropertyInfo.Name)); + throw new InvalidOperationException(SR.Format(SR.SerializerPropertyNameNull, parentType, jsonPropertyInfo.PropertyInfo.Name)); } public static void ThrowJsonException_DeserializeDataRemaining(long length, long bytesRemaining) @@ -92,5 +104,23 @@ namespace System.Text.Json throw new JsonException(message, path, exception.LineNumber, exception.BytePositionInLine, exception); } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowInvalidOperationException_SerializationDuplicateAttribute(Type attribute) + { + throw new InvalidOperationException(SR.Format(SR.SerializationDuplicateAttribute, attribute)); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowInvalidOperationException_SerializationDataExtensionPropertyInvalid(JsonClassInfo jsonClassInfo, JsonPropertyInfo jsonPropertyInfo) + { + throw new InvalidOperationException(SR.Format(SR.SerializationDataExtensionPropertyInvalid, jsonClassInfo.Type, jsonPropertyInfo.PropertyInfo.Name)); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowInvalidOperationException_SerializationDataExtensionPropertyInvalid(JsonClassInfo jsonClassInfo, Type invalidType) + { + throw new InvalidOperationException(SR.Format(SR.SerializationDataExtensionPropertyInvalidElement, jsonClassInfo.Type, jsonClassInfo.DataExtensionProperty.PropertyInfo.Name, invalidType)); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs index a69e504..5a93117 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs @@ -225,7 +225,7 @@ namespace System.Text.Json [MethodImpl(MethodImplOptions.NoInlining)] public static JsonException GetJsonReaderException(ref Utf8JsonReader json, ExceptionResource resource, byte nextByte, ReadOnlySpan bytes) { - string message = GetResourceString(ref json, resource, nextByte, Encoding.UTF8.GetString(bytes.ToArray(), 0, bytes.Length)); + string message = GetResourceString(ref json, resource, nextByte, JsonHelpers.Utf8GetString(bytes)); long lineNumber = json.CurrentState._lineNumber; long bytePositionInLine = json.CurrentState._bytePositionInLine; diff --git a/src/libraries/System.Text.Json/tests/Serialization/Array.ReadTests.cs b/src/libraries/System.Text.Json/tests/Serialization/Array.ReadTests.cs index 29458f2..b30e8de 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/Array.ReadTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/Array.ReadTests.cs @@ -37,18 +37,6 @@ namespace System.Text.Json.Serialization.Tests } } - private static void VerifyReadNull(SimpleTestClass obj, bool isNull) - { - if (isNull) - { - Assert.Null(obj); - } - else - { - obj.Verify(); - } - } - [Theory] [MemberData(nameof(ReadNullJson))] public static void ReadNull(string json, bool element0Null, bool element1Null, bool element2Null) @@ -64,6 +52,18 @@ namespace System.Text.Json.Serialization.Tests VerifyReadNull(list[0], element0Null); VerifyReadNull(list[1], element1Null); VerifyReadNull(list[2], element2Null); + + static void VerifyReadNull(SimpleTestClass obj, bool isNull) + { + if (isNull) + { + Assert.Null(obj); + } + else + { + obj.Verify(); + } + } } [Fact] @@ -177,5 +177,17 @@ namespace System.Text.Json.Serialization.Tests TestClassWithObjectImmutableTypes obj = JsonSerializer.Parse(TestClassWithObjectImmutableTypes.s_data); obj.Verify(); } + + public static void ClassWithNoSetter() + { + string json = @"{""MyList"":[1]}"; + ClassWithListButNoSetter obj = JsonSerializer.Parse(json); + Assert.Equal(1, obj.MyList[0]); + } + + public class ClassWithListButNoSetter + { + public List MyList { get; } = new List(); + } } } diff --git a/src/libraries/System.Text.Json/tests/Serialization/Array.WriteTests.cs b/src/libraries/System.Text.Json/tests/Serialization/Array.WriteTests.cs index 2926d4b..b1e7e77 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/Array.WriteTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/Array.WriteTests.cs @@ -65,8 +65,6 @@ namespace System.Text.Json.Serialization.Tests } } - - [Fact] public static void WriteClassWithGenericList() { diff --git a/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs b/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs index 7f99c2b..4e80a3a 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs @@ -103,7 +103,7 @@ namespace System.Text.Json.Serialization.Tests [Fact] public static void FirstGenericArgNotStringFail() { - Assert.Throws(() => JsonSerializer.Parse>(@"{""Key1"":1}")); + Assert.Throws(() => JsonSerializer.Parse>(@"{""Key1"":1}")); } [Fact] @@ -301,10 +301,20 @@ namespace System.Text.Json.Serialization.Tests [Fact] public static void ObjectToStringFail() { + // Baseline string json = @"{""MyDictionary"":{""Key"":""Value""}}"; + JsonSerializer.Parse>(json); + Assert.Throws(() => JsonSerializer.Parse>(json)); } + [Fact, ActiveIssue("JsonElement fails since it is a struct.")] + public static void ObjectToJsonElement() + { + string json = @"{""MyDictionary"":{""Key"":""Value""}}"; + JsonSerializer.Parse>(json); + } + [Fact] public static void HashtableFail() { @@ -315,27 +325,77 @@ namespace System.Text.Json.Serialization.Tests JsonSerializer.Parse>(json); // We don't support non-generic IDictionary - Assert.Throws(() => JsonSerializer.Parse(json)); + Assert.Throws(() => JsonSerializer.Parse(json)); } { Hashtable ht = new Hashtable(); ht.Add("Key", "Value"); - Assert.Throws(() => JsonSerializer.ToString(ht)); + + Assert.Throws(() => JsonSerializer.ToString(ht)); } { string json = @"{""Key"":""Value""}"; // We don't support non-generic IDictionary - Assert.Throws(() => JsonSerializer.Parse(json)); + Assert.Throws(() => JsonSerializer.Parse(json)); } { IDictionary ht = new Hashtable(); ht.Add("Key", "Value"); - Assert.Throws(() => JsonSerializer.ToString(ht)); + Assert.Throws(() => JsonSerializer.ToString(ht)); + } + } + + [Fact] + public static void ClassWithNoSetter() + { + string json = @"{""MyDictionary"":{""Key"":""Value""}}"; + ClassWithDictionaryButNoSetter obj = JsonSerializer.Parse(json); + Assert.Equal("Value", obj.MyDictionary["Key"]); + } + + [Fact] + public static void DictionaryNotSupported() + { + string json = @"{""MyDictionary"":{""Key"":""Value""}}"; + + try + { + JsonSerializer.Parse(json); + Assert.True(false, "Expected NotSupportedException to be thrown."); } + catch (NotSupportedException e) + { + // The exception should contain className.propertyName and the invalid type. + Assert.Contains("ClassWithNotSupportedDictionary.MyDictionary", e.Message); + Assert.Contains("Dictionary`2[System.Int32,System.Int32]", e.Message); + } + } + + [Fact] + public static void DictionaryNotSupportedButIgnored() + { + string json = @"{""MyDictionary"":{""Key"":1}}"; + ClassWithNotSupportedDictionaryButIgnored obj = JsonSerializer.Parse(json); + Assert.Null(obj.MyDictionary); + } + + public class ClassWithDictionaryButNoSetter + { + public Dictionary MyDictionary { get; } = new Dictionary(); + } + + public class ClassWithNotSupportedDictionary + { + public Dictionary MyDictionary { get; set; } + } + + public class ClassWithNotSupportedDictionaryButIgnored + { + [JsonIgnore] public Dictionary MyDictionary { get; set; } } } } diff --git a/src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs b/src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs new file mode 100644 index 0000000..c45ed4e --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs @@ -0,0 +1,266 @@ +// 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.Generic; +using System.Linq; +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public static class ExtensionDataTests + { + [Fact] + public static void ExtensionPropertyNotUsed() + { + string json = @"{""MyNestedClass"":" + SimpleTestClass.s_json + "}"; + ClassWithExtensionProperty obj = JsonSerializer.Parse(json); + Assert.Null(obj.MyOverflow); + } + + [Fact] + public static void ExtensionPropertyRoundTrip() + { + ClassWithExtensionProperty obj; + + { + string json = @"{""MyIntMissing"":2, ""MyInt"":1, ""MyNestedClassMissing"":" + SimpleTestClass.s_json + "}"; + obj = JsonSerializer.Parse(json); + Verify(); + } + + // Round-trip the json. + { + string json = JsonSerializer.ToString(obj); + obj = JsonSerializer.Parse(json); + Verify(); + } + + void Verify() + { + Assert.NotNull(obj.MyOverflow); + Assert.NotNull(obj.MyOverflow["MyIntMissing"]); + Assert.Equal(1, obj.MyInt); + Assert.Equal(2, obj.MyOverflow["MyIntMissing"].GetInt32()); + + JsonProperty[] properties = obj.MyOverflow["MyNestedClassMissing"].EnumerateObject().ToArray(); + + // Verify a couple properties + Assert.Equal(1, properties.Where(prop => prop.Name == "MyInt16").First().Value.GetInt32()); + Assert.Equal(true, properties.Where(prop => prop.Name == "MyBooleanTrue").First().Value.GetBoolean()); + } + } + + [Fact] + public static void ExtensionPropertyAlreadyInstantiated() + { + Assert.NotNull(new ClassWithExtensionPropertyAlreadyInstantiated().MyOverflow); + + string json = @"{""MyIntMissing"":2}"; + + ClassWithExtensionProperty obj = JsonSerializer.Parse(json); + Assert.Equal(2, obj.MyOverflow["MyIntMissing"].GetInt32()); + } + + [Fact] + public static void ExtensionPropertyAsObject() + { + string json = @"{""MyIntMissing"":2}"; + + ClassWithExtensionPropertyAsObject obj = JsonSerializer.Parse(json); + Assert.IsType(obj.MyOverflow["MyIntMissing"]); + Assert.Equal(2, ((JsonElement)obj.MyOverflow["MyIntMissing"]).GetInt32()); + } + + [Fact] + public static void ExtensionPropertyCamelCasing() + { + // Currently we apply no naming policy. If we do (such as a ExtensionPropertyNamingPolicy), we'd also have to add functionality to the JsonDocument. + + ClassWithExtensionProperty obj; + const string jsonWithProperty = @"{""MyIntMissing"":1}"; + const string jsonWithPropertyCamelCased = @"{""myIntMissing"":1}"; + + { + // Baseline Pascal-cased json + no casing option. + obj = JsonSerializer.Parse(jsonWithProperty); + Assert.Equal(1, obj.MyOverflow["MyIntMissing"].GetInt32()); + string json = JsonSerializer.ToString(obj); + Assert.Contains(@"""MyIntMissing"":1", json); + } + + { + // Pascal-cased json + camel casing option. + JsonSerializerOptions options = new JsonSerializerOptions(); + options.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase; + options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + + obj = JsonSerializer.Parse(jsonWithProperty, options); + Assert.Equal(1, obj.MyOverflow["MyIntMissing"].GetInt32()); + string json = JsonSerializer.ToString(obj); + Assert.Contains(@"""MyIntMissing"":1", json); + } + + { + // Baseline camel-cased json + no casing option. + obj = JsonSerializer.Parse(jsonWithPropertyCamelCased); + Assert.Equal(1, obj.MyOverflow["myIntMissing"].GetInt32()); + string json = JsonSerializer.ToString(obj); + Assert.Contains(@"""myIntMissing"":1", json); + } + + { + // Baseline camel-cased json + camel casing option. + JsonSerializerOptions options = new JsonSerializerOptions(); + options.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase; + options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + + obj = JsonSerializer.Parse(jsonWithPropertyCamelCased, options); + Assert.Equal(1, obj.MyOverflow["myIntMissing"].GetInt32()); + string json = JsonSerializer.ToString(obj); + Assert.Contains(@"""myIntMissing"":1", json); + } + } + + [Fact] + public static void NullValuesIgnored() + { + const string json = @"{""MyNestedClass"":null}"; + const string jsonMissing = @"{ ""MyNestedClassMissing"":null}"; + + { + // Baseline with no missing. + ClassWithExtensionProperty obj = JsonSerializer.Parse(json); + Assert.Null(obj.MyOverflow); + + string outJson = JsonSerializer.ToString(obj); + Assert.Contains(@"""MyNestedClass"":null", outJson); + } + + { + // Baseline with missing. + ClassWithExtensionProperty obj = JsonSerializer.Parse(jsonMissing); + Assert.Equal(1, obj.MyOverflow.Count); + Assert.Equal(JsonValueType.Null, obj.MyOverflow["MyNestedClassMissing"].Type); + } + + { + JsonSerializerOptions options = new JsonSerializerOptions(); + options.IgnoreNullValues = true; + + ClassWithExtensionProperty obj = JsonSerializer.Parse(jsonMissing, options); + + // Currently we do not ignore nulls in the extension data. The JsonDocument would also need to support this mode + // for any lower-level nulls. + Assert.Equal(1, obj.MyOverflow.Count); + Assert.Equal(JsonValueType.Null, obj.MyOverflow["MyNestedClassMissing"].Type); + } + } + + [Fact] + public static void InvalidExtensionPropertyFail() + { + // Baseline + JsonSerializer.Parse(@"{}"); + JsonSerializer.Parse(@"{}"); + + Assert.Throws(() => JsonSerializer.Parse(@"{}")); + Assert.Throws(() => JsonSerializer.Parse(@"{}")); + } + + [Fact] + public static void IgnoredDataShouldNotBeExtensionData() + { + ClassWithIgnoredData obj = JsonSerializer.Parse(@"{""MyInt"":1}"); + + Assert.Equal(0, obj.MyInt); + Assert.Null(obj.MyOverflow); + } + + [Fact] + public static void InvalidExtensionValue() + { + // Baseline + ClassWithExtensionPropertyAlreadyInstantiated obj = JsonSerializer.Parse(@"{}"); + obj.MyOverflow.Add("test", new object()); + + try + { + JsonSerializer.ToString(obj); + Assert.True(false, "InvalidOperationException should have thrown."); + } + catch (InvalidOperationException e) + { + // Verify the exception contains the property name and invalid type. + Assert.Contains("ClassWithExtensionPropertyAlreadyInstantiated.MyOverflow", e.Message); + Assert.Contains("System.Object", e.Message); + } + } + + [Fact] + public static void ObjectTree() + { + string json = @"{""MyIntMissing"":2, ""MyReference"":{""MyIntMissingChild"":3}}"; + + ClassWithReference obj = JsonSerializer.Parse(json); + Assert.IsType(obj.MyOverflow["MyIntMissing"]); + Assert.Equal(1, obj.MyOverflow.Count); + Assert.Equal(2, obj.MyOverflow["MyIntMissing"].GetInt32()); + + ClassWithExtensionProperty child = obj.MyReference; + Assert.IsType(child.MyOverflow["MyIntMissingChild"]); + Assert.IsType(child.MyOverflow["MyIntMissingChild"]); + Assert.Equal(1, child.MyOverflow.Count); + Assert.Equal(3, child.MyOverflow["MyIntMissingChild"].GetInt32()); + } + + public class ClassWithExtensionPropertyAlreadyInstantiated + { + public ClassWithExtensionPropertyAlreadyInstantiated() + { + MyOverflow = new Dictionary(); + } + + [JsonExtensionData] + public Dictionary MyOverflow { get; set; } + } + + public class ClassWithExtensionPropertyAsObject + { + [JsonExtensionData] + public Dictionary MyOverflow { get; set; } + } + + public class ClassWithIgnoredData + { + [JsonExtensionData] + public Dictionary MyOverflow { get; set; } + + [JsonIgnore] + public int MyInt { get; set; } + } + + public class ClassWithInvalidExtensionProperty + { + [JsonExtensionData] + public Dictionary MyOverflow { get; set; } + } + + public class ClassWithTwoExtensionPropertys + { + [JsonExtensionData] + public Dictionary MyOverflow1 { get; set; } + + [JsonExtensionData] + public Dictionary MyOverflow2 { get; set; } + } + + public class ClassWithReference + { + [JsonExtensionData] + public Dictionary MyOverflow { get; set; } + + public ClassWithExtensionProperty MyReference { get; set; } + } + } +} diff --git a/src/libraries/System.Text.Json/tests/Serialization/OptionsTests.cs b/src/libraries/System.Text.Json/tests/Serialization/OptionsTests.cs index 2806ee5..9fc15c0 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/OptionsTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/OptionsTests.cs @@ -91,6 +91,54 @@ namespace System.Text.Json.Serialization.Tests } [Fact] + public static void ExtensionDataUsesReaderOptions() + { + // We just verify trailing commas. + const string json = @"{""MyIntMissing"":2,}"; + + // Verify baseline without options. + Assert.Throws(() => JsonSerializer.Parse(json)); + + // Verify baseline with options. + var options = new JsonSerializerOptions(); + Assert.Throws(() => JsonSerializer.Parse(json, options)); + + // Set AllowTrailingCommas to true. + options = new JsonSerializerOptions(); + options.AllowTrailingCommas = true; + JsonSerializer.Parse(json, options); + } + + [Fact] + public static void ExtensionDataUsesWriterOptions() + { + // We just verify whitespace. + + ClassWithExtensionProperty obj = JsonSerializer.Parse(@"{""MyIntMissing"":2}"); + + // Verify baseline without options. + string json = JsonSerializer.ToString(obj); + Assert.False(HasNewLine()); + + // Verify baseline with options. + var options = new JsonSerializerOptions(); + json = JsonSerializer.ToString(obj, options); + Assert.False(HasNewLine()); + + // Set AllowTrailingCommas to true. + options = new JsonSerializerOptions(); + options.WriteIndented = true; + json = JsonSerializer.ToString(obj, options); + Assert.True(HasNewLine()); + + bool HasNewLine() + { + int iEnd = json.IndexOf("2", json.IndexOf("MyIntMissing")); + return json.Substring(iEnd + 1).StartsWith(Environment.NewLine); + } + } + + [Fact] public static void ReadCommentHandling() { Assert.Throws(() => JsonSerializer.Parse("/* commment */")); diff --git a/src/libraries/System.Text.Json/tests/Serialization/PropertyVisibilityTests.cs b/src/libraries/System.Text.Json/tests/Serialization/PropertyVisibilityTests.cs index dd4f8ec..f85cc12 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/PropertyVisibilityTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/PropertyVisibilityTests.cs @@ -75,25 +75,29 @@ namespace System.Text.Json.Serialization.Tests Assert.Equal(@"MyString", obj.MyString); Assert.Equal(@"MyStringWithIgnore", obj.MyStringWithIgnore); Assert.Equal(2, obj.MyStringsWithIgnore.Length); + Assert.Equal(1, obj.MyDictionaryWithIgnore["Key"]); // Verify serialize. string json = JsonSerializer.ToString(obj); Assert.Contains(@"""MyString""", json); Assert.DoesNotContain(@"MyStringWithIgnore", json); Assert.DoesNotContain(@"MyStringsWithIgnore", json); + Assert.DoesNotContain(@"MyDictionaryWithIgnore", json); // Verify deserialize default. obj = JsonSerializer.Parse(@"{}"); Assert.Equal(@"MyString", obj.MyString); Assert.Equal(@"MyStringWithIgnore", obj.MyStringWithIgnore); Assert.Equal(2, obj.MyStringsWithIgnore.Length); + Assert.Equal(1, obj.MyDictionaryWithIgnore["Key"]); // Verify deserialize ignores the json for MyStringWithIgnore and MyStringsWithIgnore. obj = JsonSerializer.Parse( - @"{""MyString"":""Hello"", ""MyStringWithIgnore"":""IgnoreMe"", ""MyStringsWithIgnore"":[""IgnoreMe""]}"); + @"{""MyString"":""Hello"", ""MyStringWithIgnore"":""IgnoreMe"", ""MyStringsWithIgnore"":[""IgnoreMe""], ""MyDictionaryWithIgnore"":{""Key"":9}}"); Assert.Contains(@"Hello", obj.MyString); Assert.Equal(@"MyStringWithIgnore", obj.MyStringWithIgnore); Assert.Equal(2, obj.MyStringsWithIgnore.Length); + Assert.Equal(1, obj.MyDictionaryWithIgnore["Key"]); } // Todo: add tests with missing object property and missing collection property. @@ -175,12 +179,16 @@ namespace System.Text.Json.Serialization.Tests { public ClassWithIgnoreAttributeProperty() { + MyDictionaryWithIgnore = new Dictionary { { "Key", 1 } }; MyString = "MyString"; MyStringWithIgnore = "MyStringWithIgnore"; MyStringsWithIgnore = new string[] { "1", "2" }; } [JsonIgnore] + public Dictionary MyDictionaryWithIgnore { get; set; } + + [JsonIgnore] public string MyStringWithIgnore { get; set; } public string MyString { get; set; } diff --git a/src/libraries/System.Text.Json/tests/Serialization/TestClasses.cs b/src/libraries/System.Text.Json/tests/Serialization/TestClasses.cs index a12e60e..e645316 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/TestClasses.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/TestClasses.cs @@ -1280,6 +1280,15 @@ namespace System.Text.Json.Serialization.Tests public int Aѧ34567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890 { get; set; } } + public class ClassWithExtensionProperty + { + public SimpleTestClass MyNestedClass { get; set; } + public int MyInt { get; set; } + + [JsonExtensionData] + public IDictionary MyOverflow { get; set; } + } + public class TestClassWithNestedObjectCommentsInner : ITestClass { public SimpleTestClass MyData { get; set; } 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 fb067fa..3f0bb1c 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 @@ -29,6 +29,7 @@ + -- 2.7.4