Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/dotnet/runtime.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Cantu <dacantu@microsoft.com>2020-06-04 23:46:21 +0300
committerGitHub <noreply@github.com>2020-06-04 23:46:21 +0300
commit74c06a9f83f078ab045ff046f4f72e2a7601f0a2 (patch)
tree53ee956961cca47eff21b96bfdb4658e0b28e8af
parentd224df18b9f1a482f841998831742148a59244c3 (diff)
Expose ReferenceResolver and rename ReferenceHandling to ReferenceHandler (#36829) (#37296)
* Expose ReferenceResolver and rename ReferenceHandling to ReferenceHandler * Address some feedback * Address feedback * Clean-up code * Change messages in string.resx * Add test for a badly implemented resolver * Address feedback.
-rw-r--r--src/libraries/System.Text.Json/ref/System.Text.Json.cs22
-rw-r--r--src/libraries/System.Text.Json/src/Resources/Strings.resx6
-rw-r--r--src/libraries/System.Text.Json/src/System.Text.Json.csproj7
-rw-r--r--src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs19
-rw-r--r--src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableDefaultConverter.cs28
-rw-r--r--src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs17
-rw-r--r--src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs3
-rw-r--r--src/libraries/System.Text.Json/src/System/Text/Json/Serialization/DefaultReferenceResolver.cs87
-rw-r--r--src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleMetadata.cs10
-rw-r--r--src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs2
-rw-r--r--src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleMetadata.cs66
-rw-r--r--src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs13
-rw-r--r--src/libraries/System.Text.Json/src/System/Text/Json/Serialization/PreserveReferenceHandler.cs13
-rw-r--r--src/libraries/System.Text.Json/src/System/Text/Json/Serialization/PreserveReferenceResolver.cs73
-rw-r--r--src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs19
-rw-r--r--src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceHandler.cs (renamed from src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceHandling.cs)60
-rw-r--r--src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceHandlerOfT.cs20
-rw-r--r--src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReferenceResolver.cs38
-rw-r--r--src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs7
-rw-r--r--src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs5
-rw-r--r--src/libraries/System.Text.Json/tests/Serialization/ConstructorTests/ConstructorTests.Exceptions.cs4
-rw-r--r--src/libraries/System.Text.Json/tests/Serialization/OptionsTests.cs8
-rw-r--r--src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlerTests.Deserialize.cs (renamed from src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlingTests.Deserialize.cs)34
-rw-r--r--src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlerTests.Serialize.cs (renamed from src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlingTests.Serialize.cs)4
-rw-r--r--src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlerTests.cs (renamed from src/libraries/System.Text.Json/tests/Serialization/ReferenceHandlingTests.cs)180
-rw-r--r--src/libraries/System.Text.Json/tests/Serialization/Stream.Collections.cs4
-rw-r--r--src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj6
27 files changed, 496 insertions, 259 deletions
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 4aa8377c127..bcc2ffec78c 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 @@ namespace System.Text.Json
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 @@ namespace System.Text.Json.Serialization
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<T> : 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 44de8a1f85f..1a029bf64b2 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 @@
<value>Either the JSON value is not in a supported format, or is out of bounds for a UInt16.</value>
</data>
<data name="SerializerCycleDetected" xml:space="preserve">
- <value>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.</value>
+ <value>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.</value>
</data>
<data name="EmptyStringToInitializeNumber" xml:space="preserve">
<value>Expected a number, but instead got empty string.</value>
@@ -480,7 +480,7 @@
<value>The '$id' and '$ref' metadata properties must be JSON strings. Current token type is '{0}'.</value>
</data>
<data name="MetadataInvalidPropertyWithLeadingDollarSign" xml:space="preserve">
- <value>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.</value>
+ <value>Properties that start with '$' are not allowed on preserve mode, either escape the character or turn off preserve references by setting ReferenceHandler to null.</value>
</data>
<data name="MultipleMembersBindWithConstructorParameter" xml:space="preserve">
<value>Members '{0}' and '{1}' on type '{2}' cannot both bind with parameter '{3}' in constructor '{4}' on deserialization.</value>
@@ -527,4 +527,4 @@
<data name="DefaultIgnoreConditionInvalid" xml:space="preserve">
<value>The value cannot be 'JsonIgnoreCondition.Always'.</value>
</data>
-</root> \ No newline at end of file
+</root>
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 7c82fc41548..0dce72cc7b7 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 @@
<Compile Include="System\Text\Json\Serialization\Converters\Value\UInt32Converter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Value\UInt64Converter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Value\UriConverter.cs" />
- <Compile Include="System\Text\Json\Serialization\DefaultReferenceResolver.cs" />
<Compile Include="System\Text\Json\Serialization\JsonCamelCaseNamingPolicy.cs" />
<Compile Include="System\Text\Json\Serialization\JsonClassInfo.cs" />
<Compile Include="System\Text\Json\Serialization\JsonClassInfo.Cache.cs" />
@@ -162,10 +161,14 @@
<Compile Include="System\Text\Json\Serialization\MetadataPropertyName.cs" />
<Compile Include="System\Text\Json\Serialization\ParameterRef.cs" />
<Compile Include="System\Text\Json\Serialization\PooledByteBufferWriter.cs" />
+ <Compile Include="System\Text\Json\Serialization\PreserveReferenceHandler.cs" />
+ <Compile Include="System\Text\Json\Serialization\PreserveReferenceResolver.cs" />
<Compile Include="System\Text\Json\Serialization\PropertyRef.cs" />
<Compile Include="System\Text\Json\Serialization\ReadStack.cs" />
<Compile Include="System\Text\Json\Serialization\ReadStackFrame.cs" />
- <Compile Include="System\Text\Json\Serialization\ReferenceHandling.cs" />
+ <Compile Include="System\Text\Json\Serialization\ReferenceHandler.cs" />
+ <Compile Include="System\Text\Json\Serialization\ReferenceHandlerOfT.cs" />
+ <Compile Include="System\Text\Json\Serialization\ReferenceResolver.cs" />
<Compile Include="System\Text\Json\Serialization\ReflectionEmitMemberAccessor.cs" />
<Compile Include="System\Text\Json\Serialization\ReflectionMemberAccessor.cs" />
<Compile Include="System\Text\Json\Serialization\StackFrameObjectState.cs" />
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 4603b59c82f..77c8ff88215 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 @@ namespace System.Text.Json.Serialization.Converters
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 @@ namespace System.Text.Json.Serialization.Converters
}
// 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 @@ namespace System.Text.Json.Serialization.Converters
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 @@ namespace System.Text.Json.Serialization.Converters
state.Current.PropertyState = StackFramePropertyState.Name;
// Verify property doesn't contain metadata.
- if (shouldReadPreservedReferences)
+ if (preserveReferences)
{
ReadOnlySpan<byte> propertyName = reader.GetSpan();
if (propertyName.Length > 0 && propertyName[0] == '$')
@@ -275,7 +274,7 @@ namespace System.Text.Json.Serialization.Converters
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 6803e9d5845..9dd215a4287 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 @@ namespace System.Text.Json.Serialization.Converters
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 @@ namespace System.Text.Json.Serialization.Converters
{
// 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 @@ namespace System.Text.Json.Serialization.Converters
}
// 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 @@ namespace System.Text.Json.Serialization.Converters
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 @@ namespace System.Text.Json.Serialization.Converters
}
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 10327bc0438..c6624a33a75 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 @@ namespace System.Text.Json.Serialization.Converters
{
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 @@ namespace System.Text.Json.Serialization.Converters
// 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 @@ namespace System.Text.Json.Serialization.Converters
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 @@ namespace System.Text.Json.Serialization.Converters
{
writer.WriteStartObject();
- if (options.ReferenceHandling.ShouldWritePreservedReferences())
+ if (options.ReferenceHandler != null)
{
if (JsonSerializer.WriteReferenceForObject(this, objectValue, ref state, writer) == MetadataPropertyName.Ref)
{
@@ -294,7 +293,7 @@ namespace System.Text.Json.Serialization.Converters
{
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 1af7ab0be01..89ecc996e11 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 @@ namespace System.Text.Json.Serialization.Converters
{
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 06f4ed6cdf1..00000000000
--- 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
-{
- /// <summary>
- /// The default ReferenceResolver implementation to handle duplicate object references.
- /// </summary>
- /// <remarks>
- /// 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.
- /// </remarks>
- internal struct DefaultReferenceResolver
- {
- private uint _referenceCount;
- private readonly Dictionary<string, object>? _referenceIdToObjectMap;
- private readonly Dictionary<object, string>? _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<object, string>(ReferenceEqualityComparer.Instance);
- _referenceIdToObjectMap = null;
- }
- else
- {
- _referenceIdToObjectMap = new Dictionary<string, object>();
- _objectToReferenceIdMap = null;
- }
- }
-
- /// <summary>
- /// 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.
- /// </summary>
- /// <param name="referenceId">The identifier of the respective JSON object or array.</param>
- /// <param name="value">The value of the respective CLR reference type object that results from parsing the JSON object.</param>
- /// <returns>True if the value was successfully added, false otherwise.</returns>
- public bool AddReferenceOnDeserialize(string referenceId, object value)
- {
- return JsonHelpers.TryAdd(_referenceIdToObjectMap!, referenceId, value);
- }
-
- /// <summary>
- /// 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.
- /// </summary>
- /// <param name="value">The value of the CLR reference type object to get or add an id for.</param>
- /// <param name="referenceId">The id realated to the object.</param>
- /// <returns></returns>
- 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;
- }
-
- /// <summary>
- /// Resolves the CLR reference type object related to the specified reference id.
- /// This method gets called when $ref metadata property is read.
- /// </summary>
- /// <param name="referenceId">The id related to the returned object.</param>
- /// <returns></returns>
- 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 edb6aebf97b..3375512cb05 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' };
+
/// <summary>
/// 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 @@ namespace System.Text.Json
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 @@ namespace System.Text.Json
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 @@ namespace System.Text.Json
{
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 b7b4db72886..7ed9da9a775 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 @@ namespace System.Text.Json
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 0b5061e6244..7d5b504a2b1 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 @@ namespace System.Text.Json
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 @@ namespace System.Text.Json
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 05b2aec0a2f..98587cb9f0c 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 @@ namespace System.Text.Json
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 @@ namespace System.Text.Json
_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 @@ namespace System.Text.Json
}
/// <summary>
- /// 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.
/// </summary>
- 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 00000000000..a92b1ba9c26
--- /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 00000000000..dd9469fd9db
--- /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
+{
+ /// <summary>
+ /// The default ReferenceResolver implementation to handle duplicate object references.
+ /// </summary>
+ internal sealed class PreserveReferenceResolver : ReferenceResolver
+ {
+ private uint _referenceCount;
+ private readonly Dictionary<string, object>? _referenceIdToObjectMap;
+ private readonly Dictionary<object, string>? _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<object, string>(ReferenceEqualityComparer.Instance);
+ }
+ else
+ {
+ _referenceIdToObjectMap = new Dictionary<string, object>();
+ }
+ }
+
+ 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 8914c9c665a..80db2c446e7 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 @@ namespace System.Text.Json
public bool ReadAhead;
// The bag of preservable references.
- public DefaultReferenceResolver ReferenceResolver;
+ public ReferenceResolver ReferenceResolver;
/// <summary>
/// Whether we need to read ahead in the inner read loop.
/// </summary>
public bool SupportContinuation;
+ /// <summary>
+ /// Whether we can read without the need of saving state for stream and preserve references cases.
+ /// </summary>
+ public bool UseFastPath;
+
private void AddCurrent()
{
if (_previous == null)
@@ -84,12 +89,14 @@ namespace System.Text.Json
// 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 @@ namespace System.Text.Json
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
index 269e0cb84b1..bf033787025 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,22 +9,9 @@ namespace System.Text.Json.Serialization
/// <summary>
/// This class defines how the <see cref="JsonSerializer"/> deals with references on serialization and deserialization.
/// </summary>
- public sealed class ReferenceHandling
+ public abstract class ReferenceHandler
{
/// <summary>
- /// 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.
- /// </summary>
- /// <remarks>
- /// * On Serialize:
- /// Treats duplicate object references as if they were unique and writes all their properties.
- /// The serializer throws a <see cref="JsonException"/> 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 <see cref="JsonExtensionDataAttribute"/> overflow dictionary, if one exists; otherwise, they are ignored.
- /// </remarks>
- public static ReferenceHandling Default { get; } = new ReferenceHandling(PreserveReferencesHandling.None);
-
- /// <summary>
/// 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.
/// </summary>
/// <remarks>
@@ -36,7 +23,7 @@ namespace System.Text.Json.Serialization
/// 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 <see cref="ReferenceHandling.Default"/>.
+ /// For JSON objects that don't contain any metadata properties, the deserialization behavior is identical to <see langword="null"/>.
/// For value types:
/// * The `$id` metadata property is ignored.
/// * A <see cref="JsonException"/> is thrown if a `$ref` metadata property is found within the JSON object.
@@ -51,47 +38,18 @@ namespace System.Text.Json.Serialization
/// 7) The `$values` metadata property is only valid when referring to enumerable types.
/// If the JSON is not well-formed, a <see cref="JsonException"/> is thrown.
/// </remarks>
- public static ReferenceHandling Preserve { get; } = new ReferenceHandling(PreserveReferencesHandling.All);
-
- private readonly bool _shouldReadPreservedReferences;
- private readonly bool _shouldWritePreservedReferences;
+ public static ReferenceHandler Preserve { get; } = new PreserveReferenceHandler();
/// <summary>
- /// Creates a new instance of <see cref="ReferenceHandling"/> using the specified <paramref name="handling"/>
+ /// Returns the <see cref="ReferenceResolver "/> used for each serialization call.
/// </summary>
- /// <param name="handling">The specified behavior for write/read preserved references.</param>
- 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;
- }
+ /// <returns>The resolver to use for serialization and deserialization.</returns>
+ public abstract ReferenceResolver CreateResolver();
- internal bool ShouldReadPreservedReferences()
- {
- return _shouldReadPreservedReferences;
- }
-
- internal bool ShouldWritePreservedReferences()
- {
- return _shouldWritePreservedReferences;
- }
- }
-
- /// <summary>
- /// Defines behaviors to preserve references of JSON complex types.
- /// </summary>
- internal enum PreserveReferencesHandling
- {
- /// <summary>
- /// Preserved objects and arrays will not be written/read.
- /// </summary>
- None = 0,
/// <summary>
- /// Preserved objects and arrays will be written/read.
+ /// Optimization for the resolver used when <see cref="Preserve"/> is set in <see cref="JsonSerializerOptions.ReferenceHandler"/>;
+ /// we pass a flag signaling whether this is called from serialization or deserialization to save one dictionary instantiation.
/// </summary>
- 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 00000000000..18fb4501abd
--- /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
+{
+ /// <summary>
+ /// This class defines how the <see cref="JsonSerializer"/> deals with references on serialization and deserialization.
+ /// </summary>
+ /// <typeparam name="T">The type of the <see cref="ReferenceResolver"/> to create on each serialization or deserialization call.</typeparam>
+ public sealed class ReferenceHandler<T> : ReferenceHandler
+ where T: ReferenceResolver, new()
+ {
+ /// <summary>
+ /// Creates a new <see cref="ReferenceResolver"/> of type <typeparamref name="T"/> used for each serialization call.
+ /// </summary>
+ /// <returns>The new resolver to use for serialization and deserialization.</returns>
+ 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 00000000000..a5a0147b28c
--- /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
+{
+ /// <summary>
+ /// This class defines how the <see cref="JsonSerializer"/> deals with references on serialization and deserialization.
+ /// Defines the core behavior of preserving references on serialization and deserialization.
+ /// </summary>
+ public abstract class ReferenceResolver
+ {
+ /// <summary>
+ /// 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.
+ /// </summary>
+ /// <param name="referenceId">The identifier of the respective JSON object or array.</param>
+ /// <param name="value">The value of the respective CLR reference type object that results from parsing the JSON object.</param>
+ public abstract void AddReference(string referenceId, object value);
+
+ /// <summary>
+ /// 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.
+ /// </summary>
+ /// <param name="value">The value of the CLR reference type object to get an id for.</param>
+ /// <param name="alreadyExists">When this method returns, <see langword="true"/> if a reference to value already exists; otherwise, <see langword="false"/>.</param>
+ /// <returns>The reference id for the specified object.</returns>
+ public abstract string GetReference(object value, out bool alreadyExists);
+
+ /// <summary>
+ /// Returns the CLR reference type object related to the specified reference id.
+ /// This method gets called when $ref metadata property is read.
+ /// </summary>
+ /// <param name="referenceId">The reference id related to the returned object.</param>
+ /// <returns>The reference type object related to specified reference id.</returns>
+ 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 16df62ffd10..e2efb6492c2 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 @@ namespace System.Text.Json
public bool IsContinuation => _continuationCount != 0;
// The bag of preservable references.
- public DefaultReferenceResolver ReferenceResolver;
+ public ReferenceResolver ReferenceResolver;
/// <summary>
/// Internal flag to let us know that we need to read ahead in the inner read loop.
@@ -77,9 +77,10 @@ namespace System.Text.Json
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 0d7bd68400e..71d3f2ada0f 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 @@ namespace System.Text.Json
[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 e47b1e98156..1f6c8add1e0 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 @@ namespace System.Text.Json.Serialization.Tests
// 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<NotSupportedException>(
() => Serializer.Deserialize<Employee>(json, options));
@@ -102,7 +102,7 @@ namespace System.Text.Json.Serialization.Tests
// 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<NotSupportedException>(() => Serializer.Deserialize<Employee>(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 e86537cbac1..2d5c663f71e 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 @@ namespace System.Text.Json.Serialization.Tests
// Perform serialization with options, after which it will be locked.
JsonSerializer.Serialize("1", options);
- Assert.Throws<InvalidOperationException>(() => options.ReferenceHandling = ReferenceHandling.Preserve);
+ Assert.Throws<InvalidOperationException>(() => 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 @@ namespace System.Text.Json.Serialization.Tests
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
index a20b2e1580e..65de4873790 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 @@ using Xunit;
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 @@ namespace System.Text.Json.Serialization.Tests
var options = new JsonSerializerOptions
{
- ReferenceHandling = ReferenceHandling.Preserve,
+ ReferenceHandler = ReferenceHandler.Preserve,
Converters = { new ListOfEmployeeConverter() }
};
Employee angela = JsonSerializer.Deserialize<Employee>(json, options);
@@ -1199,7 +1199,7 @@ namespace System.Text.Json.Serialization.Tests
[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<ITestClass>(obj);
((ITestClass)obj).Verify();
@@ -1276,6 +1276,32 @@ namespace System.Text.Json.Serialization.Tests
Assert.Contains("'1'", ex.Message);
}
+ class ClassWithTwoListProperties
+ {
+ public List<string> List1 { get; set; }
+ public List<string> List2 { get; set; }
+ }
+
+ [Fact]
+ public static void DuplicatedIdArray()
+ {
+ string json = @"{
+ ""List1"": {
+ ""$id"": ""1"",
+ ""$values"": []
+ },
+ ""List2"": {
+ ""$id"": ""1"",
+ ""$values"": []
+ }
+ }";
+
+ JsonException ex = Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<ClassWithTwoListProperties>(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
index 4271db99f0d..364e639324a 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 @@ using Xunit;
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
index cc01df11eb4..fc5659e41e4 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 @@ namespace System.Text.Json.Serialization.Tests
JsonException ex = Assert.Throws<JsonException>(() => JsonSerializer.Serialize(a));
}
- [Fact]
- public static void ThrowWhenPassingNullToReferenceHandling()
- {
- Assert.Throws<ArgumentNullException>(() => new JsonSerializerOptions { ReferenceHandling = null });
- }
-
#region Root Object
[Fact]
public static void ObjectLoop()
@@ -203,7 +199,7 @@ namespace System.Text.Json.Serialization.Tests
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 @@ namespace System.Text.Json.Serialization.Tests
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 @@ namespace System.Text.Json.Serialization.Tests
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<GuidReferenceResolver>()
+ };
+ ImmutableArray<PersonReference> people = JsonSerializer.Deserialize<ImmutableArray<PersonReference>>(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<PersonReference> firstListOfPeople = JsonSerializer.Deserialize<ImmutableArray<PersonReference>>(json, options);
+
+ json =
+@"[
+ {
+ ""$ref"": ""0b64ffdf-d155-44ad-9689-58d9adb137f3""
+ },
+ {
+ ""$ref"": ""ae3c399c-058d-431d-91b0-a36c266441b9""
+ }
+]";
+ ImmutableArray<PersonReference> secondListOfPeople = JsonSerializer.Deserialize<ImmutableArray<PersonReference>>(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<Guid, PersonReference> _people = new Dictionary<Guid, PersonReference>();
+
+ 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<BadReferenceResolver>() };
+
+ 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<JsonException>(() => 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 b90b50dba8a..0a075216293 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 @@ namespace System.Text.Json.Serialization.Tests
var optionsWithPreservedReferenceHandling = new JsonSerializerOptions(options)
{
- ReferenceHandling = ReferenceHandling.Preserve
+ ReferenceHandler = ReferenceHandler.Preserve
};
object obj = GetPopulatedCollection<TElement>(type, thresholdSize);
@@ -98,7 +98,7 @@ namespace System.Text.Json.Serialization.Tests
// 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<TElement>().Contains(type) && options.ReferenceHandling == ReferenceHandling.Preserve))
+ if(!(DictionaryTypes<TElement>().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 2693078629b..ddfccacb6f8 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 @@
<Compile Include="Serialization\PropertyVisibilityTests.cs" />
<Compile Include="Serialization\ReadScenarioTests.cs" />
<Compile Include="Serialization\ReadValueTests.cs" />
- <Compile Include="Serialization\ReferenceHandlingTests.cs" />
- <Compile Include="Serialization\ReferenceHandlingTests.Deserialize.cs" />
- <Compile Include="Serialization\ReferenceHandlingTests.Serialize.cs" />
+ <Compile Include="Serialization\ReferenceHandlerTests.cs" />
+ <Compile Include="Serialization\ReferenceHandlerTests.Deserialize.cs" />
+ <Compile Include="Serialization\ReferenceHandlerTests.Serialize.cs" />
<Compile Include="Serialization\SampleTestData.OrderPayload.cs" />
<Compile Include="Serialization\SerializationWrapper.cs" />
<Compile Include="Serialization\SpanTests.cs" />