diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index 59b88fa60bf186..83999dd6be891a 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -447,4 +447,45 @@ This JsonElement instance was not built from a JsonNode and is immutable. - \ No newline at end of file + + Cannot parse preserved object to Array or Immutable. Type {0}. + + + Duplicated identifier '{0}' found while reading preserved object. + + + The metadata property $id must be the first property in the JSON object. + + + Invalid reference to value type {0}. + + + Invalid token after $values metadata property. + + + Missing $id before $values on preserved array. + + + Deserialization failed for one of these reasons: +1. {0} +2. {1} + + + Invalid property in preserved array. + + + Metadata $values property was not found in preserved array. + + + Reference objects must not contain other properties except for $ref. + + + Reference not found. + + + Value for metadata properties $id and $ref must be of type string. + + + Properties that start with '$' are not allowed on preserve mode, either escape the character or turn off preserve references. + + 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 1a0d916406f9e7..9182ad207194bb 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -23,10 +23,8 @@ - - diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/DefaultReferenceResolver.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/DefaultReferenceResolver.cs index f8280ea3f4a865..c22656790658ce 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/DefaultReferenceResolver.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/DefaultReferenceResolver.cs @@ -16,15 +16,18 @@ public DefaultReferenceResolver() // On deserialization: key is TKey. // On serialization: value is TKey. _referenceMapper = new Dictionary(); + //TODO: add second dictionary that uses reference equals. } // Used on deserialization. public void AddReference(string key, object value) { - if (!_referenceMapper.TryAdd(key, value)) + if (_referenceMapper.ContainsKey(key)) { - throw new JsonException($"Duplicated $id \"{key}\" found while preserving reference."); + ThrowHelper.ThrowJsonException_MetadataDuplicateIdFound(key); } + + _referenceMapper[key] = value; } // Used on serialization. @@ -52,7 +55,7 @@ public object ResolveReference(string key) { if (!_referenceMapper.TryGetValue(key, out object value)) { - throw new JsonException("Reference not found."); + ThrowHelper.ThrowJsonException_MetadataReferenceNotFound(); } return value; 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 index 794a6cb50d1a5f..df5c6a615b85cb 100644 --- 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 @@ -53,78 +53,7 @@ private static void HandleStartDictionary(JsonSerializerOptions options, ref Rea state.Current.ReturnValue = classInfo.CreateObject(); } - else - { - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(classInfo.Type); - } - - return; - } - - state.Current.CollectionPropertyInitialized = true; - - object value = ReadStackFrame.CreateDictionaryValue(ref state); - if (value != null) - { - state.Current.DetermineIfDictionaryCanBePopulated(value); - - 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.ReturnValue = value; - } - } - } - - private static void HandleStartDictionaryRef(JsonSerializerOptions options, ref ReadStack state) - { - Debug.Assert(!state.Current.IsProcessingEnumerable()); - - JsonPropertyInfo jsonPropertyInfo = state.Current.JsonPropertyInfo; - if (jsonPropertyInfo == null) - { - jsonPropertyInfo = state.Current.JsonClassInfo.CreateRootProperty(options); - } - - Debug.Assert(jsonPropertyInfo != null); - - // A nested object or dictionary, so push new frame. - if (state.Current.CollectionPropertyInitialized) - { - state.Push(); - state.Current.JsonClassInfo = jsonPropertyInfo.ElementClassInfo; - state.Current.InitializeJsonPropertyInfo(); - - JsonClassInfo classInfo = state.Current.JsonClassInfo; - - if (state.Current.IsProcessingDictionary()) - { - object dictValue = ReadStackFrame.CreateDictionaryValue(ref state); - - // If value is not null, then we don't have a converter so apply the value. - if (dictValue != null) - { - state.Current.ReturnValue = dictValue; - state.Current.DetermineIfDictionaryCanBePopulated(state.Current.ReturnValue); - } - - state.Current.CollectionPropertyInitialized = true; - } - else if (state.Current.IsProcessingObject(ClassType.Object)) - { - if (classInfo.CreateObject == null) - { - ThrowHelper.ThrowNotSupportedException_DeserializeCreateObjectDelegateIsNull(classInfo.Type); - } - - state.Current.ReturnValue = classInfo.CreateObject(); - } - // If is reference preserved array. - else if (state.Current.IsProcessingEnumerable()) + else if (options.ReferenceHandling.ShouldReadPreservedReferences() && state.Current.IsProcessingEnumerable()) { Type preservedObjType = state.Current.JsonClassInfo.PolicyProperty.GetJsonPreservedReferenceType(); // Re-Initialize the current frame. @@ -213,60 +142,5 @@ private static void HandleEndDictionary(JsonSerializerOptions options, ref ReadS } } } - - private static void HandleEndDictionaryRef(JsonSerializerOptions options, ref ReadStack state) - { - Debug.Assert(!state.Current.SkipProperty); - - if (state.Current.IsProcessingProperty(ClassType.Dictionary)) - { - if (state.Current.TempDictionaryValues != null) - { - JsonDictionaryConverter converter = state.Current.JsonPropertyInfo.DictionaryConverter; - state.Current.JsonPropertyInfo.SetValueAsObject(state.Current.ReturnValue, converter.CreateFromDictionary(ref state, state.Current.TempDictionaryValues, options)); - state.Current.EndProperty(); - } - else - { - // Handle special case of DataExtensionProperty where we just added a dictionary element to the extension property. - // Since the JSON value is not a dictionary element (it's a normal property in JSON) a JsonTokenType.EndObject - // encountered here is from the outer object so forward to HandleEndObject(). - if (state.Current.JsonClassInfo.DataExtensionProperty == state.Current.JsonPropertyInfo) - { - HandleEndObject(ref state); - } - else - { - // We added the items to the dictionary already. - state.Current.EndProperty(); - } - } - } - else - { - object value; - if (state.Current.TempDictionaryValues != null) - { - JsonDictionaryConverter converter = state.Current.JsonPropertyInfo.DictionaryConverter; - value = converter.CreateFromDictionary(ref state, state.Current.TempDictionaryValues, options); - } - else - { - 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); - } - } - } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleMetadata.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleMetadata.cs index 4cf25170f3dced..134715dfec5767 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleMetadata.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleMetadata.cs @@ -10,7 +10,7 @@ private static void HandleMetadataPropertyValue(ref Utf8JsonReader reader, ref R { if (reader.TokenType != JsonTokenType.String) { - throw new JsonException("Value for metadata properties cannot be other than string."); + ThrowHelper.ThrowJsonException_MetadataValueWasNotString(); } MetadataPropertyName metadata = state.Current.MetadataProperty; @@ -73,24 +73,24 @@ internal static MetadataPropertyName GetMetadataPropertyName(ReadOnlySpan // Fail state. // Set PropertyInfo or KeyName to write down the conflicting property name in JsonException.Path - if (!state.Current.IsProcessingDictionary()) + if (state.Current.IsProcessingDictionary()) { - JsonPropertyInfo info = JsonPropertyInfo.s_metadataProperty; - info.JsonPropertyName = propertyName.ToArray(); - state.Current.JsonPropertyInfo = info; + state.Current.KeyName = reader.GetString(); } else { - state.Current.KeyName = reader.GetString(); + JsonPropertyInfo info = JsonPropertyInfo.s_metadataProperty; + info.JsonPropertyName = propertyName.ToArray(); + state.Current.JsonPropertyInfo = info; } - throw new JsonException("Properties that start with '$' are not allowed on preserve mode, you must either escape '$' or turn off preserve references."); + ThrowHelper.ThrowJsonException_MetadataInvalidPropertyWithLeadingSign(); } return MetadataPropertyName.NoMetadata; } - private static void HandleReference(JsonSerializerOptions options, ref ReadStack state, ref Utf8JsonReader reader) + private static void HandleReference(ref ReadStack state) { object referenceValue = state.ResolveReference(state.Current.ReferenceId); if (state.Current.IsProcessingProperty(ClassType.Dictionary)) @@ -101,7 +101,7 @@ private static void HandleReference(JsonSerializerOptions options, ref ReadStack else { state.Current.ReturnValue = referenceValue; - HandleEndObjectRef(ref state); + HandleEndObject(ref state); } state.Current.ShouldHandleReference = false; @@ -109,22 +109,23 @@ private static void HandleReference(JsonSerializerOptions options, ref ReadStack internal static void SetAsPreserved(ref ReadStackFrame frame) { - bool alreadyPreserving; + //bool alreadyPreserving; if (frame.IsProcessingProperty(ClassType.Dictionary)) { - alreadyPreserving = frame.DictionaryPropertyIsPreserved; + //alreadyPreserving = frame.DictionaryPropertyIsPreserved; frame.DictionaryPropertyIsPreserved = true; } else { - alreadyPreserving = frame.IsPreserved; + //alreadyPreserving = frame.IsPreserved; frame.IsPreserved = true; } - if (alreadyPreserving) - { - throw new JsonException("Object already defines a reference identifier."); - } + // Unreachable, if more than one $id, error "$id must be the first property" will pop-up. + //if (alreadyPreserving) + //{ + // throw new JsonException("Object already defines a reference identifier."); + //} } } 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 eb6cd1f4d433ee..86897962905803 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 @@ -14,72 +14,10 @@ internal class JsonPreservedReference public static partial class JsonSerializer { - private static void HandleStartObject(JsonSerializerOptions options, ref ReadStack state) - { - Debug.Assert(!state.Current.IsProcessingDictionary()); - - // Note: unless we are a root object, we are going to push a property onto the ReadStack - // in the if/else if check below. - - if (state.Current.IsProcessingEnumerable()) - { - // A nested object within an enumerable (non-dictionary). - - if (!state.Current.CollectionPropertyInitialized) - { - // We have bad JSON: enumerable element appeared without preceding StartArray token. - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(state.Current.JsonPropertyInfo.DeclaredPropertyType); - } - - Type objType = state.Current.GetElementType(); - state.Push(); - state.Current.Initialize(objType, options); - } - else if (state.Current.JsonPropertyInfo != null) - { - // Nested object within an object. - Debug.Assert(state.Current.IsProcessingObject(ClassType.Object)); - - Type objType = state.Current.JsonPropertyInfo.RuntimePropertyType; - state.Push(); - state.Current.Initialize(objType, options); - } - - JsonClassInfo classInfo = state.Current.JsonClassInfo; - - if (state.Current.IsProcessingObject(ClassType.Dictionary)) - { - object value = ReadStackFrame.CreateDictionaryValue(ref state); - - // If value is not null, then we don't have a converter so apply the value. - if (value != null) - { - state.Current.ReturnValue = value; - state.Current.DetermineIfDictionaryCanBePopulated(state.Current.ReturnValue); - } - - state.Current.CollectionPropertyInitialized = true; - } - else if (state.Current.IsProcessingObject(ClassType.Object)) - { - if (classInfo.CreateObject == null) - { - ThrowHelper.ThrowNotSupportedException_DeserializeCreateObjectDelegateIsNull(classInfo.Type); - } - - state.Current.ReturnValue = classInfo.CreateObject(); - } - else - { - // Only dictionaries or objects are valid given the `StartObject` token. - ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(classInfo.Type); - } - } - [PreserveDependency("get_Values", "System.Text.Json.JsonPreservedReference`1")] [PreserveDependency("set_Values", "System.Text.Json.JsonPreservedReference`1")] [PreserveDependency(".ctor()", "System.Text.Json.JsonPreservedReference`1")] - private static void HandleStartObjectRef(JsonSerializerOptions options, ref ReadStack state) + private static void HandleStartObject(JsonSerializerOptions options, ref ReadStack state) { Debug.Assert(!state.Current.IsProcessingDictionary()); @@ -88,31 +26,18 @@ private static void HandleStartObjectRef(JsonSerializerOptions options, ref Read if (state.Current.IsProcessingEnumerable()) { - // A potential Preserved Array - Hit an StartObject while enumerable has not been initialized. + // A nested object within an enumerable (non-dictionary). if (!state.Current.CollectionPropertyInitialized) { - // Check we are not dealing with an immutable collection or fixed size array. - if (state.Current.JsonPropertyInfo.EnumerableConverter != null) + if (options.ReferenceHandling.ShouldReadPreservedReferences()) { - throw new JsonException("Immutable types and fixed size arrays cannot be preserved."); + HandlePreservedArray(ref state, options); } - - Type preservedObjType = state.Current.JsonPropertyInfo.GetJsonPreservedReferenceType(); - - // Is property. - if (state.Current.IsProcessingProperty(ClassType.Enumerable)) - { - state.Push(); - state.Current.Initialize(preservedObjType, options); - } - // Is root. else { - // Re-Initialize the current frame. - state.Current.Initialize(preservedObjType, options); + // We have bad JSON: enumerable element appeared without preceding StartArray token. + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(state.Current.JsonPropertyInfo.DeclaredPropertyType); } - - state.Current.IsPreservedArray = true; } else { @@ -164,8 +89,8 @@ private static void HandleStartObjectRef(JsonSerializerOptions options, ref Read private static void HandleEndObject(ref ReadStack state) { - // Only allow dictionaries to be processed here if this is the DataExtensionProperty. - Debug.Assert(!state.Current.IsProcessingDictionary() || state.Current.JsonClassInfo.DataExtensionProperty == state.Current.JsonPropertyInfo); + // Only allow dictionaries to be processed here if this is the DataExtensionProperty or if it was a preserved reference. + Debug.Assert(!state.Current.IsProcessingDictionary() || state.Current.JsonClassInfo.DataExtensionProperty == state.Current.JsonPropertyInfo || state.Current.ShouldHandleReference); // Check if we are trying to build the sorted cache. if (state.Current.PropertyRefCache != null) @@ -173,7 +98,16 @@ private static void HandleEndObject(ref ReadStack state) state.Current.JsonClassInfo.UpdateSortedPropertyCache(ref state.Current); } - object value = state.Current.ReturnValue; + object value; + // Used for ReferenceHandling.Preserve + if (state.Current.IsPreservedArray) + { + value = GetPreservedArrayValue(ref state); + } + else + { + value = state.Current.ReturnValue; + } if (state.IsLastFrame) { @@ -188,49 +122,41 @@ private static void HandleEndObject(ref ReadStack state) } } - private static void HandleEndObjectRef(ref ReadStack state) + private static object GetPreservedArrayValue(ref ReadStack state) { - // Only allow dictionaries to be processed here if this is the DataExtensionProperty or a reference object evaluated as null and is now finishing the dictionary object. - Debug.Assert(!state.Current.IsProcessingDictionary() || state.Current.JsonClassInfo.DataExtensionProperty == state.Current.JsonPropertyInfo || state.Current.ShouldHandleReference); + // Preserved JSON arrays are wrapped into JsonPreservedReference where T is the original type of the enumerable + // and Values is the actual enumerable instance being preserved. + JsonPropertyInfo info = state.Current.JsonClassInfo.PropertyCache["Values"]; + object value = info.GetValueAsObject(state.Current.ReturnValue); - // Check if we are trying to build the sorted cache. - if (state.Current.PropertyRefCache != null) + if (value == null) { - state.Current.JsonClassInfo.UpdateSortedPropertyCache(ref state.Current); + ThrowHelper.ThrowJsonException_MetadataPreservedArrayValuesNotFound(info.DeclaredPropertyType); } - object value; - if (state.Current.IsPreservedArray) - { - // Preserved JSON arrays are wrapped into JsonPreservedReference where T is the original type of the enumerable - // and Values is the actual enumerable instance being preserved. - JsonPropertyInfo info = state.Current.JsonClassInfo.PropertyCache["Values"]; - value = info.GetValueAsObject(state.Current.ReturnValue); + return value; + } - if (value == null) - { - throw new JsonException( - "Deserializaiton failed for one of these reasons:\n" + - "1. $values property was not present in preserved array.\n" + - "2. " + SR.Format(SR.DeserializeUnableToConvertValue, info.DeclaredPropertyType)); - } - } - else + private static void HandlePreservedArray(ref ReadStack state, JsonSerializerOptions options) + { + // Check we are not parsing into immutable or array. + if (state.Current.JsonPropertyInfo.EnumerableConverter != null) { - value = state.Current.ReturnValue; + ThrowHelper.ThrowJsonException_MetadataCannotParsePreservedObjectIntoImmutable(state.Current.JsonPropertyInfo.DeclaredPropertyType); } - - if (state.IsLastFrame) + Type preservedObjType = state.Current.JsonPropertyInfo.GetJsonPreservedReferenceType(); + if (state.Current.IsProcessingProperty(ClassType.Enumerable)) { - state.Current.Reset(); - state.Current.ReturnValue = value; + state.Push(); + state.Current.Initialize(preservedObjType, options); } else { - state.Pop(); - - ApplyObjectToEnumerable(value, ref state); + // Re-Initialize the current frame. + state.Current.Initialize(preservedObjType, options); } + + state.Current.IsPreservedArray = true; } } } 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 index cbc93f23c7de93..01b750eff5b10e 100644 --- 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 @@ -35,6 +35,13 @@ private static void HandlePropertyName( state.Current.JsonPropertyInfo = state.Current.JsonClassInfo.PolicyProperty; } + if (options.ReferenceHandling.ShouldReadPreservedReferences()) + { + ReadOnlySpan propertyName = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; + MetadataPropertyName metadata = GetMetadataProperty(propertyName, ref state, ref reader); + ResolveMetadataOnDictionary(metadata, ref state); + } + state.Current.KeyName = reader.GetString(); } else @@ -44,269 +51,222 @@ private static void HandlePropertyName( state.Current.EndProperty(); ReadOnlySpan propertyName = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; - if (reader._stringHasEscaping) + if (options.ReferenceHandling.ShouldReadPreservedReferences()) { - int idx = propertyName.IndexOf(JsonConstants.BackSlash); - Debug.Assert(idx != -1); - propertyName = GetUnescapedString(propertyName, idx); - } - - JsonPropertyInfo jsonPropertyInfo = state.Current.JsonClassInfo.GetProperty(propertyName, ref state.Current); - if (jsonPropertyInfo == JsonPropertyInfo.s_missingProperty) - { - JsonPropertyInfo dataExtProperty = state.Current.JsonClassInfo.DataExtensionProperty; - if (dataExtProperty == null) - { - state.Current.JsonPropertyInfo = JsonPropertyInfo.s_missingProperty; - } - else - { - state.Current.JsonPropertyInfo = dataExtProperty; - state.Current.JsonPropertyName = propertyName.ToArray(); - state.Current.KeyName = JsonHelpers.Utf8GetString(propertyName); - state.Current.CollectionPropertyInitialized = true; - - CreateDataExtensionProperty(dataExtProperty, ref state); - } + MetadataPropertyName metadata = GetMetadataProperty(propertyName, ref state, ref reader); + ResolveMetadataOnObject(propertyName, metadata, ref state, ref reader, options); } else { - // Support JsonException.Path. - Debug.Assert( - jsonPropertyInfo.JsonPropertyName == null || - options.PropertyNameCaseInsensitive || - propertyName.SequenceEqual(jsonPropertyInfo.JsonPropertyName)); - - state.Current.JsonPropertyInfo = jsonPropertyInfo; - - if (jsonPropertyInfo.JsonPropertyName == null) - { - byte[] propertyNameArray = propertyName.ToArray(); - if (options.PropertyNameCaseInsensitive) - { - // Each payload can have a different name here; remember the value on the temporary stack. - state.Current.JsonPropertyName = propertyNameArray; - } - else - { - // Prevent future allocs by caching globally on the JsonPropertyInfo which is specific to a Type+PropertyName - // so it will match the incoming payload except when case insensitivity is enabled (which is handled above). - state.Current.JsonPropertyInfo.JsonPropertyName = propertyNameArray; - } - } + HandlePropertyNameDefault(propertyName, ref state, ref reader, options); } - - // Increment the PropertyIndex so JsonClassInfo.GetProperty() starts with the next property. - state.Current.PropertyIndex++; } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void HandlePropertyNameRef( - JsonSerializerOptions options, - ref Utf8JsonReader reader, + private static void CreateDataExtensionProperty( + JsonPropertyInfo jsonPropertyInfo, ref ReadStack state) { - if (state.Current.Drain) + Debug.Assert(jsonPropertyInfo != null); + Debug.Assert(state.Current.ReturnValue != null); + + IDictionary extensionData = (IDictionary)jsonPropertyInfo.GetValueAsObject(state.Current.ReturnValue); + if (extensionData == null) { - return; + // Create the appropriate dictionary type. We already verified the types. + Debug.Assert(jsonPropertyInfo.DeclaredPropertyType.IsGenericType); + Debug.Assert(jsonPropertyInfo.DeclaredPropertyType.GetGenericArguments().Length == 2); + Debug.Assert(jsonPropertyInfo.DeclaredPropertyType.GetGenericArguments()[0].UnderlyingSystemType == typeof(string)); + Debug.Assert( + jsonPropertyInfo.DeclaredPropertyType.GetGenericArguments()[1].UnderlyingSystemType == typeof(object) || + jsonPropertyInfo.DeclaredPropertyType.GetGenericArguments()[1].UnderlyingSystemType == typeof(JsonElement)); + + extensionData = (IDictionary)jsonPropertyInfo.RuntimeClassInfo.CreateObject(); + jsonPropertyInfo.SetValueAsObject(state.Current.ReturnValue, extensionData); } + // We don't add the value to the dictionary here because we need to support the read-ahead functionality for Streams. + } + + private static MetadataPropertyName GetMetadataProperty(ReadOnlySpan propertyName, ref ReadStack state, ref Utf8JsonReader reader) + { if (state.Current.ShouldHandleReference) { - throw new JsonException("Reference objects cannot contain other properties."); + ThrowHelper.ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties(); } - Debug.Assert(state.Current.ReturnValue != null || state.Current.TempDictionaryValues != null); - Debug.Assert(state.Current.JsonClassInfo != null); + MetadataPropertyName metadata = GetMetadataPropertyName(propertyName, ref state, ref reader); + state.Current.MetadataProperty = metadata; + return metadata; + } - bool isProcessingDictObject = state.Current.IsProcessingObject(ClassType.Dictionary); - if ((isProcessingDictObject || state.Current.IsProcessingProperty(ClassType.Dictionary)) && - state.Current.JsonClassInfo.DataExtensionProperty != state.Current.JsonPropertyInfo) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void HandlePropertyNameDefault(ReadOnlySpan propertyName, ref ReadStack state, ref Utf8JsonReader reader, JsonSerializerOptions options) + { + if (reader._stringHasEscaping) { - if (isProcessingDictObject) - { - state.Current.JsonPropertyInfo = state.Current.JsonClassInfo.PolicyProperty; - } + int idx = propertyName.IndexOf(JsonConstants.BackSlash); + Debug.Assert(idx != -1); + propertyName = GetUnescapedString(propertyName, idx); + } - ReadOnlySpan propertyName = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; - MetadataPropertyName meta = GetMetadataPropertyName(propertyName, ref state, ref reader); - state.Current.MetadataProperty = meta; + JsonPropertyInfo jsonPropertyInfo = state.Current.JsonClassInfo.GetProperty(propertyName, ref state.Current); - if (meta == MetadataPropertyName.Id) + if (jsonPropertyInfo == JsonPropertyInfo.s_missingProperty) + { + JsonPropertyInfo dataExtProperty = state.Current.JsonClassInfo.DataExtensionProperty; + if (dataExtProperty == null) { - if (state.Current.TempDictionaryValues != null) - { - throw new JsonException("Immutable types and fixed size arrays cannot be preserved."); - } - - SetAsPreserved(ref state.Current); - state.Current.ReadMetadataValue = true; + state.Current.JsonPropertyInfo = JsonPropertyInfo.s_missingProperty; } - else if (meta == MetadataPropertyName.Ref) + else { - bool isPreserved = state.Current.IsProcessingProperty(ClassType.Dictionary) ? state.Current.DictionaryPropertyIsPreserved : state.Current.IsPreserved; - if (state.Current.KeyName != null || isPreserved || state.Current.ShouldHandleReference) - { - throw new JsonException("Reference objects cannot contain other properties."); - } + state.Current.JsonPropertyInfo = dataExtProperty; + state.Current.JsonPropertyName = propertyName.ToArray(); + state.Current.KeyName = JsonHelpers.Utf8GetString(propertyName); + state.Current.CollectionPropertyInitialized = true; - state.Current.ReadMetadataValue = true; - state.Current.ShouldHandleReference = true; + CreateDataExtensionProperty(dataExtProperty, ref state); } - - state.Current.KeyName = reader.GetString(); } else { - Debug.Assert(state.Current.JsonClassInfo.ClassType == ClassType.Object); - - state.Current.EndProperty(); + // Support JsonException.Path. + Debug.Assert( + jsonPropertyInfo.JsonPropertyName == null || + options.PropertyNameCaseInsensitive || + propertyName.SequenceEqual(jsonPropertyInfo.JsonPropertyName)); - ReadOnlySpan propertyName = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; - MetadataPropertyName meta = GetMetadataPropertyName(propertyName, ref state, ref reader); - state.Current.MetadataProperty = meta; + state.Current.JsonPropertyInfo = jsonPropertyInfo; - if (meta == MetadataPropertyName.NoMetadata) + if (jsonPropertyInfo.JsonPropertyName == null) { - if (reader._stringHasEscaping) - { - int idx = propertyName.IndexOf(JsonConstants.BackSlash); - Debug.Assert(idx != -1); - propertyName = GetUnescapedString(propertyName, idx); - } - - JsonPropertyInfo jsonPropertyInfo = state.Current.JsonClassInfo.GetProperty(propertyName, ref state.Current); - - if (state.Current.IsPreservedArray) + byte[] propertyNameArray = propertyName.ToArray(); + if (options.PropertyNameCaseInsensitive) { - jsonPropertyInfo.JsonPropertyName = propertyName.ToArray(); - state.Current.JsonPropertyInfo = jsonPropertyInfo; - throw new JsonException( - "Deserializaiton failed for one of these reasons:\n" + - "1. Invalid property in preserved array.\n" + - "2. " + SR.Format(SR.DeserializeUnableToConvertValue, state.Current.JsonClassInfo.PropertyCache["Values"].DeclaredPropertyType)); - } - - if (jsonPropertyInfo == JsonPropertyInfo.s_missingProperty) - { - JsonPropertyInfo dataExtProperty = state.Current.JsonClassInfo.DataExtensionProperty; - if (dataExtProperty == null) - { - state.Current.JsonPropertyInfo = JsonPropertyInfo.s_missingProperty; - } - else - { - state.Current.JsonPropertyInfo = dataExtProperty; - state.Current.JsonPropertyName = propertyName.ToArray(); - state.Current.KeyName = JsonHelpers.Utf8GetString(propertyName); - state.Current.CollectionPropertyInitialized = true; - - CreateDataExtensionProperty(dataExtProperty, ref state); - } + // Each payload can have a different name here; remember the value on the temporary stack. + state.Current.JsonPropertyName = propertyNameArray; } else { - // Support JsonException.Path. - Debug.Assert( - jsonPropertyInfo.JsonPropertyName == null || - options.PropertyNameCaseInsensitive || - propertyName.SequenceEqual(jsonPropertyInfo.JsonPropertyName)); - - state.Current.JsonPropertyInfo = jsonPropertyInfo; - - if (jsonPropertyInfo.JsonPropertyName == null) - { - byte[] propertyNameArray = propertyName.ToArray(); - if (options.PropertyNameCaseInsensitive) - { - // Each payload can have a different name here; remember the value on the temporary stack. - state.Current.JsonPropertyName = propertyNameArray; - } - else - { - // Prevent future allocs by caching globally on the JsonPropertyInfo which is specific to a Type+PropertyName - // so it will match the incoming payload except when case insensitivity is enabled (which is handled above). - state.Current.JsonPropertyInfo.JsonPropertyName = propertyNameArray; - } - } + // Prevent future allocs by caching globally on the JsonPropertyInfo which is specific to a Type+PropertyName + // so it will match the incoming payload except when case insensitivity is enabled (which is handled above). + state.Current.JsonPropertyInfo.JsonPropertyName = propertyNameArray; } + } + } + + // Increment the PropertyIndex so JsonClassInfo.GetProperty() starts with the next property. + state.Current.PropertyIndex++; + } + + private static void ResolveMetadataOnDictionary(MetadataPropertyName metadata, ref ReadStack state) + { + bool isPreserved = state.Current.IsProcessingProperty(ClassType.Dictionary) ? + state.Current.DictionaryPropertyIsPreserved : state.Current.IsPreserved; - // Increment the PropertyIndex so JsonClassInfo.GetProperty() starts with the next property. - state.Current.PropertyIndex++; + if (metadata == MetadataPropertyName.Id) + { + // Check we are not parsing into immutable. + if (state.Current.TempDictionaryValues != null) + { + ThrowHelper.ThrowJsonException_MetadataCannotParsePreservedObjectIntoImmutable(state.Current.JsonPropertyInfo.DeclaredPropertyType); } - else if (meta == MetadataPropertyName.Id) + + if (state.Current.KeyName != null || isPreserved) { - if (state.Current.PropertyIndex > 0 || state.Current.IsPreserved || state.Current.ShouldHandleReference) - { - throw new JsonException("The identifier must be the first property in the JSON object."); - } + state.Current.KeyName = null; + ThrowHelper.ThrowJsonException_MetadataIdIsNotFirstProperty(); + } - JsonPropertyInfo info = JsonPropertyInfo.s_metadataProperty; - info.JsonPropertyName = propertyName.ToArray(); - state.Current.JsonPropertyInfo = info; + SetAsPreserved(ref state.Current); + state.Current.ReadMetadataValue = true; + } + else if (metadata == MetadataPropertyName.Ref) + { + if (state.Current.KeyName != null || isPreserved) + { + ThrowHelper.ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties(); + } + + state.Current.ShouldHandleReference = true; + state.Current.ReadMetadataValue = true; + } + } - state.Current.ReadMetadataValue = true; - SetAsPreserved(ref state.Current); + private static void ResolveMetadataOnObject(ReadOnlySpan propertyName, MetadataPropertyName meta, ref ReadStack state, ref Utf8JsonReader reader, JsonSerializerOptions options) + { + if (meta == MetadataPropertyName.NoMetadata) + { + if (state.Current.IsPreservedArray) + { + ThrowOnRegularPropertyInPreservedArray(propertyName, ref state, ref reader); } - else if (meta == MetadataPropertyName.Values) + // Regular property, call main logic for HandlePropertyName. + HandlePropertyNameDefault(propertyName, ref state, ref reader, options); + } + else if (meta == MetadataPropertyName.Id) + { + if (state.Current.PropertyIndex > 0 || state.Current.IsPreserved) { - // Preserved JSON arrays are wrapped into JsonPreservedReference where T is the original type of the enumerable - // and Values is the actual enumerable instance being preserved. - JsonPropertyInfo info = state.Current.JsonClassInfo.PropertyCache["Values"]; - info.JsonPropertyName = propertyName.ToArray(); - state.Current.JsonPropertyInfo = info; + ThrowHelper.ThrowJsonException_MetadataIdIsNotFirstProperty(); + } - if (!state.Current.IsPreserved) - { - throw new JsonException("Preserved arrays canot lack an identifier."); - } + JsonPropertyInfo info = JsonPropertyInfo.s_metadataProperty; + info.JsonPropertyName = propertyName.ToArray(); + state.Current.JsonPropertyInfo = info; + + state.Current.ReadMetadataValue = true; + SetAsPreserved(ref state.Current); + } + else if (meta == MetadataPropertyName.Values) + { + // Preserved JSON arrays are wrapped into JsonPreservedReference where T is the original type of the enumerable + // and Values is the actual enumerable instance being preserved. + JsonPropertyInfo info = state.Current.JsonClassInfo.PropertyCache["Values"]; + info.JsonPropertyName = propertyName.ToArray(); + state.Current.JsonPropertyInfo = info; + + if (!state.Current.IsPreserved) + { + ThrowHelper.ThrowJsonException_MetadataMissingIdBeforeValues(); } - else //$ref case + } + else // $ref case + { + if (state.Current.JsonClassInfo.Type.IsValueType) { - if (state.Current.JsonClassInfo.Type.IsValueType) - { - throw new JsonException("Reference objects to value types are not allowed."); - } + ThrowHelper.ThrowJsonException_MetadataInvalidReferenceToValueType(state.Current.JsonClassInfo.Type); + } - if (state.Current.PropertyIndex > 0 || state.Current.IsPreserved || state.Current.ShouldHandleReference) - { - throw new JsonException("Reference objects cannot contain other properties."); - } + if (state.Current.PropertyIndex > 0 || state.Current.IsPreserved) + { + ThrowHelper.ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties(); + } - JsonPropertyInfo info = JsonPropertyInfo.s_metadataProperty; - info.JsonPropertyName = propertyName.ToArray(); - state.Current.JsonPropertyInfo = info; + JsonPropertyInfo info = JsonPropertyInfo.s_metadataProperty; + info.JsonPropertyName = propertyName.ToArray(); + state.Current.JsonPropertyInfo = info; - state.Current.ReadMetadataValue = true; - state.Current.ShouldHandleReference = true; - } + state.Current.ReadMetadataValue = true; + state.Current.ShouldHandleReference = true; } } - private static void CreateDataExtensionProperty( - JsonPropertyInfo jsonPropertyInfo, - ref ReadStack state) + private static void ThrowOnRegularPropertyInPreservedArray(ReadOnlySpan propertyName, ref ReadStack state, ref Utf8JsonReader reader) { - Debug.Assert(jsonPropertyInfo != null); - Debug.Assert(state.Current.ReturnValue != null); - - IDictionary extensionData = (IDictionary)jsonPropertyInfo.GetValueAsObject(state.Current.ReturnValue); - if (extensionData == null) + if (reader._stringHasEscaping) { - // Create the appropriate dictionary type. We already verified the types. - Debug.Assert(jsonPropertyInfo.DeclaredPropertyType.IsGenericType); - Debug.Assert(jsonPropertyInfo.DeclaredPropertyType.GetGenericArguments().Length == 2); - Debug.Assert(jsonPropertyInfo.DeclaredPropertyType.GetGenericArguments()[0].UnderlyingSystemType == typeof(string)); - Debug.Assert( - jsonPropertyInfo.DeclaredPropertyType.GetGenericArguments()[1].UnderlyingSystemType == typeof(object) || - jsonPropertyInfo.DeclaredPropertyType.GetGenericArguments()[1].UnderlyingSystemType == typeof(JsonElement)); - - extensionData = (IDictionary)jsonPropertyInfo.RuntimeClassInfo.CreateObject(); - jsonPropertyInfo.SetValueAsObject(state.Current.ReturnValue, extensionData); + int idx = propertyName.IndexOf(JsonConstants.BackSlash); + Debug.Assert(idx != -1); + propertyName = GetUnescapedString(propertyName, idx); } - // We don't add the value to the dictionary here because we need to support the read-ahead functionality for Streams. + JsonPropertyInfo jsonPropertyInfo = state.Current.JsonClassInfo.GetProperty(propertyName, ref state.Current); + jsonPropertyInfo.JsonPropertyName = propertyName.ToArray(); + state.Current.JsonPropertyInfo = jsonPropertyInfo; + + ThrowHelper.ThrowJsonException_MetadataPreservedArrayInvalidProperty(state.Current.JsonClassInfo.PropertyCache["Values"].DeclaredPropertyType); } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleReferences.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleReferences.cs deleted file mode 100644 index ca7ba3ae3de44b..00000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleReferences.cs +++ /dev/null @@ -1,160 +0,0 @@ -// 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.Diagnostics; -using System.Runtime.CompilerServices; - -namespace System.Text.Json -{ - /// - /// Provides functionality to serialize objects or value types to JSON and - /// deserialize JSON into objects or value types. - /// - public static partial class JsonSerializer - { - private static void ReadCoreRef( - JsonSerializerOptions options, - ref Utf8JsonReader reader, - ref ReadStack readStack) - { - try - { - JsonReaderState initialState = default; - long initialBytesConsumed = default; - - while (true) - { - if (readStack.ReadAhead) - { - // When we're reading ahead we always have to save the state - // as we don't know if the next token is an opening object or - // array brace. - initialState = reader.CurrentState; - initialBytesConsumed = reader.BytesConsumed; - } - - if (!reader.Read()) - { - // Need more data - break; - } - - JsonTokenType tokenType = reader.TokenType; - - if (readStack.Current.MetadataProperty == MetadataPropertyName.Values) - { - if (tokenType != JsonTokenType.StartArray) - { - throw new JsonException("Invalid array for $values property."); - } - else - { - readStack.Current.MetadataProperty = MetadataPropertyName.NoMetadata; - } - } - - if (JsonHelpers.IsInRangeInclusive(tokenType, JsonTokenType.String, JsonTokenType.False)) - { - Debug.Assert(tokenType == JsonTokenType.String || tokenType == JsonTokenType.Number || tokenType == JsonTokenType.True || tokenType == JsonTokenType.False); - - //HandleValue(tokenType, options, ref reader, ref readStack); - HandleValueRef(tokenType, options, ref reader, ref readStack); - } - else if (tokenType == JsonTokenType.PropertyName) - { - HandlePropertyNameRef(options, ref reader, ref readStack); - } - else if (tokenType == JsonTokenType.StartObject) - { - if (readStack.Current.SkipProperty) - { - readStack.Push(); - readStack.Current.Drain = true; - } - else if (readStack.Current.IsProcessingValue()) - { - if (!HandleObjectAsValue(tokenType, options, ref reader, ref readStack, ref initialState, initialBytesConsumed)) - { - // Need more data - break; - } - } - else if (readStack.Current.IsProcessingDictionary()) - { - HandleStartDictionaryRef(options, ref readStack); - } - else - { - HandleStartObjectRef(options, ref readStack); - } - } - else if (tokenType == JsonTokenType.EndObject) - { - if (readStack.Current.ShouldHandleReference) - { - HandleReference(options, ref readStack, ref reader); - } - else if (readStack.Current.Drain) - { - readStack.Pop(); - - // Clear the current property in case it is a dictionary, since dictionaries must have EndProperty() called when completed. - // A non-dictionary property can also have EndProperty() called when completed, although it is redundant. - readStack.Current.EndProperty(); - } - else if (readStack.Current.IsProcessingDictionary()) - { - HandleEndDictionaryRef(options, ref readStack); - } - else - { - HandleEndObjectRef(ref readStack); - } - } - else if (tokenType == JsonTokenType.StartArray) - { - if (!readStack.Current.IsProcessingValue()) - { - HandleStartArray(options, ref readStack); - } - else if (!HandleObjectAsValue(tokenType, options, ref reader, ref readStack, ref initialState, initialBytesConsumed)) - { - // Need more data - break; - } - } - else if (tokenType == JsonTokenType.EndArray) - { - HandleEndArray(options, ref readStack); - } - else if (tokenType == JsonTokenType.Null) - { - HandleNull(options, ref reader, ref readStack); - } - } - } - catch (JsonReaderException ex) - { - // Re-throw with Path information. - ThrowHelper.ReThrowWithPath(readStack, ex); - } - catch (FormatException ex) when (ex.Source == ThrowHelper.ExceptionSourceValueToRethrowAsJsonException) - { - ThrowHelper.ReThrowWithPath(readStack, reader, ex); - } - catch (InvalidOperationException ex) when (ex.Source == ThrowHelper.ExceptionSourceValueToRethrowAsJsonException) - { - ThrowHelper.ReThrowWithPath(readStack, reader, ex); - } - catch (JsonException ex) - { - ThrowHelper.AddExceptionInformation(readStack, reader, ex); - throw; - } - - readStack.BytesConsumed += reader.BytesConsumed; - } - } -} 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 37fc28bcb7b6ff..584362af51e71b 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 @@ -17,24 +17,10 @@ private static void HandleValue(JsonTokenType tokenType, JsonSerializerOptions o return; } - JsonPropertyInfo jsonPropertyInfo = state.Current.JsonPropertyInfo; - if (jsonPropertyInfo == null) - { - jsonPropertyInfo = state.Current.JsonClassInfo.CreateRootProperty(options); - } - else if (state.Current.JsonClassInfo.ClassType == ClassType.Unknown) - { - jsonPropertyInfo = state.Current.JsonClassInfo.GetOrAddPolymorphicProperty(jsonPropertyInfo, typeof(object), options); - } - - jsonPropertyInfo.Read(tokenType, ref state, ref reader); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void HandleValueRef(JsonTokenType tokenType, JsonSerializerOptions options, ref Utf8JsonReader reader, ref ReadStack state) - { - if (state.Current.SkipProperty) + // Used for ReferenceHandling.Preserve. + if (state.Current.ReadMetadataValue) { + HandleMetadataPropertyValue(ref reader, ref state); return; } @@ -47,11 +33,6 @@ private static void HandleValueRef(JsonTokenType tokenType, JsonSerializerOption { jsonPropertyInfo = state.Current.JsonClassInfo.GetOrAddPolymorphicProperty(jsonPropertyInfo, typeof(object), options); } - else if (state.Current.ReadMetadataValue) - { - HandleMetadataPropertyValue(ref reader, ref state); - return; - } jsonPropertyInfo.Read(tokenType, ref state, ref reader); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs index 2322da4ee278a7..4be1a42a4c4c0e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs @@ -16,15 +16,7 @@ private static object ReadCore( ReadStack state = default; state.Current.Initialize(returnType, options); - if (options.ReferenceHandling.PreserveHandlingOnDeserialize == PreserveReferencesHandling.All) - { - ReadCoreRef(options, ref reader, ref state); - - } - else - { - ReadCore(options, ref reader, ref state); - } + ReadCore(options, ref reader, ref state); return state.Current.ReturnValue; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs index 6991a64120e413..4d8cb2913b6dbd 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs @@ -217,21 +217,10 @@ private static void ReadCore( readStack.ReadAhead = !isFinalBlock; readStack.BytesConsumed = 0; - if (options.ReferenceHandling.PreserveHandlingOnDeserialize == PreserveReferencesHandling.All) - { - ReadCoreRef( - options, - ref reader, - ref readStack); - - } - else - { - ReadCore( - options, - ref reader, - ref readStack); - } + ReadCore( + options, + ref reader, + ref readStack); readerState = reader.CurrentState; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Utf8JsonReader.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Utf8JsonReader.cs index 58098905bd08f6..aae09bc7232316 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Utf8JsonReader.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Utf8JsonReader.cs @@ -302,14 +302,7 @@ private static void ReadValueCore(JsonSerializerOptions options, ref Utf8JsonRea var newReader = new Utf8JsonReader(rentedSpan, isFinalBlock: true, state: default); - if (options.ReferenceHandling.PreserveHandlingOnDeserialize == PreserveReferencesHandling.All) - { - ReadCoreRef(options, ref newReader, ref readStack); - } - else - { - ReadCore(options, ref newReader, ref readStack); - } + ReadCore(options, ref newReader, ref readStack); // The reader should have thrown if we have remaining bytes. Debug.Assert(newReader.BytesConsumed == length); 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 d3f8ce83aa9ac8..d54a047a92d77e 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 @@ -43,6 +43,11 @@ private static void ReadCore( JsonTokenType tokenType = reader.TokenType; + if (options.ReferenceHandling.ShouldReadPreservedReferences()) + { + CheckValidTokenAfterMetadataValues(ref readStack, tokenType); + } + if (JsonHelpers.IsInRangeInclusive(tokenType, JsonTokenType.String, JsonTokenType.False)) { Debug.Assert(tokenType == JsonTokenType.String || tokenType == JsonTokenType.Number || tokenType == JsonTokenType.True || tokenType == JsonTokenType.False); @@ -87,6 +92,11 @@ private static void ReadCore( // A non-dictionary property can also have EndProperty() called when completed, although it is redundant. readStack.Current.EndProperty(); } + // Used for ReferenceHandling.Preserve. + else if (readStack.Current.ShouldHandleReference) + { + HandleReference(ref readStack); + } else if (readStack.Current.IsProcessingDictionary()) { HandleEndDictionary(options, ref readStack); @@ -202,5 +212,18 @@ private static ReadOnlySpan GetUnescapedString(ReadOnlySpan utf8Sour return propertyName; } + + private static void CheckValidTokenAfterMetadataValues(ref ReadStack state, JsonTokenType tokenType) + { + if (state.Current.MetadataProperty == MetadataPropertyName.Values) + { + if (tokenType != JsonTokenType.StartArray) + { + ThrowHelper.ThrowJsonException_MetadataValuesInvalidToken(); + } + + state.Current.MetadataProperty = MetadataPropertyName.NoMetadata; + } + } } } 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 d0256716e32e91..b59b45013d9c12 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 @@ -35,17 +35,25 @@ private static bool HandleDictionary( if (state.Current.PopStackOnEndCollection) { - state.Pop(writer, options); + state.Pop(); } return true; } - ResolvedReferenceHandling handling = options.HandleReference(ref state, out string referenceId, out bool writeAsReference, enumerable); - if (handling == ResolvedReferenceHandling.Ignore) + if (options.ReferenceHandling.ShouldWritePreservedReferences()) { - //Reference loop found, do not write anything and pop the frame from the stack. - return WriteEndDictionary(ref state, writer, options); + if (WriteReferenceDictionary(ref state, writer, options, enumerable)) + { + return WriteEndDictionary(ref state); + } + } + else + { + if (state.Current.ExtensionDataStatus != ExtensionDataWriteStatus.Writing) + { + state.Current.WriteObjectOrArrayStart(ClassType.Dictionary, writer, options); + } } // Let the dictionary return the default IEnumerator from its IEnumerable.GetEnumerator(). @@ -53,17 +61,6 @@ private static bool HandleDictionary( // For IDictionary-derived classes this is normally IDictionaryEnumerator as well // but may be IEnumerable> if the dictionary only supports generics. state.Current.CollectionEnumerator = enumerable.GetEnumerator(); - - if (state.Current.ExtensionDataStatus != ExtensionDataWriteStatus.Writing) - { - options.WriteStart(ref state.Current, ClassType.Dictionary, writer, options, writeAsReference: writeAsReference, referenceId: referenceId); - } - - // Return when writeAsReference is true. - if (handling == ResolvedReferenceHandling.IsReference) - { - return WriteEndDictionary(ref state, writer, options); - } } if (state.Current.CollectionEnumerator.MoveNext()) @@ -118,24 +115,53 @@ private static bool HandleDictionary( writer.WriteEndObject(); } - return WriteEndDictionary(ref state, writer, options); + return WriteEndDictionary(ref state); } - private static bool WriteEndDictionary(ref WriteStack state, Utf8JsonWriter writer, JsonSerializerOptions options) + private static bool WriteEndDictionary(ref WriteStack state) { if (state.Current.PopStackOnEndCollection) { - state.Pop(writer, options); + state.Pop(); } else { - options.PopReference(ref state, true); state.Current.EndDictionary(); } return true; } + private static bool WriteReferenceDictionary(ref WriteStack state, Utf8JsonWriter writer, JsonSerializerOptions options, IEnumerable enumerable) + { + ResolvedReferenceHandling handling = state.PreserveReference(enumerable, out string referenceId); + + if (handling == ResolvedReferenceHandling.IsReference) + { + // Object written before, write { "$ref": "#" } and finish. + state.Current.WriteReferenceObjectOrArrayStart(ClassType.Dictionary, writer, options, writeAsReference: true, referenceId: referenceId); + return true; + } + else if (handling == ResolvedReferenceHandling.Preserve) + { + // Object reference, write start and append $id. + // Should this also have ExtensionData condition? + // if (state.Current.ExtensionDataStatus != ExtensionDataWriteStatus.Writing)? + state.Current.WriteReferenceObjectOrArrayStart(ClassType.Dictionary, writer, options, writeAsReference: false, referenceId: referenceId); + } + else + { + // Value type, fallback on regular Write method. + // Is this even reachable for Dictionaries? + if (state.Current.ExtensionDataStatus != ExtensionDataWriteStatus.Writing) + { + state.Current.WriteObjectOrArrayStart(ClassType.Dictionary, writer, options); + } + } + + return false; + } + internal static void WriteDictionary( JsonConverter converter, JsonSerializerOptions options, 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 e303ce2279838c..cabe659f82bc11 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 @@ -33,28 +33,25 @@ private static bool HandleEnumerable( if (state.Current.PopStackOnEndCollection) { - state.Pop(writer, options); + state.Pop(); } return true; } - ResolvedReferenceHandling handling = options.HandleReference(ref state, out string referenceId, out bool writeAsReference, enumerable); - - if (handling == ResolvedReferenceHandling.Ignore) + if (options.ReferenceHandling.ShouldWritePreservedReferences()) { - //Reference loop found and ignore handling specified, do not write anything and pop the frame from the stack in case the array has an independant frame. - return WriteEndArray(ref state, writer, options); + if (WriteReferenceEnumerable(ref state, writer, options, enumerable)) + { + return WriteEndArray(ref state); + } } - state.Current.CollectionEnumerator = enumerable.GetEnumerator(); - - options.WriteStart(ref state.Current, ClassType.Enumerable, writer, options, writeAsReference: writeAsReference, referenceId: referenceId); - - if (handling == ResolvedReferenceHandling.IsReference) + else { - // We don't need to enumerate, this is a reference and was already written in WriteObjectOrArrayStart. - return WriteEndArray(ref state, writer, options); + state.Current.WriteObjectOrArrayStart(ClassType.Enumerable, writer, options); } + + state.Current.CollectionEnumerator = enumerable.GetEnumerator(); } if (state.Current.CollectionEnumerator.MoveNext()) @@ -89,27 +86,52 @@ private static bool HandleEnumerable( // We are done enumerating. writer.WriteEndArray(); + // Used for ReferenceHandling.Preserve if (state.Current.WriteWrappingBraceOnEndCollection) { writer.WriteEndObject(); } - return WriteEndArray(ref state, writer, options); + return WriteEndArray(ref state); } - private static bool WriteEndArray(ref WriteStack state, Utf8JsonWriter writer, JsonSerializerOptions options) + private static bool WriteEndArray(ref WriteStack state) { if (state.Current.PopStackOnEndCollection) { - state.Pop(writer, options); + state.Pop(); } else { - options.PopReference(ref state, true); state.Current.EndArray(); } return true; } + + + private static bool WriteReferenceEnumerable(ref WriteStack state, Utf8JsonWriter writer, JsonSerializerOptions options, IEnumerable enumerable) + { + ResolvedReferenceHandling handling = state.PreserveReference(enumerable, out string referenceId); + + if (handling == ResolvedReferenceHandling.IsReference) + { + // Object written before, write { "$ref": "#" } and finish. + state.Current.WriteReferenceObjectOrArrayStart(ClassType.Enumerable, writer, options, writeAsReference: true, referenceId: referenceId); + return true; + } + else if (handling == ResolvedReferenceHandling.Preserve) + { + // Reference-type array, write as object and append $id and $values, at the end it write EndObject token using WriteWrappingBraceOnEndCollection. + state.Current.WriteReferenceObjectOrArrayStart(ClassType.Enumerable, writer, options, writeAsReference: false, referenceId: referenceId); + } + else + { + // Value type or Immutable, fallback on regular Write method. + state.Current.WriteObjectOrArrayStart(ClassType.Enumerable, writer, options); + } + + return false; + } } } 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 23f3782c3fac95..e4dc1c8c462701 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 @@ -25,30 +25,19 @@ private static bool WriteObject( if (state.Current.CurrentValue == null) { state.Current.WriteObjectOrArrayStart(ClassType.Object, writer, options, writeNull: true); - return WriteEndObject(ref state, writer, options); + return WriteEndObject(ref state); } - //Handle reference here - //if first seen - //just write the property $id for objects; - //or write { "$id": "#", "$values": current array } for arrays. - //if seen before - //just write { "$ref": "#" } and finish processing the object/array. - - ResolvedReferenceHandling handling = options.HandleReference(ref state, out string referenceId, out bool writeAsReference, state.Current.CurrentValue); - - if (handling == ResolvedReferenceHandling.Ignore) + if (options.ReferenceHandling.ShouldWritePreservedReferences()) { - //Reference loop found, do not write anything and pop the frame from the stack. - return WriteEndObject(ref state, writer, options); + if (WriteReferenceObject(ref state, writer, options)) + { + return WriteEndObject(ref state); + } } - - //state.Current.WriteObjectOrArrayStart(ClassType.Object, writer, options); - options.WriteStart(ref state.Current, ClassType.Object, writer, options, writeAsReference: writeAsReference, referenceId: referenceId); - - if (handling == ResolvedReferenceHandling.IsReference) + else { - return WriteEndObject(ref state, writer, options); + state.Current.WriteObjectOrArrayStart(ClassType.Object, writer, options); } state.Current.MoveToNextProperty = true; @@ -72,14 +61,14 @@ private static bool WriteObject( } writer.WriteEndObject(); - return WriteEndObject(ref state, writer, options); + return WriteEndObject(ref state); } - private static bool WriteEndObject(ref WriteStack state, Utf8JsonWriter writer, JsonSerializerOptions options) + private static bool WriteEndObject(ref WriteStack state) { if (state.Current.PopStackOnEndObject) { - state.Pop(writer, options); + state.Pop(); } return true; @@ -175,5 +164,29 @@ private static void HandleObject( state.Current.MoveToNextProperty = true; } } + + private static bool WriteReferenceObject(ref WriteStack state, Utf8JsonWriter writer, JsonSerializerOptions options) + { + ResolvedReferenceHandling handling = state.PreserveReference(state.Current.CurrentValue, out string referenceId); + + if (handling == ResolvedReferenceHandling.IsReference) + { + // Object written before, write { "$ref": "#" } and finish. + state.Current.WriteReferenceObjectOrArrayStart(ClassType.Object, writer, options, writeAsReference: true, referenceId: referenceId); + return true; + } + else if (handling == ResolvedReferenceHandling.Preserve) + { + // Object reference, write start and append $id. + state.Current.WriteReferenceObjectOrArrayStart(ClassType.Object, writer, options, writeAsReference: false, referenceId: referenceId); + } + else + { + // Value type, fallback on regular Write method. + state.Current.WriteObjectOrArrayStart(ClassType.Object, writer, options); + } + + return false; + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs index 4e36fd74397580..0cc6f42acda9c7 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs @@ -140,22 +140,5 @@ private static void WriteCore(Utf8JsonWriter writer, object value, Type type, Js writer.Flush(); } - - - internal static void SetReferenceHandlingDelegates(JsonSerializerOptions options) - { - if (options.ReferenceHandling.PreserveHandlingOnSerialize == PreserveReferencesHandling.All) - { - options.WriteStart = WriteReferenceObjectOrArrayStart; - options.HandleReference = PreserveReferencesStrategy; - options.PopReference = (ref WriteStack _, bool __) => { }; //enpty delegate, we dont need to use the reference stack when optiong-in for preserve. - } - else - { - options.HandleReference = DefaultOnReferencesStrategy; - options.WriteStart = WriteObjectOrArrayStart; - options.PopReference = DefaultPopReference; - } - } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Stream.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Stream.cs index 4aa06ea4991292..ca3bd482296a98 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Stream.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Stream.cs @@ -72,7 +72,6 @@ private static async Task WriteAsyncCore(Stream utf8Json, object value, Type inp WriteStack state = default; state.Current.Initialize(inputType, options); state.Current.CurrentValue = value; - SetReferenceHandlingDelegates(options); bool isFinalBlock; int flushThreshold; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs index 53fe8f998df5e9..bafb8bc3d1e07e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs @@ -35,11 +35,6 @@ public sealed partial class JsonSerializerOptions private bool _propertyNameCaseInsensitive; private bool _writeIndented; - // Reference Handling delegates for Serialization. - internal ReferenceHandlingStrategy HandleReference = JsonSerializer.DefaultOnReferencesStrategy; - internal WriteStart WriteStart = JsonSerializer.WriteObjectOrArrayStart; - internal PopReference PopReference = JsonSerializer.DefaultPopReference; - /// /// Constructs a new instance. /// @@ -313,8 +308,6 @@ public ReferenceHandling ReferenceHandling VerifyMutable(); _referenceHandling = value; - - JsonSerializer.SetReferenceHandlingDelegates(this); } } 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 71af87a13e76b6..9614da44358a10 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 @@ -41,6 +41,7 @@ internal struct ReadStackFrame public bool IsPreserved; public bool IsPreservedArray; public bool DictionaryPropertyIsPreserved; + // maybe I can remove ShouldHandleReference if I use ReferenceId = null as false. public bool ShouldHandleReference; public bool ReadMetadataValue; public MetadataPropertyName MetadataProperty; @@ -171,9 +172,12 @@ public void Reset() JsonClassInfo = null; PropertyRefCache = null; ReturnValue = null; + // TODO: Try using a field condition to avoid setting these fields when feature is off. + //if(this.PreserveReference){ IsPreserved = false; DictionaryPropertyIsPreserved = false; IsPreservedArray = false; + //} EndObject(); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceHandling.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceHandling.cs index 962948a030e15a..7aea5f2069b907 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceHandling.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceHandling.cs @@ -40,6 +40,16 @@ private ReferenceHandling(PreserveReferencesHandling preserveHandlingOnSerialize _preserveHandlingOnDeserialize = preserveHandlingOnDeserialize; _loopHandling = loopHandling; } + + internal bool ShouldReadPreservedReferences() + { + return _preserveHandlingOnDeserialize == PreserveReferencesHandling.All; + } + + internal bool ShouldWritePreservedReferences() + { + return _preserveHandlingOnSerialize == PreserveReferencesHandling.All; + } } internal enum ReferenceLoopHandling diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceHandlingStrategy.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceHandlingStrategy.cs deleted file mode 100644 index 6dcd2c1929c63b..00000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceHandlingStrategy.cs +++ /dev/null @@ -1,209 +0,0 @@ -// 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 -{ - internal delegate ResolvedReferenceHandling ReferenceHandlingStrategy(ref WriteStack state, out string referenceId, out bool writeAsReference, object value); - - internal delegate void WriteStart(ref WriteStackFrame frame, ClassType classType, Utf8JsonWriter writer, JsonSerializerOptions options, bool writeNull = false, bool writeAsReference = false, string referenceId = null); - - internal delegate void PopReference(ref WriteStack state, bool isCollectionProperty); - - public static partial class JsonSerializer - { - internal static ResolvedReferenceHandling PreserveReferencesStrategy(ref WriteStack state, out string referenceId, out bool skip, object value) - { - // Avoid emitting metadata to value types. - Type currentType = state.Current.JsonPropertyInfo?.DeclaredPropertyType ?? state.Current.JsonClassInfo.Type; - if (currentType.IsValueType) - { - referenceId = null; - skip = false; - return ResolvedReferenceHandling.None; - } - - if (skip = state.GetPreservedReference(value, out referenceId)) - { - return ResolvedReferenceHandling.IsReference; - } - - return ResolvedReferenceHandling.Preserve; - } - - internal static ResolvedReferenceHandling IgnoreReferencesStrategy(ref WriteStack state, out string referenceId, out bool skip, object value) - { - if (!state.AddStackReference(value)) - { - //if reference wasn't added to the set, it means it was already there, therefore we should ignore it BUT not remove it from the set in order to keep validating against further references. - state.Current.KeepReferenceInSet = true; - skip = true; - referenceId = default; - return ResolvedReferenceHandling.Ignore; - } - - skip = default; - referenceId = default; - return ResolvedReferenceHandling.None; - } - - internal static ResolvedReferenceHandling DefaultOnReferencesStrategy(ref WriteStack state, out string referenceId, out bool skip, object value) - { - //params not meant for this code path. - skip = default; - referenceId = default; - return ResolvedReferenceHandling.None; - } - - internal static void WriteObjectOrArrayStart(ref WriteStackFrame frame, ClassType classType, Utf8JsonWriter writer, JsonSerializerOptions options, bool writeNull = false, bool writeAsReference = false, string referenceId = null) - { - if (frame.JsonPropertyInfo?.EscapedName.HasValue == true) - { - WriteObjectOrArrayStart(ref frame, classType, frame.JsonPropertyInfo.EscapedName.Value, writer, writeNull); - } - else if (frame.KeyName != null) - { - JsonEncodedText propertyName = JsonEncodedText.Encode(frame.KeyName, options.Encoder); - WriteObjectOrArrayStart(ref frame, classType, propertyName, writer, writeNull); - } - else - { - Debug.Assert(writeNull == false); - - // Write start without a property name. - if (classType == ClassType.Object || classType == ClassType.Dictionary) - { - writer.WriteStartObject(); - frame.StartObjectWritten = true; - } - else - { - Debug.Assert(classType == ClassType.Enumerable); - writer.WriteStartArray(); - } - } - } - - private static void WriteObjectOrArrayStart(ref WriteStackFrame frame, ClassType classType, JsonEncodedText propertyName, Utf8JsonWriter writer, bool writeNull) - { - if (writeNull) - { - writer.WriteNull(propertyName); - } - else if ((classType & (ClassType.Object | ClassType.Dictionary)) != 0) - { - writer.WriteStartObject(propertyName); - frame.StartObjectWritten = true; - } - else - { - Debug.Assert(classType == ClassType.Enumerable); - writer.WriteStartArray(propertyName); - } - } - - private static void WriteReferenceObjectOrArrayStart(ref WriteStackFrame frame, ClassType classType, Utf8JsonWriter writer, JsonSerializerOptions options, bool writeNull = false, bool writeAsReference = false, string referenceId = null) - { - if (frame.JsonPropertyInfo?.EscapedName.HasValue == true) - { - WriteReferenceObjectOrArrayStart(ref frame, classType, frame.JsonPropertyInfo.EscapedName.Value, writer, writeNull, writeAsReference, referenceId); - } - else if (frame.KeyName != null) - { - JsonEncodedText propertyName = JsonEncodedText.Encode(frame.KeyName, options.Encoder); - WriteReferenceObjectOrArrayStart(ref frame, classType, propertyName, writer, writeNull, writeAsReference, referenceId); - } - else - { - Debug.Assert(writeNull == false); - // Write start without a property name. - if (writeAsReference) - { - writer.WriteStartObject(); - writer.WriteString("$ref", referenceId); - writer.WriteEndObject(); - } - else if (classType == ClassType.Object || classType == ClassType.Dictionary) - { - writer.WriteStartObject(); - if (referenceId != null) - { - writer.WriteString("$id", referenceId); - } - frame.StartObjectWritten = true; - } - else - { - Debug.Assert(classType == ClassType.Enumerable); - if (referenceId != null) // wrap array into an object with $id and $values metadata properties. - { - writer.WriteStartObject(); - writer.WriteString("$id", referenceId); //it can be WriteString. - writer.WritePropertyName("$values"); - frame.WriteWrappingBraceOnEndCollection = true; - } - writer.WriteStartArray(); - } - } - } - - private static void WriteReferenceObjectOrArrayStart(ref WriteStackFrame frame, ClassType classType, JsonEncodedText propertyName, Utf8JsonWriter writer, bool writeNull, bool writeAsReference, string referenceId) - { - if (writeNull) - { - writer.WriteNull(propertyName); - } - else if (writeAsReference) //is a reference? write { "$ref": "1" } regardless of the type. - { - writer.WriteStartObject(propertyName); - writer.WriteString("$ref", referenceId.ToString()); - writer.WriteEndObject(); - } - else if ((classType & (ClassType.Object | ClassType.Dictionary)) != 0) - { - writer.WriteStartObject(propertyName); - frame.StartObjectWritten = true; - if (referenceId != null) - { - writer.WriteString("$id", referenceId); - } - } - else - { - Debug.Assert(classType == ClassType.Enumerable); - if (referenceId != null) // new reference? wrap array into an object with $id and $values metadata properties - { - writer.WriteStartObject(propertyName); - writer.WriteString("$id", referenceId); //it can be WriteString. - writer.WritePropertyName("$values"); - writer.WriteStartArray(); - frame.WriteWrappingBraceOnEndCollection = true; - } - else - { - writer.WriteStartArray(propertyName); - } - } - } - - private static void PopReference(ref WriteStack state, bool isCollectionProperty) - { - if (!state.Current.KeepReferenceInSet) // Only remove objects that are the first reference in the stack. - { - object value = isCollectionProperty ? - (IEnumerable)state.Current.JsonPropertyInfo.GetValueAsObject(state.Current.CurrentValue) : - state.Current.CurrentValue; - - state.PopStackReference(value); - } - } - - internal static void DefaultPopReference(ref WriteStack state, bool isCollectionProperty) - { - //Nothig to do when opting in for the default behavior. - } - } -} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs index 112f45e6d73449..010a59cd0ca039 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs @@ -16,7 +16,6 @@ internal struct WriteStack private int _index; private DefaultReferenceResolver _referenceResolver; - private HashSet _referenceStack; public void Push() { @@ -62,12 +61,9 @@ public void Push(JsonClassInfo nextClassInfo, object nextValue) } } - public void Pop(Utf8JsonWriter writer, JsonSerializerOptions options) + public void Pop() { Debug.Assert(_index > 0); - - options.PopReference(ref this, false); - Current = _previous[--_index]; } @@ -112,19 +108,25 @@ private void AppendPropertyName(StringBuilder sb, string propertyName) } } - public bool AddStackReference(object value) + internal ResolvedReferenceHandling PreserveReference(object value, out string referenceId) { - if (_referenceStack == null) + // Avoid emitting metadata to value types. + Type currentType = Current.JsonPropertyInfo?.DeclaredPropertyType ?? Current.JsonClassInfo.Type; + if (currentType.IsValueType) { - _referenceStack = new HashSet(ReferenceEqualsEqualityComparer.Comparer); + referenceId = default; + return ResolvedReferenceHandling.None; } - return _referenceStack.Add(value); - } + if (GetPreservedReference(value, out referenceId)) + { + return ResolvedReferenceHandling.IsReference; + } - public void PopStackReference(object value) => _referenceStack?.Remove(value); + return ResolvedReferenceHandling.Preserve; + } - // true if reference already existed; otherwise, false; + // true if reference already exists; otherwise, false; public bool GetPreservedReference(object value, out string id) { if (_referenceResolver == null) @@ -132,10 +134,10 @@ public bool GetPreservedReference(object value, out string id) _referenceResolver = new DefaultReferenceResolver(); } - bool isReference = _referenceResolver.IsReferenced(value); + bool handling = _referenceResolver.IsReferenced(value); id = _referenceResolver.GetReference(value); - return isReference; + return handling; } } } 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 6d690cad8404f2..4ba2db8f8cc4f5 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 @@ -29,7 +29,6 @@ internal struct WriteStackFrame public bool MoveToNextProperty; public bool WriteWrappingBraceOnEndCollection; - public bool KeepReferenceInSet; // The current property. public int PropertyEnumeratorIndex; 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 9cd7589737a4c0..f41f879064e9b8 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 @@ -258,5 +258,81 @@ public static void ThrowNotSupportedException_DeserializeCreateObjectDelegateIsN throw new NotSupportedException(SR.Format(SR.DeserializeMissingParameterlessConstructor, invalidType)); } } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowJsonException_MetadataValuesInvalidToken() + { + throw new JsonException(SR.MetadataInvalidTokenAfterValues); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowJsonException_MetadataReferenceNotFound() + { + throw new JsonException(SR.MetadataReferenceNotFound); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowJsonException_MetadataValueWasNotString() + { + throw new JsonException(SR.MetadataValueWasNotString); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties() + { + throw new JsonException(SR.MetadataReferenceCannotContainOtherProperties); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowJsonException_MetadataIdIsNotFirstProperty() + { + throw new JsonException(SR.MetadataIdIsNotFirstProperty); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowJsonException_MetadataMissingIdBeforeValues() + { + throw new JsonException(SR.MetadataMissingIdBeforeValues); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowJsonException_MetadataInvalidPropertyWithLeadingSign() + { + throw new JsonException(SR.MetadataInvalidPropertyWithLeadingSign); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowJsonException_MetadataDuplicateIdFound(string id) + { + throw new JsonException(SR.Format(SR.MetadataDuplicateIdFound, id)); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowJsonException_MetadataInvalidReferenceToValueType(Type propertyType) + { + throw new JsonException(SR.Format(SR.MetadataInvalidReferenceToValueType, propertyType)); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowJsonException_MetadataPreservedArrayInvalidProperty(Type propertyType) + { + throw new JsonException(SR.Format(SR.MetadataPreservedArrayFailed, + SR.MetadataPreservedArrayInvalidProperty, + SR.Format(SR.DeserializeUnableToConvertValue, propertyType))); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowJsonException_MetadataPreservedArrayValuesNotFound(Type propertyType) + { + throw new JsonException(SR.Format(SR.MetadataPreservedArrayFailed, + SR.MetadataPreservedArrayValuesNotFound, + SR.Format(SR.DeserializeUnableToConvertValue, propertyType))); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowJsonException_MetadataCannotParsePreservedObjectIntoImmutable(Type propertyType) + { + throw new JsonException(SR.Format(SR.MetadataCannotParsePreservedObjectToImmutable, propertyType)); + } } } diff --git a/src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlingTests.Deserialize.cs b/src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlingTests.Deserialize.cs index 72a03de437bb11..9a48e5f9c299d6 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlingTests.Deserialize.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlingTests.Deserialize.cs @@ -215,41 +215,6 @@ public static void DictionaryDuplicatedObject() Assert.Same(directory["555"], directory["557"]); } - [Fact] //This should not throw, since the references are in nested objects, not in the immutable dictionary itself. - public static void ImmutableDictionaryPreserveNestedObjects() - { - string json = - @"{ - ""Angela"": { - ""$id"": ""1"", - ""Name"": ""Angela"", - ""Subordinates"": { - ""$id"": ""2"", - ""$values"": [ - { - ""$id"": ""3"", - ""Name"": ""Carlos"", - ""Manager"": { - ""$ref"": ""1"" - } - } - ] - } - }, - ""Bob"": { - ""$id"": ""4"", - ""Name"": ""Bob"" - }, - ""Carlos"": { - ""$ref"": ""3"" - } - }"; - - ImmutableDictionary dictionary = JsonSerializer.Deserialize>(json, _deserializeOptions); - Assert.Same(dictionary["Angela"], dictionary["Angela"].Subordinates[0].Manager); - Assert.Same(dictionary["Carlos"], dictionary["Angela"].Subordinates[0]); - } - [Fact] public static void DictionaryOfArrays() { @@ -788,7 +753,7 @@ public static void ThrowOnStructWithReference() ]"; JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, _deserializeOptions)); - Assert.Equal("Reference objects to value types are not allowed.", ex.Message); + Assert.Contains("Invalid reference to value type ", ex.Message); } #endregion @@ -813,9 +778,11 @@ public static void ImmutableEnumerableAsRoot() ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, _deserializeOptions)); Assert.Equal("$", ex.Path); + Assert.Equal(string.Format("Cannot parse preserved object to Array or Immutable. Type {0}.", typeof(ImmutableList)), ex.Message); ex = Assert.Throws(() => JsonSerializer.Deserialize(json, _deserializeOptions)); Assert.Equal("$", ex.Path); + Assert.Equal(string.Format("Cannot parse preserved object to Array or Immutable. Type {0}.", typeof(EmployeeWithImmutable[])), ex.Message); } [Fact] @@ -829,6 +796,7 @@ public static void ImmutableDictionaryAsRoot() JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, _deserializeOptions)); Assert.Equal("$", ex.Path); + Assert.Equal(string.Format("Cannot parse preserved object to Array or Immutable. Type {0}.", typeof(ImmutableDictionary)), ex.Message); } [Fact] @@ -847,6 +815,7 @@ public static void ImmutableEnumerableAsProperty() ex = Assert.Throws(() => JsonSerializer.Deserialize(json, _deserializeOptions)); Assert.Equal("$.Subordinates", ex.Path); + Assert.Equal(string.Format("Cannot parse preserved object to Array or Immutable. Type {0}.", typeof(ImmutableList)), ex.Message); json = @"{ @@ -859,6 +828,7 @@ public static void ImmutableEnumerableAsProperty() ex = Assert.Throws(() => JsonSerializer.Deserialize(json, _deserializeOptions)); Assert.Equal("$.SubordinatesArray", ex.Path); + Assert.Equal(string.Format("Cannot parse preserved object to Array or Immutable. Type {0}.", typeof(EmployeeWithImmutable[])), ex.Message); } [Fact] @@ -875,10 +845,83 @@ public static void ImmutableDictionaryAsProperty() JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize(json, _deserializeOptions)); Assert.Equal("$.Contacts", ex.Path); + Assert.Equal(string.Format("Cannot parse preserved object to Array or Immutable. Type {0}.", typeof(ImmutableDictionary)), ex.Message); + } + + [Fact] + public static void ImmutableDictionaryPreserveNestedObjects() + { + string json = + @"{ + ""Angela"": { + ""$id"": ""1"", + ""Name"": ""Angela"", + ""Subordinates"": { + ""$id"": ""2"", + ""$values"": [ + { + ""$id"": ""3"", + ""Name"": ""Carlos"", + ""Manager"": { + ""$ref"": ""1"" + } + } + ] + } + }, + ""Bob"": { + ""$id"": ""4"", + ""Name"": ""Bob"" + }, + ""Carlos"": { + ""$ref"": ""3"" + } + }"; + + // Must not throw since the references are to nested objects, not the immutable dictionary itself. + ImmutableDictionary dictionary = JsonSerializer.Deserialize>(json, _deserializeOptions); + Assert.Same(dictionary["Angela"], dictionary["Angela"].Subordinates[0].Manager); + Assert.Same(dictionary["Carlos"], dictionary["Angela"].Subordinates[0]); } #endregion #region Ground Rules/Corner cases + + private class Order + { + public int ProductId { get; set; } + public int Quantity { get; set; } + } + + [Fact] + public static void OnlyStringTypeIsAllowed() + { + string json = @"{ + ""$id"": 1, + ""ProductId"": 1, + ""Quantity"": 10 + }"; + + JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize(json, _deserializeOptions)); + Assert.Equal("Value for metadata properties $id and $ref must be of type string.", ex.Message); + Assert.Equal("$.$id", ex.Path); + + json = @"[ + { + ""$id"": ""1"", + ""ProductId"": 1, + ""Quantity"": 10 + }, + { + ""$ref"": 1 + } + ]"; + + ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, _deserializeOptions)); + Assert.Equal("Value for metadata properties $id and $ref must be of type string.", ex.Message); + Assert.Equal("$[1].$ref", ex.Path); + } + #region Reference objects ($ref) [Fact] public static void ReferenceObjectsShouldNotContainMoreProperties() @@ -894,7 +937,18 @@ public static void ReferenceObjectsShouldNotContainMoreProperties() }"; JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize(json, _deserializeOptions)); - Assert.Equal("Reference objects cannot contain other properties.", ex.Message); + Assert.Equal("Reference objects must not contain other properties except for $ref.", ex.Message); + + //Regular dictionary key before $ref + json = @"{ + ""Angela"": { + ""Name"": ""Angela"", + ""$ref"": ""1"" + } + }"; + + ex = Assert.Throws(() => JsonSerializer.Deserialize>>(json, _deserializeOptions)); + Assert.Equal("Reference objects must not contain other properties except for $ref.", ex.Message); //Regular property after $ref json = @"{ @@ -907,7 +961,7 @@ public static void ReferenceObjectsShouldNotContainMoreProperties() }"; ex = Assert.Throws(() => JsonSerializer.Deserialize(json, _deserializeOptions)); - Assert.Equal("Reference objects cannot contain other properties.", ex.Message); + Assert.Equal("Reference objects must not contain other properties except for $ref.", ex.Message); //Metadata property before $ref json = @"{ @@ -920,7 +974,7 @@ public static void ReferenceObjectsShouldNotContainMoreProperties() }"; ex = Assert.Throws(() => JsonSerializer.Deserialize(json, _deserializeOptions)); - Assert.Equal("Reference objects cannot contain other properties.", ex.Message); + Assert.Equal("Reference objects must not contain other properties except for $ref.", ex.Message); //Metadata property after $ref json = @"{ @@ -933,7 +987,7 @@ public static void ReferenceObjectsShouldNotContainMoreProperties() }"; ex = Assert.Throws(() => JsonSerializer.Deserialize(json, _deserializeOptions)); - Assert.Equal("Reference objects cannot contain other properties.", ex.Message); + Assert.Equal("Reference objects must not contain other properties except for $ref.", ex.Message); } [Fact] @@ -971,7 +1025,7 @@ public static void MoreThanOneId() JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize(json, _deserializeOptions)); Assert.Equal("$", ex.Path); - Assert.Equal("The identifier must be the first property in the JSON object.", ex.Message); + Assert.Equal("The metadata property $id must be the first property in the JSON object.", ex.Message); } [Fact] @@ -986,7 +1040,16 @@ public static void IdIsNotFirstProperty() }"; JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize(json, _deserializeOptions)); - Assert.Equal("The identifier must be the first property in the JSON object.", ex.Message); + Assert.Equal("The metadata property $id must be the first property in the JSON object.", ex.Message); + Assert.Equal("$", ex.Path); + + json = @"{ + ""Name"": ""Angela"", + ""$id"": ""1"" + }"; + + ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, _deserializeOptions)); + Assert.Equal("The metadata property $id must be the first property in the JSON object.", ex.Message); Assert.Equal("$", ex.Path); } @@ -1007,7 +1070,7 @@ public static void DuplicatedId() JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, _deserializeOptions)); Assert.Equal("$[1].$id", ex.Path); - Assert.Equal("Duplicated $id \"1\" found while preserving reference.", ex.Message); + Assert.Equal("Duplicated identifier '1' found while reading preserved object.", ex.Message); } #endregion @@ -1019,7 +1082,7 @@ public static void PreservedArrayWithoutMetadata() JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, _deserializeOptions)); Assert.Equal("$", ex.Path); - Assert.Contains("Deserializaiton failed for one of these reasons:\n1. $values property was not present in preserved array.\n2. The JSON value could not be converted to ", ex.Message); + Assert.Contains("Deserialization failed for one of these reasons:\r\n1. Metadata $values property was not found in preserved array.\r\n2. The JSON value could not be converted to ", ex.Message); } [Fact] @@ -1030,8 +1093,9 @@ public static void PreservedArrayWithoutValues() }"; JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, _deserializeOptions)); - Assert.Equal("$.$id", ex.Path); // Not sure if is ok for the Path to have this value. - Assert.Contains("Deserializaiton failed for one of these reasons:\n1. $values property was not present in preserved array.\n2. The JSON value could not be converted to ", ex.Message); + // Not sure if is ok for Path to have this value. + Assert.Equal("$.$id", ex.Path); + Assert.Contains("Deserialization failed for one of these reasons:\r\n1. Metadata $values property was not found in preserved array.\r\n2. The JSON value could not be converted to ", ex.Message); } [Fact] @@ -1044,7 +1108,7 @@ public static void PreservedArrayWithoutId() JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, _deserializeOptions)); Assert.Equal("$.$values", ex.Path); - Assert.Equal("Preserved arrays canot lack an identifier.", ex.Message); + Assert.Equal("Missing $id before $values on preserved array.", ex.Message); } [Fact] @@ -1058,7 +1122,7 @@ public static void PreservedArrayValuesContainsNull() JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, _deserializeOptions)); Assert.Equal("$.$values", ex.Path); - Assert.Equal("Invalid array for $values property.", ex.Message); + Assert.Equal("Invalid token after $values metadata property.", ex.Message); } [Fact] @@ -1072,7 +1136,7 @@ public static void PreservedArrayValuesContainsValue() JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, _deserializeOptions)); Assert.Equal("$.$values", ex.Path); - Assert.Equal("Invalid array for $values property.", ex.Message); + Assert.Equal("Invalid token after $values metadata property.", ex.Message); } [Fact] @@ -1086,7 +1150,7 @@ public static void PreservedArrayValuesContainsObject() JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, _deserializeOptions)); Assert.Equal("$.$values", ex.Path); - Assert.Equal("Invalid array for $values property.", ex.Message); + Assert.Equal("Invalid token after $values metadata property.", ex.Message); } [Fact] @@ -1101,7 +1165,8 @@ public static void PreservedArrayExtraProperties() JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, _deserializeOptions)); Assert.Equal("$.LeadingProperty", ex.Path); - Assert.Contains("Deserializaiton failed for one of these reasons:\n1. Invalid property in preserved array.\n2. The JSON value could not be converted to ", ex.Message); + // is it safe to compare using CR+LF? My guess is that this test might fail on UNIX-based. + Assert.Contains("Deserialization failed for one of these reasons:\r\n1. Invalid property in preserved array.\r\n2. The JSON value could not be converted to ", ex.Message); json = @"{ ""$id"": ""1"", @@ -1112,7 +1177,7 @@ public static void PreservedArrayExtraProperties() ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, _deserializeOptions)); Assert.Equal("$.TrailingProperty", ex.Path); - Assert.Contains("Deserializaiton failed for one of these reasons:\n1. Invalid property in preserved array.\n2. The JSON value could not be converted to ", ex.Message); + Assert.Contains("Deserialization failed for one of these reasons:\r\n1. Invalid property in preserved array.\r\n2. The JSON value could not be converted to ", ex.Message); } #endregion @@ -1134,11 +1199,11 @@ public static void JsonObjectNonCollectionTest() //The reason for this message is that there is no reason for non-preserved arrays to contain $values, therefore we throw the same error for $.* JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize(json, _deserializeOptions)); - Assert.Equal("Properties that start with '$' are not allowed on preserve mode, you must either escape '$' or turn off preserve references.", ex.Message); + Assert.Equal("Properties that start with '$' are not allowed on preserve mode, either escape the character or turn off preserve references.", ex.Message); Assert.Equal("$.$values", ex.Path); ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, _deserializeOptions)); - Assert.Equal("Properties that start with '$' are not allowed on preserve mode, you must either escape '$' or turn off preserve references.", ex.Message); + Assert.Equal("Properties that start with '$' are not allowed on preserve mode, either escape the character or turn off preserve references.", ex.Message); Assert.Equal("$.$values", ex.Path); // $.* Not valid (i.e: $test) @@ -1148,11 +1213,11 @@ public static void JsonObjectNonCollectionTest() }"; ex = Assert.Throws(() => JsonSerializer.Deserialize(json, _deserializeOptions)); - Assert.Equal("Properties that start with '$' are not allowed on preserve mode, you must either escape '$' or turn off preserve references.", ex.Message); + Assert.Equal("Properties that start with '$' are not allowed on preserve mode, either escape the character or turn off preserve references.", ex.Message); Assert.Equal("$.$test", ex.Path); ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, _deserializeOptions)); - Assert.Equal("Properties that start with '$' are not allowed on preserve mode, you must either escape '$' or turn off preserve references.", ex.Message); + Assert.Equal("Properties that start with '$' are not allowed on preserve mode, either escape the character or turn off preserve references.", ex.Message); Assert.Equal("$.$test", ex.Path); json = @"{ @@ -1180,6 +1245,7 @@ public static void JsonObjectCollectionTest() }"; JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, _deserializeOptions)); + Assert.Equal("$.$ref", ex.Path); Assert.Equal("Reference not found.", ex.Message); // $id Valid under conditions: must be the first property in the object. @@ -1200,7 +1266,7 @@ public static void JsonObjectCollectionTest() }"; ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, _deserializeOptions)); - Assert.Equal("Properties that start with '$' are not allowed on preserve mode, you must either escape '$' or turn off preserve references.", ex.Message); + Assert.Equal("Properties that start with '$' are not allowed on preserve mode, either escape the character or turn off preserve references.", ex.Message); } #endregion #endregion