diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs index 4aa8377c12746c..bcc2ffec78ceed 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -222,7 +222,7 @@ public JsonSerializerOptions(System.Text.Json.JsonSerializerDefaults defaults) { public bool PropertyNameCaseInsensitive { get { throw null; } set { } } public System.Text.Json.JsonNamingPolicy? PropertyNamingPolicy { get { throw null; } set { } } public System.Text.Json.JsonCommentHandling ReadCommentHandling { get { throw null; } set { } } - public System.Text.Json.Serialization.ReferenceHandling ReferenceHandling { get { throw null; } set { } } + public System.Text.Json.Serialization.ReferenceHandler? ReferenceHandler { get { throw null; } set { } } public bool WriteIndented { get { throw null; } set { } } public System.Text.Json.Serialization.JsonConverter GetConverter(System.Type typeToConvert) { throw null; } } @@ -535,10 +535,22 @@ public JsonStringEnumConverter(System.Text.Json.JsonNamingPolicy? namingPolicy = public override bool CanConvert(System.Type typeToConvert) { throw null; } public override System.Text.Json.Serialization.JsonConverter CreateConverter(System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options) { throw null; } } - public sealed partial class ReferenceHandling + public abstract partial class ReferenceHandler { - internal ReferenceHandling() { } - public static System.Text.Json.Serialization.ReferenceHandling Default { get { throw null; } } - public static System.Text.Json.Serialization.ReferenceHandling Preserve { get { throw null; } } + protected ReferenceHandler() { } + public static System.Text.Json.Serialization.ReferenceHandler Preserve { get { throw null; } } + public abstract System.Text.Json.Serialization.ReferenceResolver CreateResolver(); + } + public sealed partial class ReferenceHandler : System.Text.Json.Serialization.ReferenceHandler where T : System.Text.Json.Serialization.ReferenceResolver, new() + { + public ReferenceHandler() { } + public override System.Text.Json.Serialization.ReferenceResolver CreateResolver() { throw null; } + } + public abstract partial class ReferenceResolver + { + protected ReferenceResolver() { } + public abstract void AddReference(string referenceId, object value); + public abstract string GetReference(object value, out bool alreadyExists); + public abstract object ResolveReference(string referenceId); } } diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index 44de8a1f85fb79..1a029bf64b2354 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -418,7 +418,7 @@ Either the JSON value is not in a supported format, or is out of bounds for a UInt16. - A possible object cycle was detected. This can either be due to a cycle or if the object depth is larger than the maximum allowed depth of {0}. Consider using ReferenceHandling.Preserve on JsonSerializerOptions to support cycles. + A possible object cycle was detected. This can either be due to a cycle or if the object depth is larger than the maximum allowed depth of {0}. Consider using ReferenceHandler.Preserve on JsonSerializerOptions to support cycles. Expected a number, but instead got empty string. @@ -480,7 +480,7 @@ The '$id' and '$ref' metadata properties must be JSON strings. Current token type is '{0}'. - Properties that start with '$' are not allowed on preserve mode, either escape the character or turn off preserve references by setting ReferenceHandling to ReferenceHandling.Default. + Properties that start with '$' are not allowed on preserve mode, either escape the character or turn off preserve references by setting ReferenceHandler to null. Members '{0}' and '{1}' on type '{2}' cannot both bind with parameter '{3}' in constructor '{4}' on deserialization. @@ -527,4 +527,4 @@ The value cannot be 'JsonIgnoreCondition.Always'. - \ No newline at end of file + diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index 7c82fc41548ad8..0dce72cc7b798b 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -123,7 +123,6 @@ - @@ -162,10 +161,14 @@ + + - + + + diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs index 4603b59c82f960..77c8ff882159ec 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs @@ -69,9 +69,7 @@ internal sealed override bool OnTryRead( ref ReadStack state, [MaybeNullWhen(false)] out TCollection value) { - bool shouldReadPreservedReferences = options.ReferenceHandling.ShouldReadPreservedReferences(); - - if (!state.SupportContinuation && !shouldReadPreservedReferences) + if (state.UseFastPath) { // Fast path that avoids maintaining state variables and dealing with preserved references. @@ -148,7 +146,8 @@ internal sealed override bool OnTryRead( } // Handle the metadata properties. - if (shouldReadPreservedReferences && state.Current.ObjectState < StackFrameObjectState.PropertyValue) + bool preserveReferences = options.ReferenceHandler != null; + if (preserveReferences && state.Current.ObjectState < StackFrameObjectState.PropertyValue) { if (JsonSerializer.ResolveMetadata(this, ref reader, ref state)) { @@ -175,10 +174,10 @@ internal sealed override bool OnTryRead( Debug.Assert(CanHaveIdMetadata); value = (TCollection)state.Current.ReturnValue!; - if (!state.ReferenceResolver.AddReferenceOnDeserialize(state.Current.MetadataId, value)) - { - ThrowHelper.ThrowJsonException_MetadataDuplicateIdFound(state.Current.MetadataId, ref state); - } + state.ReferenceResolver.AddReference(state.Current.MetadataId, value); + // Clear metadata name, if the next read fails + // we want to point the JSON path to the property's object. + state.Current.JsonPropertyName = null; } state.Current.ObjectState = StackFrameObjectState.CreatedObject; @@ -214,7 +213,7 @@ internal sealed override bool OnTryRead( state.Current.PropertyState = StackFramePropertyState.Name; // Verify property doesn't contain metadata. - if (shouldReadPreservedReferences) + if (preserveReferences) { ReadOnlySpan propertyName = reader.GetSpan(); if (propertyName.Length > 0 && propertyName[0] == '$') @@ -275,7 +274,7 @@ internal sealed override bool OnTryWrite( state.Current.ProcessedStartToken = true; writer.WriteStartObject(); - if (options.ReferenceHandling.ShouldWritePreservedReferences()) + if (options.ReferenceHandler != null) { if (JsonSerializer.WriteReferenceForObject(this, dictionary, ref state, writer) == MetadataPropertyName.Ref) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableDefaultConverter.cs index 6803e9d58454fc..9dd215a4287e1d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableDefaultConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableDefaultConverter.cs @@ -40,9 +40,7 @@ internal override bool OnTryRead( ref ReadStack state, [MaybeNullWhen(false)] out TCollection value) { - bool shouldReadPreservedReferences = options.ReferenceHandling.ShouldReadPreservedReferences(); - - if (!state.SupportContinuation && !shouldReadPreservedReferences) + if (state.UseFastPath) { // Fast path that avoids maintaining state variables and dealing with preserved references. @@ -91,13 +89,14 @@ internal override bool OnTryRead( { // Slower path that supports continuation and preserved references. + bool preserveReferences = options.ReferenceHandler != null; if (state.Current.ObjectState == StackFrameObjectState.None) { if (reader.TokenType == JsonTokenType.StartArray) { state.Current.ObjectState = StackFrameObjectState.PropertyValue; } - else if (shouldReadPreservedReferences) + else if (preserveReferences) { if (reader.TokenType != JsonTokenType.StartObject) { @@ -113,7 +112,7 @@ internal override bool OnTryRead( } // Handle the metadata properties. - if (shouldReadPreservedReferences && state.Current.ObjectState < StackFrameObjectState.PropertyValue) + if (preserveReferences && state.Current.ObjectState < StackFrameObjectState.PropertyValue) { if (JsonSerializer.ResolveMetadata(this, ref reader, ref state)) { @@ -137,10 +136,17 @@ internal override bool OnTryRead( if (state.Current.MetadataId != null) { value = (TCollection)state.Current.ReturnValue!; - if (!state.ReferenceResolver.AddReferenceOnDeserialize(state.Current.MetadataId, value)) - { - ThrowHelper.ThrowJsonException_MetadataDuplicateIdFound(state.Current.MetadataId, ref state); - } + + // TODO: https://github.com/dotnet/runtime/issues/37168 + //Separate logic for IEnumerable to call AddReference when the reader is at `$id`, in order to avoid remembering the last metadata. + + // Remember the prior metadata and temporarily use '$id' to write it in the path in case AddReference throws + // in this case, the last property seen will be '$values' when we reach this point. + byte[]? lastMetadataProperty = state.Current.JsonPropertyName; + state.Current.JsonPropertyName = JsonSerializer.s_idPropertyName; + + state.ReferenceResolver.AddReference(state.Current.MetadataId, value); + state.Current.JsonPropertyName = lastMetadataProperty; } state.Current.JsonPropertyInfo = state.Current.JsonClassInfo.ElementClassInfo!.PropertyInfoForClassInfo; @@ -247,13 +253,11 @@ internal sealed override bool OnTryWrite(Utf8JsonWriter writer, TCollection valu } else { - bool shouldWritePreservedReferences = options.ReferenceHandling.ShouldWritePreservedReferences(); - if (!state.Current.ProcessedStartToken) { state.Current.ProcessedStartToken = true; - if (!shouldWritePreservedReferences) + if (options.ReferenceHandler == null) { writer.WriteStartArray(); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs index 10327bc0438b28..c6624a33a75181 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs @@ -14,10 +14,9 @@ internal class ObjectDefaultConverter : JsonObjectConverter where T : notn { internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, [MaybeNullWhen(false)] out T value) { - bool shouldReadPreservedReferences = options.ReferenceHandling.ShouldReadPreservedReferences(); object obj; - if (!state.SupportContinuation && !shouldReadPreservedReferences) + if (state.UseFastPath) { // Fast path that avoids maintaining state variables and dealing with preserved references. @@ -76,7 +75,7 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, // Handle the metadata properties. if (state.Current.ObjectState < StackFrameObjectState.PropertyValue) { - if (shouldReadPreservedReferences) + if (options.ReferenceHandler != null) { if (JsonSerializer.ResolveMetadata(this, ref reader, ref state)) { @@ -106,10 +105,10 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, obj = state.Current.JsonClassInfo.CreateObject!()!; if (state.Current.MetadataId != null) { - if (!state.ReferenceResolver.AddReferenceOnDeserialize(state.Current.MetadataId, obj)) - { - ThrowHelper.ThrowJsonException_MetadataDuplicateIdFound(state.Current.MetadataId, ref state); - } + state.ReferenceResolver.AddReference(state.Current.MetadataId, obj); + // Clear metadata name, if the next read fails + // we want to point the JSON path to the property's object. + state.Current.JsonPropertyName = null; } state.Current.ReturnValue = obj; @@ -239,7 +238,7 @@ internal sealed override bool OnTryWrite(Utf8JsonWriter writer, T value, JsonSer { writer.WriteStartObject(); - if (options.ReferenceHandling.ShouldWritePreservedReferences()) + if (options.ReferenceHandler != null) { if (JsonSerializer.WriteReferenceForObject(this, objectValue, ref state, writer) == MetadataPropertyName.Ref) { @@ -294,7 +293,7 @@ internal sealed override bool OnTryWrite(Utf8JsonWriter writer, T value, JsonSer { writer.WriteStartObject(); - if (options.ReferenceHandling.ShouldWritePreservedReferences()) + if (options.ReferenceHandler != null) { if (JsonSerializer.WriteReferenceForObject(this, objectValue, ref state, writer) == MetadataPropertyName.Ref) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs index 1af7ab0be01f39..89ecc996e11a54 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs @@ -21,10 +21,9 @@ internal abstract partial class ObjectWithParameterizedConstructorConverter : { internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, [MaybeNullWhen(false)] out T value) { - bool shouldReadPreservedReferences = options.ReferenceHandling.ShouldReadPreservedReferences(); object obj; - if (!state.SupportContinuation && !shouldReadPreservedReferences) + if (state.UseFastPath) { // Fast path that avoids maintaining state variables. 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 deleted file mode 100644 index 06f4ed6cdf13dc..00000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/DefaultReferenceResolver.cs +++ /dev/null @@ -1,87 +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.Generic; - -namespace System.Text.Json.Serialization -{ - /// - /// The default ReferenceResolver implementation to handle duplicate object references. - /// - /// - /// It is currently a struct to save one unnecessary allcation while (de)serializing. - /// If we choose to expose the ReferenceResolver in a future, we may need to create an abstract class/interface and change this type to become a class that inherits from that abstract class/interface. - /// - internal struct DefaultReferenceResolver - { - private uint _referenceCount; - private readonly Dictionary? _referenceIdToObjectMap; - private readonly Dictionary? _objectToReferenceIdMap; - - public DefaultReferenceResolver(bool writing) - { - _referenceCount = default; - - if (writing) - { - // Comparer used here to always do a Reference Equality comparison on serialization which is where we use the objects as the TKey in our dictionary. - _objectToReferenceIdMap = new Dictionary(ReferenceEqualityComparer.Instance); - _referenceIdToObjectMap = null; - } - else - { - _referenceIdToObjectMap = new Dictionary(); - _objectToReferenceIdMap = null; - } - } - - /// - /// Adds an entry to the bag of references using the specified id and value. - /// This method gets called when an $id metadata property from a JSON object is read. - /// - /// The identifier of the respective JSON object or array. - /// The value of the respective CLR reference type object that results from parsing the JSON object. - /// True if the value was successfully added, false otherwise. - public bool AddReferenceOnDeserialize(string referenceId, object value) - { - return JsonHelpers.TryAdd(_referenceIdToObjectMap!, referenceId, value); - } - - /// - /// Gets the reference id of the specified value if exists; otherwise a new id is assigned. - /// This method gets called before a CLR object is written so we can decide whether to write $id and the rest of its properties or $ref and step into the next object. - /// The first $id value will be 1. - /// - /// The value of the CLR reference type object to get or add an id for. - /// The id realated to the object. - /// - public bool TryGetOrAddReferenceOnSerialize(object value, out string referenceId) - { - bool result = _objectToReferenceIdMap!.TryGetValue(value, out referenceId!); - if (!result) - { - _referenceCount++; - referenceId = _referenceCount.ToString(); - _objectToReferenceIdMap.Add(value, referenceId); - } - return result; - } - - /// - /// Resolves the CLR reference type object related to the specified reference id. - /// This method gets called when $ref metadata property is read. - /// - /// The id related to the returned object. - /// - public object ResolveReferenceOnDeserialize(string referenceId) - { - if (!_referenceIdToObjectMap!.TryGetValue(referenceId, out object? value)) - { - ThrowHelper.ThrowJsonException_MetadataReferenceNotFound(referenceId); - } - - return value; - } - } -} 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 edb6aebf97bd03..3375512cb05653 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,6 +10,9 @@ namespace System.Text.Json { public static partial class JsonSerializer { + internal static readonly byte[] s_idPropertyName + = new byte[] { (byte)'$', (byte)'i', (byte)'d' }; + /// /// Returns true if successful, false is the reader ran out of buffer. /// Sets state.Current.ReturnValue to the $ref target for MetadataRefProperty cases. @@ -123,7 +126,7 @@ internal static bool ResolveMetadata( string key = reader.GetString()!; // todo: https://github.com/dotnet/runtime/issues/32354 - state.Current.ReturnValue = state.ReferenceResolver.ResolveReferenceOnDeserialize(key); + state.Current.ReturnValue = state.ReferenceResolver.ResolveReference(key); state.Current.ObjectState = StackFrameObjectState.ReadAheadRefEndObject; } else if (state.Current.ObjectState == StackFrameObjectState.ReadIdValue) @@ -135,9 +138,6 @@ internal static bool ResolveMetadata( state.Current.MetadataId = reader.GetString(); - // Clear the MetadataPropertyName since we are done processing Id. - state.Current.JsonPropertyName = default; - if (converter.ClassType == ClassType.Enumerable) { // Need to Read $values property name. @@ -184,6 +184,8 @@ internal static bool ResolveMetadata( { if (reader.TokenType != JsonTokenType.PropertyName) { + // Missing $values, JSON path should point to the property's object. + state.Current.JsonPropertyName = null; ThrowHelper.ThrowJsonException_MetadataPreservedArrayValuesNotFound(converter.TypeToConvert); } 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 b7b4db728865a4..7ed9da9a775c3f 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 @@ -84,7 +84,7 @@ internal static ReadOnlySpan GetPropertyName( unescapedPropertyName = propertyName; } - if (options.ReferenceHandling.ShouldReadPreservedReferences()) + if (options.ReferenceHandler != null) { if (propertyName.Length > 0 && propertyName[0] == '$') { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleMetadata.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleMetadata.cs index 0b5061e624470b..7d5b504a2b1ae5 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleMetadata.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleMetadata.cs @@ -20,28 +20,33 @@ internal static MetadataPropertyName WriteReferenceForObject( ref WriteStack state, Utf8JsonWriter writer) { - MetadataPropertyName metadataToWrite; + MetadataPropertyName writtenMetadataName; // If the jsonConverter supports immutable dictionaries or value types, don't write any metadata if (!jsonConverter.CanHaveIdMetadata || jsonConverter.IsValueType) { - metadataToWrite = MetadataPropertyName.NoMetadata; - } - else if (state.ReferenceResolver.TryGetOrAddReferenceOnSerialize(currentValue, out string referenceId)) - { - Debug.Assert(referenceId != null); - writer.WriteString(s_metadataRef, referenceId); - writer.WriteEndObject(); - metadataToWrite = MetadataPropertyName.Ref; + writtenMetadataName = MetadataPropertyName.NoMetadata; } else { + + string referenceId = state.ReferenceResolver.GetReference(currentValue, out bool alreadyExists); Debug.Assert(referenceId != null); - writer.WriteString(s_metadataId, referenceId); - metadataToWrite = MetadataPropertyName.Id; + + if (alreadyExists) + { + writer.WriteString(s_metadataRef, referenceId); + writer.WriteEndObject(); + writtenMetadataName = MetadataPropertyName.Ref; + } + else + { + writer.WriteString(s_metadataId, referenceId); + writtenMetadataName = MetadataPropertyName.Id; + } } - return metadataToWrite; + return writtenMetadataName; } internal static MetadataPropertyName WriteReferenceForCollection( @@ -50,32 +55,37 @@ internal static MetadataPropertyName WriteReferenceForCollection( ref WriteStack state, Utf8JsonWriter writer) { - MetadataPropertyName metadataToWrite; + MetadataPropertyName writtenMetadataName; // If the jsonConverter supports immutable enumerables or value type collections, don't write any metadata if (!jsonConverter.CanHaveIdMetadata || jsonConverter.IsValueType) { writer.WriteStartArray(); - metadataToWrite = MetadataPropertyName.NoMetadata; - } - else if (state.ReferenceResolver.TryGetOrAddReferenceOnSerialize(currentValue, out string referenceId)) - { - Debug.Assert(referenceId != null); - writer.WriteStartObject(); - writer.WriteString(s_metadataRef, referenceId); - writer.WriteEndObject(); - metadataToWrite = MetadataPropertyName.Ref; + writtenMetadataName = MetadataPropertyName.NoMetadata; } else { - Debug.Assert(referenceId != null); - writer.WriteStartObject(); - writer.WriteString(s_metadataId, referenceId); - writer.WriteStartArray(s_metadataValues); - metadataToWrite = MetadataPropertyName.Id; + string referenceId = state.ReferenceResolver.GetReference(currentValue, out bool alreadyExists); + + if (alreadyExists) + { + Debug.Assert(referenceId != null); + writer.WriteStartObject(); + writer.WriteString(s_metadataRef, referenceId); + writer.WriteEndObject(); + writtenMetadataName = MetadataPropertyName.Ref; + } + else + { + Debug.Assert(referenceId != null); + writer.WriteStartObject(); + writer.WriteString(s_metadataId, referenceId); + writer.WriteStartArray(s_metadataValues); + writtenMetadataName = MetadataPropertyName.Id; + } } - return metadataToWrite; + return writtenMetadataName; } } } 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 05b2aec0a2faa3..98587cb9f0c99a 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 @@ -27,7 +27,7 @@ public sealed partial class JsonSerializerOptions private JsonNamingPolicy? _dictionaryKeyPolicy; private JsonNamingPolicy? _jsonPropertyNamingPolicy; private JsonCommentHandling _readCommentHandling; - private ReferenceHandling _referenceHandling = ReferenceHandling.Default; + private ReferenceHandler? _referenceHandler; private JavaScriptEncoder? _encoder = null; private JsonIgnoreCondition _defaultIgnoreCondition; @@ -66,7 +66,7 @@ public JsonSerializerOptions(JsonSerializerOptions options) _dictionaryKeyPolicy = options._dictionaryKeyPolicy; _jsonPropertyNamingPolicy = options._jsonPropertyNamingPolicy; _readCommentHandling = options._readCommentHandling; - _referenceHandling = options._referenceHandling; + _referenceHandler = options._referenceHandler; _encoder = options._encoder; _defaultIgnoreCondition = options._defaultIgnoreCondition; @@ -404,16 +404,15 @@ public bool WriteIndented } /// - /// Defines how references are treated when reading and writing JSON, this is convenient to deal with circularity. + /// Configures how object references are handled when reading and writing JSON. /// - public ReferenceHandling ReferenceHandling + public ReferenceHandler? ReferenceHandler { - get => _referenceHandling; + get => _referenceHandler; set { VerifyMutable(); - - _referenceHandling = value ?? throw new ArgumentNullException(nameof(value)); + _referenceHandler = value; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/PreserveReferenceHandler.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/PreserveReferenceHandler.cs new file mode 100644 index 00000000000000..a92b1ba9c26b60 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/PreserveReferenceHandler.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Text.Json.Serialization +{ + internal sealed class PreserveReferenceHandler : ReferenceHandler + { + public override ReferenceResolver CreateResolver() => throw new InvalidOperationException(); + + internal override ReferenceResolver CreateResolver(bool writing) => new PreserveReferenceResolver(writing); + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/PreserveReferenceResolver.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/PreserveReferenceResolver.cs new file mode 100644 index 00000000000000..dd9469fd9db682 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/PreserveReferenceResolver.cs @@ -0,0 +1,73 @@ +// Licensed to the.NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Diagnostics; + +namespace System.Text.Json.Serialization +{ + /// + /// The default ReferenceResolver implementation to handle duplicate object references. + /// + internal sealed class PreserveReferenceResolver : ReferenceResolver + { + private uint _referenceCount; + private readonly Dictionary? _referenceIdToObjectMap; + private readonly Dictionary? _objectToReferenceIdMap; + + public PreserveReferenceResolver(bool writing) + { + if (writing) + { + // Comparer used here does a reference equality comparison on serialization, which is where we use the objects as the dictionary keys. + _objectToReferenceIdMap = new Dictionary(ReferenceEqualityComparer.Instance); + } + else + { + _referenceIdToObjectMap = new Dictionary(); + } + } + + public override void AddReference(string referenceId, object value) + { + Debug.Assert(_referenceIdToObjectMap != null); + + if (!JsonHelpers.TryAdd(_referenceIdToObjectMap, referenceId, value)) + { + ThrowHelper.ThrowJsonException_MetadataDuplicateIdFound(referenceId); + } + } + + public override string GetReference(object value, out bool alreadyExists) + { + Debug.Assert(_objectToReferenceIdMap != null); + + if (_objectToReferenceIdMap.TryGetValue(value, out string? referenceId)) + { + alreadyExists = true; + } + else + { + _referenceCount++; + referenceId = _referenceCount.ToString(); + _objectToReferenceIdMap.Add(value, referenceId); + alreadyExists = false; + } + + return referenceId; + } + + public override object ResolveReference(string referenceId) + { + Debug.Assert(_referenceIdToObjectMap != null); + + if (!_referenceIdToObjectMap.TryGetValue(referenceId, out object? value)) + { + ThrowHelper.ThrowJsonException_MetadataReferenceNotFound(referenceId); + } + + return value; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs index 8914c9c665a2b6..80db2c446e7907 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs @@ -47,13 +47,18 @@ internal struct ReadStack public bool ReadAhead; // The bag of preservable references. - public DefaultReferenceResolver ReferenceResolver; + public ReferenceResolver ReferenceResolver; /// /// Whether we need to read ahead in the inner read loop. /// public bool SupportContinuation; + /// + /// Whether we can read without the need of saving state for stream and preserve references cases. + /// + public bool UseFastPath; + private void AddCurrent() { if (_previous == null) @@ -84,12 +89,14 @@ public void Initialize(Type type, JsonSerializerOptions options, bool supportCon // The initial JsonPropertyInfo will be used to obtain the converter. Current.JsonPropertyInfo = jsonClassInfo.PropertyInfoForClassInfo; - if (options.ReferenceHandling.ShouldReadPreservedReferences()) + bool preserveReferences = options.ReferenceHandler != null; + if (preserveReferences) { - ReferenceResolver = new DefaultReferenceResolver(writing: false); + ReferenceResolver = options.ReferenceHandler!.CreateResolver(writing: false); } SupportContinuation = supportContinuation; + UseFastPath = !supportContinuation && !preserveReferences; } public void Push() @@ -244,8 +251,10 @@ static void AppendStackFrame(StringBuilder sb, in ReadStackFrame frame) return; } - // Once all elements are read, the exception is not within the array. - if (frame.ObjectState < StackFrameObjectState.ReadElements) + // For continuation scenarios only, before or after all elements are read, the exception is not within the array. + if (frame.ObjectState == StackFrameObjectState.None || + frame.ObjectState == StackFrameObjectState.CreatedObject || + frame.ObjectState == StackFrameObjectState.ReadElements) { sb.Append('['); sb.Append(GetCount(enumerable)); 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/ReferenceHandler.cs similarity index 51% rename from src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceHandling.cs rename to src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceHandler.cs index 269e0cb84b1bb1..bf03378702536b 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/ReferenceHandler.cs @@ -9,21 +9,8 @@ namespace System.Text.Json.Serialization /// /// This class defines how the deals with references on serialization and deserialization. /// - public sealed class ReferenceHandling + public abstract class ReferenceHandler { - /// - /// Serialization does not support objects with cycles and does not preserve duplicate references. Metadata properties will not be written when serializing reference types and will be treated as regular properties on deserialize. - /// - /// - /// * On Serialize: - /// Treats duplicate object references as if they were unique and writes all their properties. - /// The serializer throws a if an object contains a cycle. - /// * On Deserialize: - /// Metadata properties (`$id`, `$values`, and `$ref`) will not be consumed and therefore will be treated as regular JSON properties. - /// The metadata properties can map to a real property on the returned object if the property names match, or will be added to the overflow dictionary, if one exists; otherwise, they are ignored. - /// - public static ReferenceHandling Default { get; } = new ReferenceHandling(PreserveReferencesHandling.None); - /// /// Metadata properties will be honored when deserializing JSON objects and arrays into reference types and written when serializing reference types. This is necessary to create round-trippable JSON from objects that contain cycles or duplicate references. /// @@ -36,7 +23,7 @@ public sealed class ReferenceHandling /// No metadata properties are written for value types. /// * On Deserialize: /// The metadata properties within the JSON that are used to preserve duplicated references and cycles will be honored as long as they are well-formed**. - /// For JSON objects that don't contain any metadata properties, the deserialization behavior is identical to . + /// For JSON objects that don't contain any metadata properties, the deserialization behavior is identical to . /// For value types: /// * The `$id` metadata property is ignored. /// * A is thrown if a `$ref` metadata property is found within the JSON object. @@ -51,47 +38,18 @@ public sealed class ReferenceHandling /// 7) The `$values` metadata property is only valid when referring to enumerable types. /// If the JSON is not well-formed, a is thrown. /// - public static ReferenceHandling Preserve { get; } = new ReferenceHandling(PreserveReferencesHandling.All); - - private readonly bool _shouldReadPreservedReferences; - private readonly bool _shouldWritePreservedReferences; + public static ReferenceHandler Preserve { get; } = new PreserveReferenceHandler(); /// - /// Creates a new instance of using the specified + /// Returns the used for each serialization call. /// - /// The specified behavior for write/read preserved references. - private ReferenceHandling(PreserveReferencesHandling handling) : this(handling, handling) { } - - // For future, someone may want to define their own custom Handler with different behaviors of PreserveReferenceHandling on Serialize vs Deserialize. - private ReferenceHandling(PreserveReferencesHandling preserveHandlingOnSerialize, PreserveReferencesHandling preserveHandlingOnDeserialize) - { - _shouldReadPreservedReferences = preserveHandlingOnDeserialize == PreserveReferencesHandling.All; - _shouldWritePreservedReferences = preserveHandlingOnSerialize == PreserveReferencesHandling.All; - } + /// The resolver to use for serialization and deserialization. + public abstract ReferenceResolver CreateResolver(); - internal bool ShouldReadPreservedReferences() - { - return _shouldReadPreservedReferences; - } - - internal bool ShouldWritePreservedReferences() - { - return _shouldWritePreservedReferences; - } - } - - /// - /// Defines behaviors to preserve references of JSON complex types. - /// - internal enum PreserveReferencesHandling - { - /// - /// Preserved objects and arrays will not be written/read. - /// - None = 0, /// - /// Preserved objects and arrays will be written/read. + /// Optimization for the resolver used when is set in ; + /// we pass a flag signaling whether this is called from serialization or deserialization to save one dictionary instantiation. /// - All = 1, + internal virtual ReferenceResolver CreateResolver(bool writing) => CreateResolver(); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceHandlerOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceHandlerOfT.cs new file mode 100644 index 00000000000000..18fb4501abd413 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceHandlerOfT.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Text.Json.Serialization +{ + /// + /// This class defines how the deals with references on serialization and deserialization. + /// + /// The type of the to create on each serialization or deserialization call. + public sealed class ReferenceHandler : ReferenceHandler + where T: ReferenceResolver, new() + { + /// + /// Creates a new of type used for each serialization call. + /// + /// The new resolver to use for serialization and deserialization. + public override ReferenceResolver CreateResolver() => new T(); + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceResolver.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceResolver.cs new file mode 100644 index 00000000000000..a5a0147b28c18a --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceResolver.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Text.Json.Serialization +{ + /// + /// This class defines how the deals with references on serialization and deserialization. + /// Defines the core behavior of preserving references on serialization and deserialization. + /// + public abstract class ReferenceResolver + { + /// + /// Adds an entry to the bag of references using the specified id and value. + /// This method gets called when an $id metadata property from a JSON object is read. + /// + /// The identifier of the respective JSON object or array. + /// The value of the respective CLR reference type object that results from parsing the JSON object. + public abstract void AddReference(string referenceId, object value); + + /// + /// Gets the reference identifier of the specified value if exists; otherwise a new id is assigned. + /// This method gets called before a CLR object is written so we can decide whether to write $id and enumerate the rest of its properties or $ref and step into the next object. + /// + /// The value of the CLR reference type object to get an id for. + /// When this method returns, if a reference to value already exists; otherwise, . + /// The reference id for the specified object. + public abstract string GetReference(object value, out bool alreadyExists); + + /// + /// Returns the CLR reference type object related to the specified reference id. + /// This method gets called when $ref metadata property is read. + /// + /// The reference id related to the returned object. + /// The reference type object related to specified reference id. + public abstract object ResolveReference(string referenceId); + } +} 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 16df62ffd10ec5..e2efb6492c24d3 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 @@ -35,7 +35,7 @@ internal struct WriteStack public bool IsContinuation => _continuationCount != 0; // The bag of preservable references. - public DefaultReferenceResolver ReferenceResolver; + public ReferenceResolver ReferenceResolver; /// /// Internal flag to let us know that we need to read ahead in the inner read loop. @@ -77,9 +77,10 @@ public void Initialize(Type type, JsonSerializerOptions options, bool supportCon Current.DeclaredJsonPropertyInfo = jsonClassInfo.PropertyInfoForClassInfo; } - if (options.ReferenceHandling.ShouldWritePreservedReferences()) + bool preserveReferences = options.ReferenceHandler != null; + if (preserveReferences) { - ReferenceResolver = new DefaultReferenceResolver(writing: true); + ReferenceResolver = options.ReferenceHandler!.CreateResolver(writing: true); } SupportContinuation = supportContinuation; 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 0d7bd68400ee44..71d3f2ada0f49a 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 @@ -509,11 +509,8 @@ public static void ThrowJsonException_MetadataInvalidPropertyWithLeadingDollarSi [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] - public static void ThrowJsonException_MetadataDuplicateIdFound(string id, ref ReadStack state) + public static void ThrowJsonException_MetadataDuplicateIdFound(string id) { - // Set so JsonPath throws exception with $id in it. - state.Current.JsonPropertyName = JsonSerializer.s_metadataId.EncodedUtf8Bytes.ToArray(); - ThrowJsonException(SR.Format(SR.MetadataDuplicateIdFound, id)); } diff --git a/src/libraries/System.Text.Json/tests/Serialization/ConstructorTests/ConstructorTests.Exceptions.cs b/src/libraries/System.Text.Json/tests/Serialization/ConstructorTests/ConstructorTests.Exceptions.cs index e47b1e98156cde..1f6c8add1e0873 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/ConstructorTests/ConstructorTests.Exceptions.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/ConstructorTests/ConstructorTests.Exceptions.cs @@ -68,7 +68,7 @@ public void LeadingReferenceMetadataNotSupported() // Metadata not supported with preserve ref feature on. - var options = new JsonSerializerOptions { ReferenceHandling = ReferenceHandling.Preserve }; + var options = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve }; NotSupportedException ex = Assert.Throws( () => Serializer.Deserialize(json, options)); @@ -102,7 +102,7 @@ public void RandomReferenceMetadataNotSupported() // Metadata not supported with preserve ref feature on. - var options = new JsonSerializerOptions { ReferenceHandling = ReferenceHandling.Preserve }; + var options = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve }; NotSupportedException ex = Assert.Throws(() => Serializer.Deserialize(json, options)); string exStr = ex.ToString(); diff --git a/src/libraries/System.Text.Json/tests/Serialization/OptionsTests.cs b/src/libraries/System.Text.Json/tests/Serialization/OptionsTests.cs index e86537cbac1f16..2d5c663f71ece2 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/OptionsTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/OptionsTests.cs @@ -467,13 +467,13 @@ public static void CopyConstructor_OriginalLocked() // Perform serialization with options, after which it will be locked. JsonSerializer.Serialize("1", options); - Assert.Throws(() => options.ReferenceHandling = ReferenceHandling.Preserve); + Assert.Throws(() => options.ReferenceHandler = ReferenceHandler.Preserve); var newOptions = new JsonSerializerOptions(options); VerifyOptionsEqual(options, newOptions); // No exception is thrown on mutating the new options instance because it is "unlocked". - newOptions.ReferenceHandling = ReferenceHandling.Preserve; + newOptions.ReferenceHandler = ReferenceHandler.Preserve; } [Fact] @@ -582,9 +582,9 @@ private static JsonSerializerOptions GetFullyPopulatedOptionsInstance() options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; options.DictionaryKeyPolicy = new SimpleSnakeCasePolicy(); } - else if (propertyType == typeof(ReferenceHandling)) + else if (propertyType == typeof(ReferenceHandler)) { - options.ReferenceHandling = ReferenceHandling.Preserve; + options.ReferenceHandler = ReferenceHandler.Preserve; } else if (propertyType.IsValueType) { diff --git a/src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlingTests.Deserialize.cs b/src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlerTests.Deserialize.cs similarity index 98% rename from src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlingTests.Deserialize.cs rename to src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlerTests.Deserialize.cs index a20b2e1580e536..65de48737903e8 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlingTests.Deserialize.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlerTests.Deserialize.cs @@ -9,9 +9,9 @@ namespace System.Text.Json.Serialization.Tests { - public static partial class ReferenceHandlingTests + public static partial class ReferenceHandlerTests { - private static readonly JsonSerializerOptions s_deserializerOptionsPreserve = new JsonSerializerOptions { ReferenceHandling = ReferenceHandling.Preserve }; + private static readonly JsonSerializerOptions s_deserializerOptionsPreserve = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve }; private class EmployeeWithContacts { @@ -636,7 +636,7 @@ public static void DeserializeWithListConverter() var options = new JsonSerializerOptions { - ReferenceHandling = ReferenceHandling.Preserve, + ReferenceHandler = ReferenceHandler.Preserve, Converters = { new ListOfEmployeeConverter() } }; Employee angela = JsonSerializer.Deserialize(json, options); @@ -1199,7 +1199,7 @@ public static void ReferenceObjectBeforePreservedObject() [MemberData(nameof(ReadSuccessCases))] public static void ReadTestClassesWithExtensionOption(Type classType, byte[] data) { - var options = new JsonSerializerOptions { ReferenceHandling = ReferenceHandling.Preserve }; + var options = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve }; object obj = JsonSerializer.Deserialize(data, classType, options); Assert.IsAssignableFrom(obj); ((ITestClass)obj).Verify(); @@ -1276,6 +1276,32 @@ public static void DuplicatedId() Assert.Contains("'1'", ex.Message); } + class ClassWithTwoListProperties + { + public List List1 { get; set; } + public List List2 { get; set; } + } + + [Fact] + public static void DuplicatedIdArray() + { + string json = @"{ + ""List1"": { + ""$id"": ""1"", + ""$values"": [] + }, + ""List2"": { + ""$id"": ""1"", + ""$values"": [] + } + }"; + + JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize(json, s_deserializerOptionsPreserve)); + + Assert.Equal("$.List2.$id", ex.Path); + Assert.Contains("'1'", ex.Message); + } + [Theory] [InlineData(@"{""$id"":""A"", ""Manager"":{""$ref"":""A""}}")] [InlineData(@"{""$id"":""00000000-0000-0000-0000-000000000000"", ""Manager"":{""$ref"":""00000000-0000-0000-0000-000000000000""}}")] diff --git a/src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlingTests.Serialize.cs b/src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlerTests.Serialize.cs similarity index 98% rename from src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlingTests.Serialize.cs rename to src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlerTests.Serialize.cs index 4271db99f0d609..364e639324a506 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlingTests.Serialize.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlerTests.Serialize.cs @@ -9,9 +9,9 @@ namespace System.Text.Json.Serialization.Tests { - public static partial class ReferenceHandlingTests + public static partial class ReferenceHandlerTests { - private static readonly JsonSerializerOptions s_serializerOptionsPreserve = new JsonSerializerOptions { ReferenceHandling = ReferenceHandling.Preserve }; + private static readonly JsonSerializerOptions s_serializerOptionsPreserve = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve }; private static readonly JsonSerializerSettings s_newtonsoftSerializerSettingsPreserve = new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.All, ReferenceLoopHandling = ReferenceLoopHandling.Serialize }; private class Employee diff --git a/src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlingTests.cs b/src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlerTests.cs similarity index 81% rename from src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlingTests.cs rename to src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlerTests.cs index cc01df11eb493c..fc5659e41e4699 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlingTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlerTests.cs @@ -3,13 +3,15 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using System.Collections.Immutable; using System.Text.Encodings.Web; +using System.Text.Json.Tests; using Newtonsoft.Json; using Xunit; namespace System.Text.Json.Serialization.Tests { - public static partial class ReferenceHandlingTests + public static partial class ReferenceHandlerTests { [Fact] @@ -21,12 +23,6 @@ public static void ThrowByDefaultOnLoop() JsonException ex = Assert.Throws(() => JsonSerializer.Serialize(a)); } - [Fact] - public static void ThrowWhenPassingNullToReferenceHandling() - { - Assert.Throws(() => new JsonSerializerOptions { ReferenceHandling = null }); - } - #region Root Object [Fact] public static void ObjectLoop() @@ -203,7 +199,7 @@ public static void UnicodePropertyNames() var optionsWithEncoder = new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - ReferenceHandling = ReferenceHandling.Preserve + ReferenceHandler = ReferenceHandler.Preserve }; json = JsonSerializer.Serialize(obj, optionsWithEncoder); Assert.StartsWith("{\"$id\":\"1\",", json); @@ -393,7 +389,7 @@ public static void UnicodeDictionaryKeys() var optionsWithEncoder = new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - ReferenceHandling = ReferenceHandling.Preserve + ReferenceHandler = ReferenceHandler.Preserve }; json = JsonSerializer.Serialize(obj, optionsWithEncoder); Assert.Equal("{\"$id\":\"1\",\"A\u0467\":1}", json); @@ -536,5 +532,171 @@ public static void ArrayPreserveDuplicateDictionaries() Assert.Same(rootCopy[0], rootCopy[1]); } #endregion + + #region ReferenceResolver + [Fact] + public static void CustomReferenceResolver() + { + string json = @"[ + { + ""$id"": ""0b64ffdf-d155-44ad-9689-58d9adb137f3"", + ""Name"": ""John Smith"", + ""Spouse"": { + ""$id"": ""ae3c399c-058d-431d-91b0-a36c266441b9"", + ""Name"": ""Jane Smith"", + ""Spouse"": { + ""$ref"": ""0b64ffdf-d155-44ad-9689-58d9adb137f3"" + } + } + }, + { + ""$ref"": ""ae3c399c-058d-431d-91b0-a36c266441b9"" + } +]"; + var options = new JsonSerializerOptions + { + WriteIndented = true, + ReferenceHandler = new ReferenceHandler() + }; + ImmutableArray people = JsonSerializer.Deserialize>(json, options); + + Assert.Equal(2, people.Length); + + PersonReference john = people[0]; + PersonReference jane = people[1]; + + Assert.Same(john, jane.Spouse); + Assert.Same(jane, john.Spouse); + + Assert.Equal(json, JsonSerializer.Serialize(people, options)); + } + + [Fact] + public static void CustomReferenceResolverPersistent() + { + var options = new JsonSerializerOptions + { + WriteIndented = true, + ReferenceHandler = new PresistentGuidReferenceHandler + { + // Re-use the same resolver instance across all (de)serialiations based on this options instance. + PersistentResolver = new GuidReferenceResolver() + } + }; + + string json = +@"[ + { + ""$id"": ""0b64ffdf-d155-44ad-9689-58d9adb137f3"", + ""Name"": ""John Smith"", + ""Spouse"": { + ""$id"": ""ae3c399c-058d-431d-91b0-a36c266441b9"", + ""Name"": ""Jane Smith"", + ""Spouse"": { + ""$ref"": ""0b64ffdf-d155-44ad-9689-58d9adb137f3"" + } + } + }, + { + ""$ref"": ""ae3c399c-058d-431d-91b0-a36c266441b9"" + } +]"; + ImmutableArray firstListOfPeople = JsonSerializer.Deserialize>(json, options); + + json = +@"[ + { + ""$ref"": ""0b64ffdf-d155-44ad-9689-58d9adb137f3"" + }, + { + ""$ref"": ""ae3c399c-058d-431d-91b0-a36c266441b9"" + } +]"; + ImmutableArray secondListOfPeople = JsonSerializer.Deserialize>(json, options); + + Assert.Same(firstListOfPeople[0], secondListOfPeople[0]); + Assert.Same(firstListOfPeople[1], secondListOfPeople[1]); + Assert.Same(firstListOfPeople[0].Spouse, secondListOfPeople[0].Spouse); + Assert.Same(firstListOfPeople[1].Spouse, secondListOfPeople[1].Spouse); + + Assert.Equal(json, JsonSerializer.Serialize(secondListOfPeople, options)); + } + + internal class PresistentGuidReferenceHandler : ReferenceHandler + { + public ReferenceResolver PersistentResolver { get; set; } + public override ReferenceResolver CreateResolver() => PersistentResolver; + } + + public class GuidReferenceResolver : ReferenceResolver + { + private readonly IDictionary _people = new Dictionary(); + + public override object ResolveReference(string referenceId) + { + Guid id = new Guid(referenceId); + + PersonReference p; + _people.TryGetValue(id, out p); + + return p; + } + + public override string GetReference(object value, out bool alreadyExists) + { + PersonReference p = (PersonReference)value; + + alreadyExists = _people.ContainsKey(p.Id); + _people[p.Id] = p; + + return p.Id.ToString(); + } + + public override void AddReference(string reference, object value) + { + Guid id = new Guid(reference); + PersonReference person = (PersonReference)value; + person.Id = id; + _people[id] = person; + } + } + + [Fact] + public static void TestBadReferenceResolver() + { + var options = new JsonSerializerOptions { ReferenceHandler = new ReferenceHandler() }; + + PersonReference angela = new PersonReference { Name = "Angela" }; + PersonReference bob = new PersonReference { Name = "Bob" }; + + angela.Spouse = bob; + bob.Spouse = angela; + + // Nothing is preserved, hence MaxDepth will be reached. + Assert.Throws(() => JsonSerializer.Serialize(angela, options)); + } + + class BadReferenceResolver : ReferenceResolver + { + private int _count; + public override void AddReference(string referenceId, object value) + { + throw new NotImplementedException(); + } + + public override string GetReference(object value, out bool alreadyExists) + { + alreadyExists = false; + _count++; + + return _count.ToString(); + } + + public override object ResolveReference(string referenceId) + { + throw new NotImplementedException(); + } + } + #endregion } } diff --git a/src/libraries/System.Text.Json/tests/Serialization/Stream.Collections.cs b/src/libraries/System.Text.Json/tests/Serialization/Stream.Collections.cs index b90b50dba8ac08..0a07521629313f 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/Stream.Collections.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/Stream.Collections.cs @@ -47,7 +47,7 @@ private static async Task RunTest() var optionsWithPreservedReferenceHandling = new JsonSerializerOptions(options) { - ReferenceHandling = ReferenceHandling.Preserve + ReferenceHandler = ReferenceHandler.Preserve }; object obj = GetPopulatedCollection(type, thresholdSize); @@ -98,7 +98,7 @@ private static async Task TestDeserialization( // TODO: https://github.com/dotnet/runtime/issues/35611. // Can't control order of dictionary elements when serializing, so reference metadata might not match up. - if (!(DictionaryTypes().Contains(type) && options.ReferenceHandling == ReferenceHandling.Preserve)) + if(!(DictionaryTypes().Contains(type) && options.ReferenceHandler == ReferenceHandler.Preserve)) { JsonTestHelper.AssertJsonEqual(expectedJson, serialized); } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj index 2693078629bfcb..ddfccacb6f8a5e 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj @@ -101,9 +101,9 @@ - - - + + +