* Refactor metadata reading infrastructure.
* Update src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectConverter.cs
<data name="ConstructorMaxOf64Parameters" xml:space="preserve">
<value>The deserialization constructor on type '{0}' may not have more than 64 parameters for deserialization.</value>
</data>
- <data name="ObjectWithParameterizedCtorRefMetadataNotHonored" xml:space="preserve">
- <value>Reference metadata is not honored when deserializing types using parameterized constructors. See type '{0}'.</value>
+ <data name="ObjectWithParameterizedCtorRefMetadataNotSupported" xml:space="preserve">
+ <value>Reference metadata is not supported when deserializing constructor parameters. See type '{0}'.</value>
</data>
<data name="SerializerConverterFactoryReturnsNull" xml:space="preserve">
<value>The converter '{0}' cannot return a null value.</value>
/// </summary>
internal sealed class ArrayConverter<TCollection, TElement> : IEnumerableDefaultConverter<TElement[], TElement>
{
- internal override bool CanHaveIdMetadata => false;
+ internal override bool CanHaveMetadata => false;
protected override void Add(in TElement value, ref ReadStack state)
{
where TDictionary : IEnumerable<KeyValuePair<TKey, TValue>>
where TKey : notnull
{
- internal override bool CanHaveIdMetadata => true;
+ internal override bool CanHaveMetadata => true;
protected internal override bool OnWriteResume(
Utf8JsonWriter writer,
internal abstract class IEnumerableDefaultConverter<TCollection, TElement> : JsonCollectionConverter<TCollection, TElement>
where TCollection : IEnumerable<TElement>
{
- internal override bool CanHaveIdMetadata => true;
+ internal override bool CanHaveMetadata => true;
protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state)
{
((Dictionary<TKey, TValue>)state.Current.ReturnValue!)[key] = value;
}
- internal sealed override bool CanHaveIdMetadata => false;
+ internal sealed override bool CanHaveMetadata => false;
protected sealed override void CreateCollection(ref Utf8JsonReader reader, ref ReadStack state)
{
((List<TElement>)state.Current.ReturnValue!).Add(value);
}
- internal sealed override bool CanHaveIdMetadata => false;
+ internal sealed override bool CanHaveMetadata => false;
protected sealed override void CreateCollection(ref Utf8JsonReader reader, ref ReadStack state, JsonSerializerOptions options)
{
}
else
{
- // Slower path that supports continuation and preserved references.
+ // Slower path that supports continuation and reading metadata.
- bool preserveReferences = options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve;
if (state.Current.ObjectState == StackFrameObjectState.None)
{
if (reader.TokenType == JsonTokenType.StartArray)
{
- state.Current.ObjectState = StackFrameObjectState.PropertyValue;
+ state.Current.ObjectState = StackFrameObjectState.ReadMetadata;
}
- else if (preserveReferences)
+ else if (state.CanContainMetadata)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
}
// Handle the metadata properties.
- if (preserveReferences && state.Current.ObjectState < StackFrameObjectState.PropertyValue)
+ if (state.CanContainMetadata && state.Current.ObjectState < StackFrameObjectState.ReadMetadata)
{
- if (JsonSerializer.ResolveMetadataForJsonArray<TCollection>(ref reader, ref state, options))
- {
- if (state.Current.ObjectState == StackFrameObjectState.ReadRefEndObject)
- {
- // This will never throw since it was previously validated in ResolveMetadataForJsonArray.
- value = (TCollection)state.Current.ReturnValue!;
- return true;
- }
- }
- else
+ if (!JsonSerializer.TryReadMetadata(this, ref reader, ref state))
{
value = default;
return false;
}
+
+ state.Current.ObjectState = StackFrameObjectState.ReadMetadata;
}
if (state.Current.ObjectState < StackFrameObjectState.CreatedObject)
{
+ if (state.CanContainMetadata)
+ {
+ JsonSerializer.ValidateMetadataForArrayConverter(this, ref reader, ref state);
+ }
+
+ if (state.Current.MetadataPropertyNames == MetadataPropertyName.Ref)
+ {
+ value = JsonSerializer.ResolveReferenceId<TCollection>(ref state);
+ return true;
+ }
+
CreateCollection(ref reader, ref state, options);
+
+ if (state.Current.MetadataPropertyNames.HasFlag(MetadataPropertyName.Id))
+ {
+ Debug.Assert(state.ReferenceId != null);
+ Debug.Assert(options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve);
+ Debug.Assert(state.Current.ReturnValue is TCollection);
+ state.ReferenceResolver.AddReference(state.ReferenceId, state.Current.ReturnValue);
+ state.ReferenceId = null;
+ }
+
state.Current.ObjectState = StackFrameObjectState.CreatedObject;
}
{
state.Current.ObjectState = StackFrameObjectState.EndToken;
- // Read the EndObject token for an array with preserve semantics.
- if (state.Current.ValidateEndTokenOnArray)
+ // Array payload is nested inside a $values metadata property.
+ if (state.Current.MetadataPropertyNames.HasFlag(MetadataPropertyName.Values))
{
if (!reader.Read())
{
if (state.Current.ObjectState < StackFrameObjectState.EndTokenValidation)
{
- if (state.Current.ValidateEndTokenOnArray)
+ // Array payload is nested inside a $values metadata property.
+ if (state.Current.MetadataPropertyNames.HasFlag(MetadataPropertyName.Values))
{
if (reader.TokenType != JsonTokenType.EndObject)
{
}
protected abstract bool OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state);
-
- internal sealed override void CreateInstanceForReferenceResolver(ref Utf8JsonReader reader, ref ReadStack state, JsonSerializerOptions options)
- => CreateCollection(ref reader, ref state, options);
}
}
}
else
{
- // Slower path that supports continuation and preserved references.
+ // Slower path that supports continuation and reading metadata.
if (state.Current.ObjectState == StackFrameObjectState.None)
{
}
// Handle the metadata properties.
- bool preserveReferences = options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve;
- if (preserveReferences && state.Current.ObjectState < StackFrameObjectState.PropertyValue)
+ if (state.CanContainMetadata && state.Current.ObjectState < StackFrameObjectState.ReadMetadata)
{
- if (JsonSerializer.ResolveMetadataForJsonObject<TDictionary>(ref reader, ref state, options))
- {
- if (state.Current.ObjectState == StackFrameObjectState.ReadRefEndObject)
- {
- // This will never throw since it was previously validated in ResolveMetadataForJsonObject.
- value = (TDictionary)state.Current.ReturnValue!;
- return true;
- }
- }
- else
+ if (!JsonSerializer.TryReadMetadata(this, ref reader, ref state))
{
value = default;
return false;
}
+
+ state.Current.ObjectState = StackFrameObjectState.ReadMetadata;
}
// Create the dictionary.
if (state.Current.ObjectState < StackFrameObjectState.CreatedObject)
{
+ if (state.CanContainMetadata)
+ {
+ JsonSerializer.ValidateMetadataForObjectConverter(this, ref reader, ref state);
+ }
+
+ if (state.Current.MetadataPropertyNames == MetadataPropertyName.Ref)
+ {
+ value = JsonSerializer.ResolveReferenceId<TDictionary>(ref state);
+ return true;
+ }
+
CreateCollection(ref reader, ref state);
+
+ if (state.Current.MetadataPropertyNames.HasFlag(MetadataPropertyName.Id))
+ {
+ Debug.Assert(state.ReferenceId != null);
+ Debug.Assert(options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve);
+ Debug.Assert(state.Current.ReturnValue is TDictionary);
+ state.ReferenceResolver.AddReference(state.ReferenceId, state.Current.ReturnValue);
+ state.ReferenceId = null;
+ }
+
state.Current.ObjectState = StackFrameObjectState.CreatedObject;
}
state.Current.PropertyState = StackFramePropertyState.Name;
- if (preserveReferences)
+ if (state.CanContainMetadata)
{
ReadOnlySpan<byte> propertyName = reader.GetSpan();
if (propertyName.Length > 0 && propertyName[0] == '$')
return success;
}
-
- internal sealed override void CreateInstanceForReferenceResolver(ref Utf8JsonReader reader, ref ReadStack state, JsonSerializerOptions options)
- => CreateCollection(ref reader, ref state);
}
}
((List<Tuple<TKey, TValue>>)state.Current.ReturnValue!).Add (new Tuple<TKey, TValue>(key, value));
}
- internal override bool CanHaveIdMetadata => false;
+ internal override bool CanHaveMetadata => false;
protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStack state)
{
internal override bool ConstructorIsParameterized => Converter.ConstructorIsParameterized;
- internal override bool CanHaveIdMetadata => Converter.CanHaveIdMetadata;
+ internal override bool CanHaveMetadata => Converter.CanHaveMetadata;
public JsonMetadataServicesConverter(Func<JsonConverter<T>> converterCreator!!, ConverterStrategy converterStrategy)
{
internal override void ConfigureJsonTypeInfo(JsonTypeInfo jsonTypeInfo, JsonSerializerOptions options)
=> Converter.ConfigureJsonTypeInfo(jsonTypeInfo, options);
-
- internal override void CreateInstanceForReferenceResolver(ref Utf8JsonReader reader, ref ReadStack state, JsonSerializerOptions options)
- => Converter.CreateInstanceForReferenceResolver(ref reader, ref state, options);
}
}
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
+using System.Text.Json.Nodes;
namespace System.Text.Json.Serialization.Converters
{
internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out object? value)
{
+ object? referenceValue;
+
if (options.UnknownTypeHandling == JsonUnknownTypeHandling.JsonElement)
{
JsonElement element = JsonElement.ParseValue(ref reader);
// Edge case where we want to lookup for a reference when parsing into typeof(object)
if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve &&
- JsonSerializer.TryGetReferenceFromJsonElement(ref state, element, out object? referenceValue))
+ JsonSerializer.TryHandleReferenceFromJsonElement(ref reader, ref state, element, out referenceValue))
{
value = referenceValue;
}
}
Debug.Assert(options.UnknownTypeHandling == JsonUnknownTypeHandling.JsonNode);
- value = JsonNodeConverter.Instance.Read(ref reader, typeToConvert, options)!;
- // TODO reference lookup for JsonNode deserialization.
+
+ JsonNode node = JsonNodeConverter.Instance.Read(ref reader, typeToConvert, options)!;
+
+ if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve &&
+ JsonSerializer.TryHandleReferenceFromJsonNode(ref reader, ref state, node, out referenceValue))
+ {
+ value = referenceValue;
+ }
+ else
+ {
+ value = node;
+ }
+
return true;
}
/// </summary>
internal class ObjectDefaultConverter<T> : JsonObjectConverter<T> where T : notnull
{
- internal override bool CanHaveIdMetadata => true;
+ internal override bool CanHaveMetadata => true;
internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, [MaybeNullWhen(false)] out T value)
{
}
else
{
- // Slower path that supports continuation and preserved references.
+ // Slower path that supports continuation and reading metadata.
if (state.Current.ObjectState == StackFrameObjectState.None)
{
}
// Handle the metadata properties.
- if (state.Current.ObjectState < StackFrameObjectState.PropertyValue)
+ if (state.CanContainMetadata && state.Current.ObjectState < StackFrameObjectState.ReadMetadata)
{
- if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve)
+ if (!JsonSerializer.TryReadMetadata(this, ref reader, ref state))
{
- if (JsonSerializer.ResolveMetadataForJsonObject<T>(ref reader, ref state, options))
- {
- if (state.Current.ObjectState == StackFrameObjectState.ReadRefEndObject)
- {
- // This will never throw since it was previously validated in ResolveMetadataForJsonObject.
- value = (T)state.Current.ReturnValue!;
- return true;
- }
- }
- else
- {
- value = default;
- return false;
- }
+ value = default;
+ return false;
}
+
+ state.Current.ObjectState = StackFrameObjectState.ReadMetadata;
}
if (state.Current.ObjectState < StackFrameObjectState.CreatedObject)
{
+ if (state.CanContainMetadata)
+ {
+ JsonSerializer.ValidateMetadataForObjectConverter(this, ref reader, ref state);
+ }
+
+ if (state.Current.MetadataPropertyNames == MetadataPropertyName.Ref)
+ {
+ value = JsonSerializer.ResolveReferenceId<T>(ref state);
+ return true;
+ }
+
if (jsonTypeInfo.CreateObject == null)
{
ThrowHelper.ThrowNotSupportedException_DeserializeNoConstructor(jsonTypeInfo.Type, ref reader, ref state);
obj = jsonTypeInfo.CreateObject!()!;
+ if (state.Current.MetadataPropertyNames.HasFlag(MetadataPropertyName.Id))
+ {
+ Debug.Assert(state.ReferenceId != null);
+ Debug.Assert(options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve);
+ state.ReferenceResolver.AddReference(state.ReferenceId, obj);
+ state.ReferenceId = null;
+ }
+
if (obj is IJsonOnDeserializing onDeserializing)
{
onDeserializing.OnDeserializing();
return true;
}
-
- internal sealed override void CreateInstanceForReferenceResolver(ref Utf8JsonReader reader, ref ReadStack state, JsonSerializerOptions options)
- {
- if (state.Current.JsonTypeInfo.CreateObject == null)
- {
- ThrowHelper.ThrowNotSupportedException_DeserializeNoConstructor(state.Current.JsonTypeInfo.Type, ref reader, ref state);
- }
-
- object obj = state.Current.JsonTypeInfo.CreateObject!()!;
- state.Current.ReturnValue = obj;
-
- if (obj is IJsonOnDeserializing onDeserializing)
- {
- onDeserializing.OnDeserializing();
- }
- }
}
}
{
// Fast path that avoids maintaining state variables.
+ if (reader.TokenType != JsonTokenType.StartObject)
+ {
+ ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert);
+ }
+
ReadOnlySpan<byte> originalSpan = reader.OriginalSpan;
ReadConstructorArguments(ref state, ref reader, options);
}
else
{
- // Slower path that supports continuation.
+ // Slower path that supports continuation and metadata reads.
if (state.Current.ObjectState == StackFrameObjectState.None)
{
+ if (reader.TokenType != JsonTokenType.StartObject)
+ {
+ ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert);
+ }
+
state.Current.ObjectState = StackFrameObjectState.StartToken;
- BeginRead(ref state, ref reader, options);
+ }
+
+ // Read any metadata properties.
+ if (state.CanContainMetadata && state.Current.ObjectState < StackFrameObjectState.ReadMetadata)
+ {
+ if (!JsonSerializer.TryReadMetadata(this, ref reader, ref state))
+ {
+ value = default;
+ return false;
+ }
+
+ state.Current.ObjectState = StackFrameObjectState.ReadMetadata;
+ }
+
+ if (state.Current.ObjectState < StackFrameObjectState.ConstructorArguments)
+ {
+ if (state.CanContainMetadata)
+ {
+ JsonSerializer.ValidateMetadataForObjectConverter(this, ref reader, ref state);
+ }
+
+ if (state.Current.MetadataPropertyNames == MetadataPropertyName.Ref)
+ {
+ value = JsonSerializer.ResolveReferenceId<T>(ref state);
+ return true;
+ }
+
+ BeginRead(ref state, ref reader, options);
+
+ state.Current.ObjectState = StackFrameObjectState.ConstructorArguments;
}
if (!ReadConstructorArgumentsWithContinuation(ref state, ref reader, options))
obj = (T)CreateObject(ref state.Current);
+ if (state.Current.MetadataPropertyNames.HasFlag(MetadataPropertyName.Id))
+ {
+ Debug.Assert(state.ReferenceId != null);
+ Debug.Assert(options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve);
+ state.ReferenceResolver.AddReference(state.ReferenceId, obj);
+ state.ReferenceId = null;
+ }
+
if (obj is IJsonOnDeserializing onDeserializing)
{
onDeserializing.OnDeserializing();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void BeginRead(ref ReadStack state, ref Utf8JsonReader reader, JsonSerializerOptions options)
{
- if (reader.TokenType != JsonTokenType.StartObject)
- {
- ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert);
- }
-
if (state.Current.JsonTypeInfo.ParameterCount != state.Current.JsonTypeInfo.ParameterCache!.Count)
{
ThrowHelper.ThrowInvalidOperationException_ConstructorParameterIncompleteBinding(TypeToConvert);
/// <summary>
/// Can the converter have $id metadata.
/// </summary>
- internal virtual bool CanHaveIdMetadata => false;
+ internal virtual bool CanHaveMetadata => false;
/// <summary>
/// The converter supports polymorphic writes; only reserved for System.Object types.
/// </summary>
[RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
internal virtual void ConfigureJsonTypeInfoUsingReflection(JsonTypeInfo jsonTypeInfo, JsonSerializerOptions options) { }
-
- /// <summary>
- /// Creates the instance and assigns it to state.Current.ReturnValue.
- /// </summary>
- internal virtual void CreateInstanceForReferenceResolver(ref Utf8JsonReader reader, ref ReadStack state, JsonSerializerOptions options) { }
}
}
break;
case ReferenceHandlingStrategy.Preserve:
- bool canHaveIdMetata = polymorphicConverter?.CanHaveIdMetadata ?? CanHaveIdMetadata;
- if (canHaveIdMetata && JsonSerializer.TryGetReferenceForValue(value, ref state, writer))
+ bool canHaveMetadata = polymorphicConverter?.CanHaveMetadata ?? CanHaveMetadata;
+ if (canHaveMetadata && JsonSerializer.TryGetReferenceForValue(value, ref state, writer))
{
// We found a repeating reference and wrote the relevant metadata; serialization complete.
return true;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
-using System.Runtime.CompilerServices;
+using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace System.Text.Json
internal static readonly byte[] s_valuesPropertyName
= new byte[] { (byte)'$', (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', (byte)'s' };
- /// <summary>
- /// Returns true if successful, false is the reader ran out of buffer.
- /// Sets state.Current.ReturnValue to the reference target for $ref cases;
- /// Sets state.Current.ReturnValue to a new instance for $id cases.
- /// </summary>
- internal static bool ResolveMetadataForJsonObject<T>(
- ref Utf8JsonReader reader,
- ref ReadStack state,
- JsonSerializerOptions options)
+ internal static bool TryReadMetadata(JsonConverter converter, ref Utf8JsonReader reader, ref ReadStack state)
{
- JsonConverter converter = state.Current.JsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase;
-
- if (state.Current.ObjectState < StackFrameObjectState.ReadAheadNameOrEndObject)
- {
- // Read the first metadata property name.
- if (!TryReadAheadMetadataAndSetState(ref reader, ref state, StackFrameObjectState.ReadNameOrEndObject))
- {
- return false;
- }
- }
+ Debug.Assert(state.Current.ObjectState == StackFrameObjectState.StartToken);
+ Debug.Assert(state.CanContainMetadata);
- if (state.Current.ObjectState == StackFrameObjectState.ReadNameOrEndObject)
+ while (true)
{
- if (reader.TokenType != JsonTokenType.PropertyName)
+ if (state.Current.PropertyState == StackFramePropertyState.None)
{
- // Since this was an empty object, we are done reading metadata.
- state.Current.ObjectState = StackFrameObjectState.PropertyValue;
- // Skip the read of the first property name, since we already read it above.
state.Current.PropertyState = StackFramePropertyState.ReadName;
- return true;
- }
- ReadOnlySpan<byte> propertyName = reader.GetSpan();
- MetadataPropertyName metadata = GetMetadataPropertyName(propertyName);
- if (metadata == MetadataPropertyName.Id)
- {
- state.Current.JsonPropertyName = s_idPropertyName;
-
- if (!converter.CanHaveIdMetadata)
+ // Read the property name.
+ if (!reader.Read())
{
- ThrowHelper.ThrowJsonException_MetadataCannotParsePreservedObjectIntoImmutable(converter.TypeToConvert);
+ return false;
}
-
- state.Current.ObjectState = StackFrameObjectState.ReadAheadIdValue;
}
- else if (metadata == MetadataPropertyName.Ref)
- {
- state.Current.JsonPropertyName = s_refPropertyName;
- if (converter.IsValueType)
+ if (state.Current.PropertyState < StackFramePropertyState.Name)
+ {
+ if (reader.TokenType == JsonTokenType.EndObject)
{
- ThrowHelper.ThrowJsonException_MetadataInvalidReferenceToValueType(converter.TypeToConvert);
+ // Read the entire object while parsing for metadata.
+ return true;
}
- state.Current.ObjectState = StackFrameObjectState.ReadAheadRefValue;
- }
- else if (metadata == MetadataPropertyName.Values)
- {
- ThrowHelper.ThrowJsonException_MetadataInvalidPropertyWithLeadingDollarSign(propertyName, ref state, reader);
- }
- else
- {
- Debug.Assert(metadata == MetadataPropertyName.NoMetadata);
- // We are done reading metadata, the object didn't contain any.
- state.Current.ObjectState = StackFrameObjectState.PropertyValue;
- // Skip the read of the first property name, since we already read it above.
- state.Current.PropertyState = StackFramePropertyState.ReadName;
- return true;
- }
- }
-
- if (state.Current.ObjectState == StackFrameObjectState.ReadAheadRefValue)
- {
- if (!TryReadAheadMetadataAndSetState(ref reader, ref state, StackFrameObjectState.ReadRefValue))
- {
- return false;
- }
- }
- else if (state.Current.ObjectState == StackFrameObjectState.ReadAheadIdValue)
- {
- if (!TryReadAheadMetadataAndSetState(ref reader, ref state, StackFrameObjectState.ReadIdValue))
- {
- return false;
- }
- }
-
- if (state.Current.ObjectState == StackFrameObjectState.ReadRefValue)
- {
- if (reader.TokenType != JsonTokenType.String)
- {
- ThrowHelper.ThrowJsonException_MetadataValueWasNotString(reader.TokenType);
- }
-
- string referenceId = reader.GetString()!;
- object value = state.ReferenceResolver.ResolveReference(referenceId);
- ValidateValueIsCorrectType<T>(value, referenceId);
- state.Current.ReturnValue = value;
-
- state.Current.ObjectState = StackFrameObjectState.ReadAheadRefEndObject;
- }
- else if (state.Current.ObjectState == StackFrameObjectState.ReadIdValue)
- {
- if (reader.TokenType != JsonTokenType.String)
- {
- ThrowHelper.ThrowJsonException_MetadataValueWasNotString(reader.TokenType);
- }
-
- converter.CreateInstanceForReferenceResolver(ref reader, ref state, options);
-
- string referenceId = reader.GetString()!;
- state.ReferenceResolver.AddReference(referenceId, state.Current.ReturnValue!);
-
- // We are done reading metadata plus we instantiated the object.
- state.Current.ObjectState = StackFrameObjectState.CreatedObject;
- }
-
- // Clear the metadata property name that was set in case of failure on ResolveReference/AddReference.
- state.Current.JsonPropertyName = null;
-
- if (state.Current.ObjectState == StackFrameObjectState.ReadAheadRefEndObject)
- {
- if (!TryReadAheadMetadataAndSetState(ref reader, ref state, StackFrameObjectState.ReadRefEndObject))
- {
- return false;
- }
- }
-
- if (state.Current.ObjectState == StackFrameObjectState.ReadRefEndObject)
- {
- if (reader.TokenType != JsonTokenType.EndObject)
- {
// We just read a property. The only valid next tokens are EndObject and PropertyName.
Debug.Assert(reader.TokenType == JsonTokenType.PropertyName);
- ThrowHelper.ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties(reader.GetSpan(), ref state);
- }
- }
-
- return true;
- }
-
- /// <summary>
- /// Returns true if successful, false is the reader ran out of buffer.
- /// Sets state.Current.ReturnValue to the reference target for $ref cases;
- /// Sets state.Current.ReturnValue to a new instance for $id cases.
- /// </summary>
- internal static bool ResolveMetadataForJsonArray<T>(
- ref Utf8JsonReader reader,
- ref ReadStack state,
- JsonSerializerOptions options)
- {
- JsonConverter converter = state.Current.JsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase;
-
- if (state.Current.ObjectState < StackFrameObjectState.ReadAheadNameOrEndObject)
- {
- // Read the first metadata property name.
- if (!TryReadAheadMetadataAndSetState(ref reader, ref state, StackFrameObjectState.ReadNameOrEndObject))
- {
- return false;
- }
- }
-
- if (state.Current.ObjectState == StackFrameObjectState.ReadNameOrEndObject)
- {
- if (reader.TokenType != JsonTokenType.PropertyName)
- {
- // The reader should have detected other invalid cases.
- Debug.Assert(reader.TokenType == JsonTokenType.EndObject);
-
- // An enumerable needs metadata since it starts with StartObject.
- ThrowHelper.ThrowJsonException_MetadataPreservedArrayValuesNotFound(ref state, converter.TypeToConvert);
- }
-
- ReadOnlySpan<byte> propertyName = reader.GetSpan();
- MetadataPropertyName metadata = GetMetadataPropertyName(propertyName);
- if (metadata == MetadataPropertyName.Id)
- {
- state.Current.JsonPropertyName = s_idPropertyName;
-
- if (!converter.CanHaveIdMetadata)
+ if (state.Current.MetadataPropertyNames.HasFlag(MetadataPropertyName.Ref))
{
- ThrowHelper.ThrowJsonException_MetadataCannotParsePreservedObjectIntoImmutable(converter.TypeToConvert);
+ // No properties whatsoever should follow a $ref property.
+ ThrowHelper.ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties(reader.GetSpan(), ref state);
}
- state.Current.ObjectState = StackFrameObjectState.ReadAheadIdValue;
- }
- else if (metadata == MetadataPropertyName.Ref)
- {
- state.Current.JsonPropertyName = s_refPropertyName;
-
- if (converter.IsValueType)
+ ReadOnlySpan<byte> propertyName = reader.GetSpan();
+ switch (state.Current.LatestMetadataPropertyName = GetMetadataPropertyName(propertyName))
{
- ThrowHelper.ThrowJsonException_MetadataInvalidReferenceToValueType(converter.TypeToConvert);
+ case MetadataPropertyName.Id:
+ state.Current.JsonPropertyName = s_idPropertyName;
+
+ if ((state.Current.MetadataPropertyNames & (MetadataPropertyName.Id | MetadataPropertyName.Ref)) != 0)
+ {
+ // No $id or $ref properties should precede $id properties.
+ ThrowHelper.ThrowJsonException_MetadataIdIsNotFirstProperty(propertyName, ref state);
+ }
+ if (!converter.CanHaveMetadata)
+ {
+ // Should not be permitted unless the converter is capable of handling metadata.
+ ThrowHelper.ThrowJsonException_MetadataCannotParsePreservedObjectIntoImmutable(converter.TypeToConvert);
+ }
+
+ break;
+
+ case MetadataPropertyName.Ref:
+ state.Current.JsonPropertyName = s_refPropertyName;
+
+ if (converter.IsValueType)
+ {
+ // Should not be permitted if the converter is a struct.
+ ThrowHelper.ThrowJsonException_MetadataInvalidReferenceToValueType(converter.TypeToConvert);
+ }
+ if (state.Current.MetadataPropertyNames != 0)
+ {
+ // No metadata properties should precede a $ref property.
+ ThrowHelper.ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties(reader.GetSpan(), ref state);
+ }
+
+ break;
+
+ case MetadataPropertyName.Values:
+ state.Current.JsonPropertyName = s_valuesPropertyName;
+
+ if (state.Current.MetadataPropertyNames == MetadataPropertyName.None)
+ {
+ // Cannot have a $values property unless there are preceding metadata properties.
+ ThrowHelper.ThrowJsonException_MetadataMissingIdBeforeValues(ref state, propertyName);
+ }
+
+ break;
+
+ default:
+ Debug.Assert(state.Current.LatestMetadataPropertyName == MetadataPropertyName.None);
+
+ // Encountered a non-metadata property, exit the reader.
+ return true;
}
- state.Current.ObjectState = StackFrameObjectState.ReadAheadRefValue;
- }
- else if (metadata == MetadataPropertyName.Values)
- {
- ThrowHelper.ThrowJsonException_MetadataMissingIdBeforeValues(ref state, propertyName);
- }
- else
- {
- Debug.Assert(metadata == MetadataPropertyName.NoMetadata);
-
- // Having a StartObject without metadata properties is not allowed.
- ThrowHelper.ThrowJsonException_MetadataPreservedArrayInvalidProperty(ref state, converter.TypeToConvert, reader);
+ state.Current.PropertyState = StackFramePropertyState.Name;
}
- }
- if (state.Current.ObjectState == StackFrameObjectState.ReadAheadRefValue)
- {
- if (!TryReadAheadMetadataAndSetState(ref reader, ref state, StackFrameObjectState.ReadRefValue))
+ if (state.Current.PropertyState < StackFramePropertyState.ReadValue)
{
- return false;
- }
- }
- else if (state.Current.ObjectState == StackFrameObjectState.ReadAheadIdValue)
- {
- if (!TryReadAheadMetadataAndSetState(ref reader, ref state, StackFrameObjectState.ReadIdValue))
- {
- return false;
- }
- }
-
- if (state.Current.ObjectState == StackFrameObjectState.ReadRefValue)
- {
- if (reader.TokenType != JsonTokenType.String)
- {
- ThrowHelper.ThrowJsonException_MetadataValueWasNotString(reader.TokenType);
- }
-
- string referenceId = reader.GetString()!;
- object value = state.ReferenceResolver.ResolveReference(referenceId);
- ValidateValueIsCorrectType<T>(value, referenceId);
- state.Current.ReturnValue = value;
+ state.Current.PropertyState = StackFramePropertyState.ReadValue;
- state.Current.ObjectState = StackFrameObjectState.ReadAheadRefEndObject;
- }
- else if (state.Current.ObjectState == StackFrameObjectState.ReadIdValue)
- {
- if (reader.TokenType != JsonTokenType.String)
- {
- ThrowHelper.ThrowJsonException_MetadataValueWasNotString(reader.TokenType);
+ // Read the property value.
+ if (!reader.Read())
+ {
+ return false;
+ }
}
- converter.CreateInstanceForReferenceResolver(ref reader, ref state, options);
+ Debug.Assert(state.Current.PropertyState == StackFramePropertyState.ReadValue);
- string referenceId = reader.GetString()!;
- state.ReferenceResolver.AddReference(referenceId, state.Current.ReturnValue!);
-
- // Need to Read $values property name.
- state.Current.ObjectState = StackFrameObjectState.ReadAheadValuesName;
- }
-
- // Clear the metadata property name that was set in case of failure on ResolverReference/AddReference.
- state.Current.JsonPropertyName = null;
-
- if (state.Current.ObjectState == StackFrameObjectState.ReadAheadRefEndObject)
- {
- if (!TryReadAheadMetadataAndSetState(ref reader, ref state, StackFrameObjectState.ReadRefEndObject))
- {
- return false;
- }
- }
-
- if (state.Current.ObjectState == StackFrameObjectState.ReadRefEndObject)
- {
- if (reader.TokenType != JsonTokenType.EndObject)
+ switch (state.Current.LatestMetadataPropertyName)
{
- // We just read a property. The only valid next tokens are EndObject and PropertyName.
- Debug.Assert(reader.TokenType == JsonTokenType.PropertyName);
+ case MetadataPropertyName.Id:
+ if (reader.TokenType != JsonTokenType.String)
+ {
+ ThrowHelper.ThrowJsonException_MetadataValueWasNotString(reader.TokenType);
+ }
- ThrowHelper.ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties(reader.GetSpan(), ref state);
- }
+ if (state.ReferenceId != null)
+ {
+ ThrowHelper.ThrowNotSupportedException_ObjectWithParameterizedCtorRefMetadataNotSupported(s_refPropertyName, ref reader, ref state);
+ }
- return true;
- }
+ state.ReferenceId = reader.GetString();
+ break;
- if (state.Current.ObjectState == StackFrameObjectState.ReadAheadValuesName)
- {
- if (!TryReadAheadMetadataAndSetState(ref reader, ref state, StackFrameObjectState.ReadValuesName))
- {
- return false;
- }
- }
+ case MetadataPropertyName.Ref:
+ if (reader.TokenType != JsonTokenType.String)
+ {
+ ThrowHelper.ThrowJsonException_MetadataValueWasNotString(reader.TokenType);
+ }
- if (state.Current.ObjectState == StackFrameObjectState.ReadValuesName)
- {
- if (reader.TokenType != JsonTokenType.PropertyName)
- {
- ThrowHelper.ThrowJsonException_MetadataPreservedArrayValuesNotFound(ref state, converter.TypeToConvert);
- }
+ if (state.ReferenceId != null)
+ {
+ ThrowHelper.ThrowNotSupportedException_ObjectWithParameterizedCtorRefMetadataNotSupported(s_refPropertyName, ref reader, ref state);
+ }
- ReadOnlySpan<byte> propertyName = reader.GetSpan();
+ state.ReferenceId = reader.GetString();
+ break;
- if (GetMetadataPropertyName(propertyName) != MetadataPropertyName.Values)
- {
- ThrowHelper.ThrowJsonException_MetadataPreservedArrayInvalidProperty(ref state, converter.TypeToConvert, reader);
- }
+ case MetadataPropertyName.Values:
- // Remember the property in case we get an exception in one of the array elements.
- state.Current.JsonPropertyName = s_valuesPropertyName;
+ if (reader.TokenType != JsonTokenType.StartArray)
+ {
+ ThrowHelper.ThrowJsonException_MetadataValuesInvalidToken(reader.TokenType);
+ }
- state.Current.ObjectState = StackFrameObjectState.ReadAheadValuesStartArray;
- }
+ state.Current.PropertyState = StackFramePropertyState.None;
+ state.Current.MetadataPropertyNames |= state.Current.LatestMetadataPropertyName;
+ return true; // "$values" property contains the nested payload, exit the metadata reader now.
- if (state.Current.ObjectState == StackFrameObjectState.ReadAheadValuesStartArray)
- {
- if (!TryReadAheadMetadataAndSetState(ref reader, ref state, StackFrameObjectState.ReadValuesStartArray))
- {
- return false;
+ default:
+ Debug.Fail("Non-metadata properties should not reach this stage.");
+ break;
}
- }
-
- if (state.Current.ObjectState == StackFrameObjectState.ReadValuesStartArray)
- {
- // Temporary workaround for the state machine accidentally
- // erasing the JsonPropertyName property in certain async
- // re-entrancy patterns.
- state.Current.JsonPropertyName = s_valuesPropertyName;
- if (reader.TokenType != JsonTokenType.StartArray)
- {
- ThrowHelper.ThrowJsonException_MetadataValuesInvalidToken(reader.TokenType);
- }
- state.Current.ValidateEndTokenOnArray = true;
- state.Current.ObjectState = StackFrameObjectState.CreatedObject;
+ state.Current.MetadataPropertyNames |= state.Current.LatestMetadataPropertyName;
+ state.Current.PropertyState = StackFramePropertyState.None;
+ state.Current.JsonPropertyName = null;
}
-
- return true;
}
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool TryReadAheadMetadataAndSetState(ref Utf8JsonReader reader, ref ReadStack state, StackFrameObjectState nextState)
- {
- // If we can't read here, the read will be completed at the root API by asking the stream for more data.
- // Set the state so we know where to resume on re-entry.
- state.Current.ObjectState = nextState;
- return reader.Read();
- }
-
internal static MetadataPropertyName GetMetadataPropertyName(ReadOnlySpan<byte> propertyName)
{
if (propertyName.Length > 0 && propertyName[0] == '$')
}
}
- return MetadataPropertyName.NoMetadata;
+ return MetadataPropertyName.None;
}
- internal static bool TryGetReferenceFromJsonElement(
+ internal static bool TryHandleReferenceFromJsonElement(
+ ref Utf8JsonReader reader,
ref ReadStack state,
JsonElement element,
[NotNullWhen(true)] out object? referenceValue)
// There are more properties in an object with $ref.
ThrowHelper.ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties();
}
+ else if (property.EscapedNameEquals(s_idPropertyName))
+ {
+ if (state.ReferenceId != null)
+ {
+ ThrowHelper.ThrowNotSupportedException_ObjectWithParameterizedCtorRefMetadataNotSupported(s_refPropertyName, ref reader, ref state);
+ }
+
+ if (property.Value.ValueKind != JsonValueKind.String)
+ {
+ ThrowHelper.ThrowJsonException_MetadataValueWasNotString(property.Value.ValueKind);
+ }
+
+ object boxedElement = element;
+ state.ReferenceResolver.AddReference(property.Value.GetString()!, boxedElement);
+ referenceValue = boxedElement;
+ return true;
+ }
else if (property.EscapedNameEquals(s_refPropertyName))
{
+ if (state.ReferenceId != null)
+ {
+ ThrowHelper.ThrowNotSupportedException_ObjectWithParameterizedCtorRefMetadataNotSupported(s_refPropertyName, ref reader, ref state);
+ }
+
if (propertyCount > 1)
{
// $ref was found but there were other properties before.
return refMetadataFound;
}
- private static void ValidateValueIsCorrectType<T>(object value, string referenceId)
+ internal static bool TryHandleReferenceFromJsonNode(
+ ref Utf8JsonReader reader,
+ ref ReadStack state,
+ JsonNode jsonNode,
+ [NotNullWhen(true)] out object? referenceValue)
+ {
+ bool refMetadataFound = false;
+ referenceValue = default;
+
+ if (jsonNode is JsonObject jsonObject)
+ {
+ int propertyCount = 0;
+ foreach (var property in jsonObject)
+ {
+ propertyCount++;
+ if (refMetadataFound)
+ {
+ // There are more properties in an object with $ref.
+ ThrowHelper.ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties();
+ }
+ else if (property.Key == "$id")
+ {
+ if (state.ReferenceId != null)
+ {
+ ThrowHelper.ThrowNotSupportedException_ObjectWithParameterizedCtorRefMetadataNotSupported(s_refPropertyName, ref reader, ref state);
+ }
+
+ string referenceId = ReadAsStringMetadataValue(property.Value);
+ state.ReferenceResolver.AddReference(referenceId, jsonNode);
+ referenceValue = jsonNode;
+ return true;
+ }
+ else if (property.Key == "$ref")
+ {
+ if (state.ReferenceId != null)
+ {
+ ThrowHelper.ThrowNotSupportedException_ObjectWithParameterizedCtorRefMetadataNotSupported(s_refPropertyName, ref reader, ref state);
+ }
+
+ if (propertyCount > 1)
+ {
+ // $ref was found but there were other properties before.
+ ThrowHelper.ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties();
+ }
+
+ string referenceId = ReadAsStringMetadataValue(property.Value);
+ referenceValue = state.ReferenceResolver.ResolveReference(referenceId);
+ refMetadataFound = true;
+ }
+
+ static string ReadAsStringMetadataValue(JsonNode? jsonNode)
+ {
+ if (jsonNode is JsonValue jsonValue &&
+ jsonValue.TryGetValue(out string? value) &&
+ value is not null)
+ {
+ return value;
+ }
+
+ JsonValueKind metadataValueKind = jsonNode switch
+ {
+ null => JsonValueKind.Null,
+ JsonObject => JsonValueKind.Object,
+ JsonArray => JsonValueKind.Array,
+ JsonValue<JsonElement> element => element.Value.ValueKind,
+ _ => JsonValueKind.Undefined,
+ };
+
+ Debug.Assert(metadataValueKind != JsonValueKind.Undefined);
+ ThrowHelper.ThrowJsonException_MetadataValueWasNotString(metadataValueKind);
+ return null!;
+ }
+ }
+ }
+
+ return refMetadataFound;
+ }
+
+ internal static void ValidateMetadataForObjectConverter(JsonConverter converter, ref Utf8JsonReader reader, ref ReadStack state)
+ {
+ if (state.Current.MetadataPropertyNames.HasFlag(MetadataPropertyName.Values))
+ {
+ // Object converters do not support $values metadata.
+ ThrowHelper.ThrowJsonException_MetadataInvalidPropertyWithLeadingDollarSign(s_valuesPropertyName, ref state, reader);
+ }
+ }
+
+ internal static void ValidateMetadataForArrayConverter(JsonConverter converter, ref Utf8JsonReader reader, ref ReadStack state)
+ {
+ switch (reader.TokenType)
+ {
+ case JsonTokenType.StartArray:
+ Debug.Assert(state.Current.MetadataPropertyNames == MetadataPropertyName.None || state.Current.LatestMetadataPropertyName == MetadataPropertyName.Values);
+ break;
+
+ case JsonTokenType.EndObject:
+ if (state.Current.MetadataPropertyNames != MetadataPropertyName.Ref)
+ {
+ // Read the entire JSON object while parsing for metadata: for collection converters this is only legal for $ref nodes.
+ ThrowHelper.ThrowJsonException_MetadataPreservedArrayValuesNotFound(ref state, converter.TypeToConvert);
+ }
+ break;
+
+ default:
+ Debug.Assert(reader.TokenType == JsonTokenType.PropertyName);
+ // Do not tolerate non-metadata properties in collection converters.
+ ThrowHelper.ThrowJsonException_MetadataPreservedArrayInvalidProperty(ref state, converter.TypeToConvert, reader);
+ break;
+ }
+ }
+
+ internal static T ResolveReferenceId<T>(ref ReadStack state)
{
+ Debug.Assert(!typeof(T).IsValueType);
+ Debug.Assert(state.ReferenceId != null);
+
+ string referenceId = state.ReferenceId;
+ object value = state.ReferenceResolver.ResolveReference(referenceId);
+ state.ReferenceId = null;
+
try
{
- // No need to worry about unboxing here since T will always be a reference type at this point.
- T _ = (T)value;
+ return (T)value;
}
catch (InvalidCastException)
{
ThrowHelper.ThrowInvalidOperationException_MetadataReferenceOfTypeCannotBeAssignedToType(
referenceId, value.GetType(), typeof(T));
- throw;
+ return default!;
}
}
}
unescapedPropertyName = propertyName;
}
- if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve)
+ if (state.CanContainMetadata)
{
if (propertyName.Length > 0 && propertyName[0] == '$')
{
{
if (state.NewReferenceId != null)
{
- Debug.Assert(jsonConverter.CanHaveIdMetadata);
+ Debug.Assert(jsonConverter.CanHaveMetadata);
writer.WriteString(s_metadataId, state.NewReferenceId);
state.NewReferenceId = null;
return MetadataPropertyName.Id;
}
- return MetadataPropertyName.NoMetadata;
+ return MetadataPropertyName.None;
}
internal static MetadataPropertyName WriteReferenceForCollection(
{
if (state.NewReferenceId != null)
{
- Debug.Assert(jsonConverter.CanHaveIdMetadata);
+ Debug.Assert(jsonConverter.CanHaveMetadata);
writer.WriteStartObject();
writer.WriteString(s_metadataId, state.NewReferenceId);
writer.WriteStartArray(s_metadataValues);
// If the jsonConverter supports immutable enumerables or value type collections, don't write any metadata
writer.WriteStartArray();
- return MetadataPropertyName.NoMetadata;
+ return MetadataPropertyName.None;
}
/// <summary>
namespace System.Text.Json
{
- internal enum MetadataPropertyName
+ [Flags]
+ internal enum MetadataPropertyName : byte
{
- NoMetadata,
- Values,
- Id,
- Ref,
+ None = 0,
+ Values = 1,
+ Id = 2,
+ Ref = 4,
}
}
public bool SupportContinuation;
/// <summary>
+ /// Holds the value of $id or $ref of the currently read object
+ /// </summary>
+ public string? ReferenceId;
+
+ /// <summary>
/// Whether we can read without the need of saving state for stream and preserve references cases.
/// </summary>
public bool UseFastPath;
/// <summary>
+ /// Global flag indicating whether the current deserializer supports metadata.
+ /// </summary>
+ public bool CanContainMetadata;
+
+ /// <summary>
/// Ensures that the stack buffer has sufficient capacity to hold an additional frame.
/// </summary>
private void EnsurePushCapacity()
internal void Initialize(JsonTypeInfo jsonTypeInfo, bool supportContinuation = false)
{
- Current.JsonTypeInfo = jsonTypeInfo;
-
- // The initial JsonPropertyInfo will be used to obtain the converter.
- Current.JsonPropertyInfo = jsonTypeInfo.PropertyInfoForTypeInfo;
-
- Current.NumberHandling = Current.JsonPropertyInfo.NumberHandling;
-
JsonSerializerOptions options = jsonTypeInfo.Options;
- bool preserveReferences = options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve;
- if (preserveReferences)
+ if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve)
{
ReferenceResolver = options.ReferenceHandler!.CreateResolver(writing: false);
+ CanContainMetadata = true;
}
SupportContinuation = supportContinuation;
- UseFastPath = !supportContinuation && !preserveReferences;
+ Current.JsonTypeInfo = jsonTypeInfo;
+ Current.JsonPropertyInfo = jsonTypeInfo.PropertyInfoForTypeInfo;
+ Current.NumberHandling = Current.JsonPropertyInfo.NumberHandling;
+ UseFastPath = !supportContinuation && !CanContainMetadata;
}
public void Push()
// We are re-entering a continuation, adjust indices accordingly
if (_count++ > 0)
{
+ _stack[_count - 2] = Current;
Current = _stack[_count - 1];
}
}
}
+ // Traverses the stack for the outermost object being deserialized using constructor parameters
+ // Only called when calculating exception information.
+ public JsonTypeInfo GetTopJsonTypeInfoWithParameterizedConstructor()
+ {
+ Debug.Assert(!IsContinuation);
+
+ for (int i = 0; i < _count - 1; i++)
+ {
+ if (_stack[i].JsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase.ConstructorIsParameterized)
+ {
+ return _stack[i].JsonTypeInfo;
+ }
+ }
+
+ Debug.Assert(Current.JsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase.ConstructorIsParameterized);
+ return Current.JsonTypeInfo;
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetConstructorArgumentState()
{
public JsonTypeInfo JsonTypeInfo;
public StackFrameObjectState ObjectState; // State tracking the current object.
- // Validate EndObject token on array with preserve semantics.
- public bool ValidateEndTokenOnArray;
+ public MetadataPropertyName LatestMetadataPropertyName;
+ public MetadataPropertyName MetadataPropertyNames;
// For performance, we order the properties by the first deserialize and PropertyIndex helps find the right slot quicker.
public int PropertyIndex;
JsonPropertyName = null;
JsonPropertyNameAsString = null;
PropertyState = StackFramePropertyState.None;
- ValidateEndTokenOnArray = false;
// No need to clear these since they are overwritten each time:
// NumberHandling
None = 0,
StartToken,
-
- ReadAheadNameOrEndObject, // Try to move the reader to the the first $id, $ref, or the EndObject token.
- ReadNameOrEndObject, // Read the first $id, $ref, or the EndObject token.
-
- ReadAheadIdValue, // Try to move the reader to the value for $id.
- ReadAheadRefValue, // Try to move the reader to the value for $ref.
- ReadIdValue, // Read value for $id.
- ReadRefValue, // Read value for $ref.
- ReadAheadRefEndObject, // Try to move the reader to the EndObject for $ref.
- ReadRefEndObject, // Read the EndObject for $ref.
-
- ReadAheadValuesName, // Try to move the reader to the $values property name.
- ReadValuesName, // Read $values property name.
- ReadAheadValuesStartArray, // Try to move the reader to the StartArray for $values.
- ReadValuesStartArray, // Read the StartArray for $values.
-
- PropertyValue, // Whether all metadata properties has been read.
-
+ ReadMetadata,
+ ConstructorArguments,
CreatedObject,
ReadElements,
EndToken,
}
[DoesNotReturn]
- public static void ThrowNotSupportedException_ObjectWithParameterizedCtorRefMetadataNotHonored(
+ public static void ThrowNotSupportedException_ObjectWithParameterizedCtorRefMetadataNotSupported(
ReadOnlySpan<byte> propertyName,
ref Utf8JsonReader reader,
ref ReadStack state)
{
+ JsonTypeInfo jsonTypeInfo = state.GetTopJsonTypeInfoWithParameterizedConstructor();
state.Current.JsonPropertyName = propertyName.ToArray();
NotSupportedException ex = new NotSupportedException(
- SR.Format(SR.ObjectWithParameterizedCtorRefMetadataNotHonored, state.Current.JsonTypeInfo.Type));
+ SR.Format(SR.ObjectWithParameterizedCtorRefMetadataNotSupported, jsonTypeInfo.Type));
ThrowNotSupportedException(ref state, reader, ex);
}
ref Utf8JsonReader reader,
ref ReadStack state)
{
- if (state.Current.JsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase.ConstructorIsParameterized)
- {
- ThrowNotSupportedException_ObjectWithParameterizedCtorRefMetadataNotHonored(propertyName, ref reader, ref state);
- }
MetadataPropertyName name = JsonSerializer.GetMetadataPropertyName(propertyName);
if (name == MetadataPropertyName.Id)
string exStr = ex.ToString();
Assert.Contains("System.Text.Json.Serialization.Tests.ConstructorTests+Employee", exStr);
- Assert.Contains("$.$id", exStr);
+ Assert.Contains("$.Manager.$ref", exStr);
}
public class Employee
var options = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve };
- NotSupportedException ex = await Assert.ThrowsAsync<NotSupportedException>(() => Serializer.DeserializeWrapper<Employee>(json, options));
- string exStr = ex.ToString();
- Assert.Contains("System.Text.Json.Serialization.Tests.ConstructorTests+Employee", exStr);
- Assert.Contains("$.$random", exStr);
+ JsonException ex = await Assert.ThrowsAsync<JsonException>(() => Serializer.DeserializeWrapper<Employee>(json, options));
+ Assert.Equal("$.$random", ex.Path);
}
[Fact]
}";
JsonException ex = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<Employee>(json, s_deserializerOptionsPreserve));
- Assert.Equal("$.$ref", ex.Path);
+ Assert.Equal("$", ex.Path);
}
[Fact()]
}";
JsonException ex = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<List<Employee>>(json, s_deserializerOptionsPreserve));
- Assert.Equal("$.$ref", ex.Path);
+ Assert.Equal("$", ex.Path);
}
[Fact]
}";
JsonException ex = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<Dictionary<string, Employee>>(json, s_deserializerOptionsPreserve));
- Assert.Equal("$.$ref", ex.Path);
+ Assert.Equal("$", ex.Path);
}
[Fact]
}";
JsonException ex = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<Employee>(json, s_deserializerOptionsPreserve));
- Assert.Equal("$.Manager.$ref", ex.Path);
+ Assert.Equal("$.Manager", ex.Path);
}
[Fact]
}";
JsonException ex = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<Employee>(json, s_deserializerOptionsPreserve));
- Assert.Equal("$.Subordinates.$ref", ex.Path);
+ Assert.Equal("$.Subordinates", ex.Path);
}
[Fact]
}";
JsonException ex = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<Employee>(json, s_deserializerOptionsPreserve));
- Assert.Equal("$.Contacts.$ref", ex.Path);
+ Assert.Equal("$.Contacts", ex.Path);
}
#endregion
JsonException ex = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<List<Employee>>(json, s_deserializerOptionsPreserve));
Assert.Contains("'1'", ex.Message);
- Assert.Equal("$[0].$ref", ex.Path);
+ Assert.Equal("$[0]", ex.Path);
}
[Theory]
JsonException ex = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<List<Employee>>(json, s_deserializerOptionsPreserve));
- Assert.Equal("$[1].$id", ex.Path);
+ Assert.Equal("$[1]", ex.Path);
Assert.Contains("'1'", ex.Message);
}
JsonException ex = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<ClassWithTwoListProperties>(json, s_deserializerOptionsPreserve));
- Assert.Equal("$.List2.$id", ex.Path);
+ Assert.Equal("$.List2.$values", ex.Path);
Assert.Contains("'1'", ex.Message);
}
}";
JsonException ex = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<List<string>>(json, s_deserializerOptionsPreserve));
- Assert.Equal("$.$ref", ex.Path);
+ Assert.Equal("$", ex.Path);
// $id Valid under conditions: must be the first property in the object.
// $values Valid under conditions: must be after $id.
public class Derived : Base { }
public class Base { }
+
+ [Theory]
+ [InlineData(JsonUnknownTypeHandling.JsonElement)]
+ [InlineData(JsonUnknownTypeHandling.JsonNode)]
+ public async Task ObjectConverter_ShouldHandleReferenceMetadata(JsonUnknownTypeHandling typehandling)
+ {
+ var options = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve, UnknownTypeHandling = typehandling };
+ string json = @"[{ ""$id"" : ""1"" },{ ""$ref"" : ""1""}]";
+ object[] deserialized = await Serializer.DeserializeWrapper<object[]>(json, options);
+ Assert.Same(deserialized[0], deserialized[1]);
+ }
+
+ [Theory]
+ [InlineData(@"{ ""$id"" : 42 }", JsonUnknownTypeHandling.JsonElement)]
+ [InlineData(@"{ ""$id"" : 42 }", JsonUnknownTypeHandling.JsonNode)]
+ [InlineData(@"{ ""$ref"" : 42 }", JsonUnknownTypeHandling.JsonElement)]
+ [InlineData(@"{ ""$ref"" : 42 }", JsonUnknownTypeHandling.JsonNode)]
+ public async Task ObjectConverter_InvalidMetadataPropertyType_ShouldThrowJsonException(string json, JsonUnknownTypeHandling typehandling)
+ {
+ var options = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve, UnknownTypeHandling = typehandling };
+ await Assert.ThrowsAsync<JsonException>(() => Serializer.DeserializeWrapper<object>(json, options));
+ }
+
+ [Theory]
+ [InlineData(JsonUnknownTypeHandling.JsonElement)]
+ [InlineData(JsonUnknownTypeHandling.JsonNode)]
+ public async Task ObjectConverter_PropertyTrailingRefMetadata_ShouldThrowJsonException(JsonUnknownTypeHandling typehandling)
+ {
+ var options = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve, UnknownTypeHandling = typehandling };
+ string json = @"[{ ""$id"" : ""1"" }, { ""$ref"" : ""1"", ""trailingProperty"" : true }]";
+ await Assert.ThrowsAsync<JsonException>(() => Serializer.DeserializeWrapper<object[]>(json, options));
+ }
+
+ [Fact]
+ public async Task ConstructorDeserialization_ReferencePreservation()
+ {
+ string json = @"[{ ""$id"" : ""1"", ""Value"" : 42, ""Next"" : null }, { ""$ref"" : ""1"" }]";
+ LinkedList<int>[] deserialized = await Serializer.DeserializeWrapper<LinkedList<int>[]>(json, s_serializerOptionsPreserve);
+
+ Assert.Equal(2, deserialized.Length);
+ Assert.Equal(42, deserialized[0].Value);
+ Assert.Null(deserialized[0].Next);
+ Assert.Same(deserialized[0], deserialized[1]);
+ }
+
+ [Theory]
+ [InlineData(@"{ ""Value"" : 1, ""Next"" : { ""$id"" : ""1"", ""Value"" : 2, ""Next"" : null }}", typeof(LinkedList<int>))]
+ [InlineData(@"[{ ""$id"" : ""1"", ""Value"" : 2, ""Next"" : null }, { ""Value"" : 1, ""Next"" : { ""$ref"" : ""1""}}]", typeof(LinkedList<int>[]))]
+ public Task ConstructorDeserialization_NestedConstructorArgumentReference_SupportedScenaria(string json, Type type)
+ => Serializer.DeserializeWrapper(json, type, s_serializerOptionsPreserve);
+
+ [Theory]
+ [InlineData(@"{ ""$id"" : ""1"", ""Value"" : 1, ""Next"" : { ""$id"" : ""2"", ""Value"" : 2, ""Next"" : null }}", typeof(LinkedList<int>))]
+ [InlineData(@"{ ""$id"" : ""1"", ""Value"" : 1, ""Next"" : { ""$ref"" : ""1"" }}", typeof(LinkedList<int>))]
+ [InlineData(@"[{ ""$id"" : ""1"", ""Value"" : 2, ""Next"" : null }, { ""$id"" : ""2"", ""Value"" : 1, ""Next"" : { ""$ref"" : ""1""}}]", typeof(LinkedList<int>[]))]
+ [InlineData(@"{ ""$id"" : ""1"", ""Value"" : [{""$id"" : ""2""}], ""Next"" : null }", typeof(LinkedList<Base[]>))]
+ [InlineData(@"{ ""$id"" : ""1"", ""Value"" : [[{""$id"" : ""2""}]], ""Next"" : null }", typeof(LinkedList<Base[][]>))]
+ [InlineData(@"{ ""$id"" : ""1"", ""Value"" : [{""$ref"" : ""1""}], ""Next"" : null }", typeof(LinkedList<object[]>))]
+ [InlineData(@"{ ""$id"" : ""1"", ""PropertyWithSetter"" : { ""$id"" : ""2"" }}", typeof(LinkedList<object?>))]
+ [InlineData(@"{ ""$id"" : ""1"", ""PropertyWithSetter"" : { ""$ref"" : ""1"" }}", typeof(LinkedList<object?>))]
+ public async Task ConstructorDeserialization_NestedConstructorArgumentReference_ThrowsNotSupportedException(string json, Type type)
+ {
+ NotSupportedException ex = await Assert.ThrowsAsync<NotSupportedException>(() => Serializer.DeserializeWrapper(json, type, s_serializerOptionsPreserve));
+ Assert.Contains("LinkedList", ex.Message);
+ }
+
+ public class LinkedList<T>
+ {
+ [JsonConstructor]
+ public LinkedList(T value, LinkedList<T>? next)
+ {
+ Value = value;
+ Next = next;
+ }
+
+ public T Value { get; }
+ public LinkedList<T>? Next { get; }
+
+ public T? PropertyWithSetter { get; set; }
+ }
}
}
}
[Fact]
- public async Task PreserveReferenceOfTypeObjectAsync()
- {
- if (StreamingSerializer is null)
- {
- return;
- }
-
- var root = new ClassWithObjectProperty();
- root.Child = new ClassWithObjectProperty();
- root.Sibling = root.Child;
-
- Assert.Same(root.Child, root.Sibling);
-
- string json = await StreamingSerializer.SerializeWrapper(root, s_serializerOptionsPreserve);
-
- ClassWithObjectProperty rootCopy = await StreamingSerializer.DeserializeWrapper<ClassWithObjectProperty>(json, s_serializerOptionsPreserve);
- Assert.Same(rootCopy.Child, rootCopy.Sibling);
- }
-
- [Fact]
public async Task PreserveReferenceOfTypeOfObjectOnCollection()
{
var root = new ClassWithListOfObjectProperty();
[JsonSerializable(typeof(List<object>))]
[JsonSerializable(typeof(StructCollection))]
[JsonSerializable(typeof(ImmutableArray<int>))]
+ [JsonSerializable(typeof(LinkedList<int>))]
+ [JsonSerializable(typeof(LinkedList<int>[]))]
+ [JsonSerializable(typeof(LinkedList<object>))]
+ [JsonSerializable(typeof(LinkedList<object[]>))]
+ [JsonSerializable(typeof(LinkedList<Base[]>))]
+ [JsonSerializable(typeof(LinkedList<Base[][]>))]
internal sealed partial class ReferenceHandlerTestsContext_Metadata : JsonSerializerContext
{
}
[JsonSerializable(typeof(List<object>))]
[JsonSerializable(typeof(StructCollection))]
[JsonSerializable(typeof(ImmutableArray<int>))]
+ [JsonSerializable(typeof(LinkedList<int>))]
+ [JsonSerializable(typeof(LinkedList<int>[]))]
+ [JsonSerializable(typeof(LinkedList<object>))]
+ [JsonSerializable(typeof(LinkedList<object[]>))]
+ [JsonSerializable(typeof(LinkedList<Base[]>))]
+ [JsonSerializable(typeof(LinkedList<Base[][]>))]
internal sealed partial class ReferenceHandlerTestsContext_Default : JsonSerializerContext
{
}