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; }
}
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);
}
}
<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>
<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>
<data name="DefaultIgnoreConditionInvalid" xml:space="preserve">
<value>The value cannot be 'JsonIgnoreCondition.Always'.</value>
</data>
-</root>
\ No newline at end of file
+</root>
<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" />
<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" />
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.
}
// 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))
{
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;
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] == '$')
state.Current.ProcessedStartToken = true;
writer.WriteStartObject();
- if (options.ReferenceHandling.ShouldWritePreservedReferences())
+ if (options.ReferenceHandler != null)
{
if (JsonSerializer.WriteReferenceForObject(this, dictionary, ref state, writer) == MetadataPropertyName.Ref)
{
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.
{
// 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)
{
}
// 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))
{
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;
}
else
{
- bool shouldWritePreservedReferences = options.ReferenceHandling.ShouldWritePreservedReferences();
-
if (!state.Current.ProcessedStartToken)
{
state.Current.ProcessedStartToken = true;
- if (!shouldWritePreservedReferences)
+ if (options.ReferenceHandler == null)
{
writer.WriteStartArray();
}
{
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.
// Handle the metadata properties.
if (state.Current.ObjectState < StackFrameObjectState.PropertyValue)
{
- if (shouldReadPreservedReferences)
+ if (options.ReferenceHandler != null)
{
if (JsonSerializer.ResolveMetadata(this, ref reader, ref state))
{
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;
{
writer.WriteStartObject();
- if (options.ReferenceHandling.ShouldWritePreservedReferences())
+ if (options.ReferenceHandler != null)
{
if (JsonSerializer.WriteReferenceForObject(this, objectValue, ref state, writer) == MetadataPropertyName.Ref)
{
{
writer.WriteStartObject();
- if (options.ReferenceHandling.ShouldWritePreservedReferences())
+ if (options.ReferenceHandler != null)
{
if (JsonSerializer.WriteReferenceForObject(this, objectValue, ref state, writer) == MetadataPropertyName.Ref)
{
{
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.
+++ /dev/null
-// 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;
- }
- }
-}
{
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.
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)
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.
{
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);
}
unescapedPropertyName = propertyName;
}
- if (options.ReferenceHandling.ShouldReadPreservedReferences())
+ if (options.ReferenceHandler != null)
{
if (propertyName.Length > 0 && propertyName[0] == '$')
{
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(
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;
}
}
}
private JsonNamingPolicy? _dictionaryKeyPolicy;
private JsonNamingPolicy? _jsonPropertyNamingPolicy;
private JsonCommentHandling _readCommentHandling;
- private ReferenceHandling _referenceHandling = ReferenceHandling.Default;
+ private ReferenceHandler? _referenceHandler;
private JavaScriptEncoder? _encoder = null;
private JsonIgnoreCondition _defaultIgnoreCondition;
_dictionaryKeyPolicy = options._dictionaryKeyPolicy;
_jsonPropertyNamingPolicy = options._jsonPropertyNamingPolicy;
_readCommentHandling = options._readCommentHandling;
- _referenceHandling = options._referenceHandling;
+ _referenceHandler = options._referenceHandler;
_encoder = options._encoder;
_defaultIgnoreCondition = options._defaultIgnoreCondition;
}
/// <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;
}
}
--- /dev/null
+// 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);
+ }
+}
--- /dev/null
+// 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;
+ }
+ }
+}
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)
// 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()
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));
/// <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>
/// 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.
/// 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();
}
}
--- /dev/null
+// 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();
+ }
+}
--- /dev/null
+// 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);
+ }
+}
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.
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;
[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));
}
// 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));
// 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();
// 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]
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)
{
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
{
var options = new JsonSerializerOptions
{
- ReferenceHandling = ReferenceHandling.Preserve,
+ ReferenceHandler = ReferenceHandler.Preserve,
Converters = { new ListOfEmployeeConverter() }
};
Employee angela = JsonSerializer.Deserialize<Employee>(json, options);
[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();
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""}}")]
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
// 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]
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()
var optionsWithEncoder = new JsonSerializerOptions
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
- ReferenceHandling = ReferenceHandling.Preserve
+ ReferenceHandler = ReferenceHandler.Preserve
};
json = JsonSerializer.Serialize(obj, optionsWithEncoder);
Assert.StartsWith("{\"$id\":\"1\",", json);
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);
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
}
}
var optionsWithPreservedReferenceHandling = new JsonSerializerOptions(options)
{
- ReferenceHandling = ReferenceHandling.Preserve
+ ReferenceHandler = ReferenceHandler.Preserve
};
object obj = GetPopulatedCollection<TElement>(type, thresholdSize);
// 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);
}
<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" />