// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization.Metadata;
using FoundProperty = System.ValueTuple<System.Text.Json.Serialization.Metadata.JsonPropertyInfo, System.Text.Json.JsonReaderState, long, byte[]?, string?>;
using FoundPropertyAsync = System.ValueTuple<System.Text.Json.Serialization.Metadata.JsonPropertyInfo, object?, string?>;
namespace System.Text.Json.Serialization.Converters
{
/// <summary>
/// Implementation of <cref>JsonObjectConverter{T}</cref> that supports the deserialization
/// of JSON objects using parameterized constructors.
/// </summary>
internal abstract partial class ObjectWithParameterizedConstructorConverter<T> : ObjectDefaultConverter<T> where T : notnull
{
internal sealed override bool ConstructorIsParameterized => true;
internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, scoped ref ReadStack state, [MaybeNullWhen(false)] out T value)
{
JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo;
if (!jsonTypeInfo.UsesParameterizedConstructor || state.Current.IsPopulating)
{
// Fall back to default object converter in following cases:
// - if user configuration has invalidated the parameterized constructor
// - we're continuing populating an object.
return base.OnTryRead(ref reader, typeToConvert, options, ref state, out value);
}
object obj;
ArgumentState argumentState = state.Current.CtorArgumentState!;
if (!state.SupportContinuation && !state.Current.CanContainMetadata)
{
// Fast path that avoids maintaining state variables.
if (reader.TokenType != JsonTokenType.StartObject)
{
ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(Type);
}
if (state.ParentProperty?.TryGetPrePopulatedValue(ref state) == true)
{
object populatedObject = state.Current.ReturnValue!;
PopulatePropertiesFastPath(populatedObject, jsonTypeInfo, options, ref reader, ref state);
value = (T)populatedObject;
return true;
}
ReadOnlySpan<byte> originalSpan = reader.OriginalSpan;
ReadOnlySequence<byte> originalSequence = reader.OriginalSequence;
ReadConstructorArguments(ref state, ref reader, options);
// We've read all ctor parameters and properties,
// validate that all required parameters were provided
// before calling the constructor which may throw.
state.Current.ValidateAllRequiredPropertiesAreRead(jsonTypeInfo);
obj = (T)CreateObject(ref state.Current);
jsonTypeInfo.OnDeserializing?.Invoke(obj);
if (argumentState.FoundPropertyCount > 0)
{
Utf8JsonReader tempReader;
FoundProperty[]? properties = argumentState.FoundProperties;
Debug.Assert(properties is not null);
for (int i = 0; i < argumentState.FoundPropertyCount; i++)
{
JsonPropertyInfo jsonPropertyInfo = properties[i].Item1;
long resumptionByteIndex = properties[i].Item3;
byte[]? propertyNameArray = properties[i].Item4;
string? dataExtKey = properties[i].Item5;
tempReader = originalSequence.IsEmpty
? new Utf8JsonReader(
originalSpan.Slice(checked((int)resumptionByteIndex)),
isFinalBlock: true,
state: properties[i].Item2)
: new Utf8JsonReader(
originalSequence.Slice(resumptionByteIndex),
isFinalBlock: true,
state: properties[i].Item2);
Debug.Assert(tempReader.TokenType == JsonTokenType.PropertyName);
state.Current.JsonPropertyName = propertyNameArray;
state.Current.JsonPropertyInfo = jsonPropertyInfo;
state.Current.NumberHandling = jsonPropertyInfo.EffectiveNumberHandling;
bool useExtensionProperty = dataExtKey is not null;
if (useExtensionProperty)
{
Debug.Assert(jsonPropertyInfo == state.Current.JsonTypeInfo.ExtensionDataProperty);
state.Current.JsonPropertyNameAsString = dataExtKey;
JsonSerializer.CreateExtensionDataProperty(obj, jsonPropertyInfo, options);
}
ReadPropertyValue(obj, ref state, ref tempReader, jsonPropertyInfo, useExtensionProperty);
}
FoundProperty[] toReturn = argumentState.FoundProperties!;
argumentState.FoundProperties = null;
ArrayPool<FoundProperty>.Shared.Return(toReturn, clearArray: true);
}
}
else
{
// Slower path that supports continuation and metadata reads.
if (state.Current.ObjectState == StackFrameObjectState.None)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(Type);
}
state.Current.ObjectState = StackFrameObjectState.StartToken;
}
// Read any metadata properties.
if (state.Current.CanContainMetadata && state.Current.ObjectState < StackFrameObjectState.ReadMetadata)
{
if (!JsonSerializer.TryReadMetadata(this, jsonTypeInfo, ref reader, ref state))
{
value = default;
return false;
}
if (state.Current.MetadataPropertyNames == MetadataPropertyName.Ref)
{
value = JsonSerializer.ResolveReferenceId<T>(ref state);
return true;
}
state.Current.ObjectState = StackFrameObjectState.ReadMetadata;
}
// Dispatch to any polymorphic converters: should always be entered regardless of ObjectState progress
if (((state.Current.MetadataPropertyNames & MetadataPropertyName.Type) != 0 || state.PolymorphicResolvedType is not null) &&
state.Current.PolymorphicSerializationState != PolymorphicSerializationState.PolymorphicReEntryStarted &&
ResolvePolymorphicConverter(jsonTypeInfo, ref state) is JsonConverter polymorphicConverter)
{
Debug.Assert(!IsValueType);
bool success = polymorphicConverter.OnTryReadAsObject(ref reader, polymorphicConverter.Type!, options, ref state, out object? objectResult);
value = (T)objectResult!;
state.ExitPolymorphicConverter(success);
return success;
}
// We need to populate before we started reading constructor arguments.
// Metadata is disallowed with Populate option and therefore ordering here is irrelevant.
// Since state.Current.IsPopulating is being checked early on in this method the continuation
// will be handled there.
if (state.ParentProperty?.TryGetPrePopulatedValue(ref state) == true)
{
object populatedObject = state.Current.ReturnValue!;
jsonTypeInfo.OnDeserializing?.Invoke(populatedObject);
state.Current.ObjectState = StackFrameObjectState.CreatedObject;
state.Current.InitializePropertiesValidationState(jsonTypeInfo);
return base.OnTryRead(ref reader, typeToConvert, options, ref state, out value);
}
// Handle metadata post polymorphic dispatch
if (state.Current.ObjectState < StackFrameObjectState.ConstructorArguments)
{
if (state.Current.CanContainMetadata)
{
JsonSerializer.ValidateMetadataForObjectConverter(ref state);
}
if (state.Current.MetadataPropertyNames == MetadataPropertyName.Ref)
{
value = JsonSerializer.ResolveReferenceId<T>(ref state);
return true;
}
BeginRead(ref state, options);
state.Current.ObjectState = StackFrameObjectState.ConstructorArguments;
}
if (!ReadConstructorArgumentsWithContinuation(ref state, ref reader, options))
{
value = default;
return false;
}
// We've read all ctor parameters and properties,
// validate that all required parameters were provided
// before calling the constructor which may throw.
state.Current.ValidateAllRequiredPropertiesAreRead(jsonTypeInfo);
obj = (T)CreateObject(ref state.Current);
if ((state.Current.MetadataPropertyNames & MetadataPropertyName.Id) != 0)
{
Debug.Assert(state.ReferenceId is not null);
Debug.Assert(options.ReferenceHandlingStrategy == JsonKnownReferenceHandler.Preserve);
state.ReferenceResolver.AddReference(state.ReferenceId, obj);
state.ReferenceId = null;
}
jsonTypeInfo.OnDeserializing?.Invoke(obj);
if (argumentState.FoundPropertyCount > 0)
{
for (int i = 0; i < argumentState.FoundPropertyCount; i++)
{
JsonPropertyInfo jsonPropertyInfo = argumentState.FoundPropertiesAsync![i].Item1;
object? propValue = argumentState.FoundPropertiesAsync![i].Item2;
string? dataExtKey = argumentState.FoundPropertiesAsync![i].Item3;
if (dataExtKey is null)
{
Debug.Assert(jsonPropertyInfo.Set is not null);
if (propValue is not null || !jsonPropertyInfo.IgnoreNullTokensOnRead || default(T) is not null)
{
jsonPropertyInfo.Set(obj, propValue);
}
}
else
{
Debug.Assert(jsonPropertyInfo == state.Current.JsonTypeInfo.ExtensionDataProperty);
JsonSerializer.CreateExtensionDataProperty(obj, jsonPropertyInfo, options);
object extDictionary = jsonPropertyInfo.GetValueAsObject(obj)!;
if (extDictionary is IDictionary<string, JsonElement> dict)
{
if (options.AllowDuplicateProperties)
{
dict[dataExtKey] = (JsonElement)propValue!;
}
else if (!dict.TryAdd(dataExtKey, (JsonElement)propValue!))
{
ThrowHelper.ThrowJsonException_DuplicatePropertyNotAllowed(dataExtKey);
}
}
else
{
IDictionary<string, object> objDict = (IDictionary<string, object>)extDictionary;
if (options.AllowDuplicateProperties)
{
objDict[dataExtKey] = propValue!;
}
else if (!objDict.TryAdd(dataExtKey, propValue!))
{
ThrowHelper.ThrowJsonException_DuplicatePropertyNotAllowed(dataExtKey);
}
}
}
}
FoundPropertyAsync[] toReturn = argumentState.FoundPropertiesAsync!;
argumentState.FoundPropertiesAsync = null;
ArrayPool<FoundPropertyAsync>.Shared.Return(toReturn, clearArray: true);
}
}
jsonTypeInfo.OnDeserialized?.Invoke(obj);
// Unbox
Debug.Assert(obj is not null);
value = (T)obj;
// Check if we are trying to update the UTF-8 property cache.
if (state.Current.PropertyRefCacheBuilder is not null)
{
jsonTypeInfo.UpdateUtf8PropertyCache(ref state.Current);
}
return true;
}
protected abstract void InitializeConstructorArgumentCaches(ref ReadStack state, JsonSerializerOptions options);
protected abstract bool ReadAndCacheConstructorArgument(scoped ref ReadStack state, ref Utf8JsonReader reader, JsonParameterInfo jsonParameterInfo);
protected abstract object CreateObject(ref ReadStackFrame frame);
/// <summary>
/// Performs a full first pass of the JSON input and deserializes the ctor args.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ReadConstructorArguments(scoped ref ReadStack state, ref Utf8JsonReader reader, JsonSerializerOptions options)
{
BeginRead(ref state, options);
while (true)
{
// Read the next property name or EndObject.
reader.ReadWithVerify();
JsonTokenType tokenType = reader.TokenType;
if (tokenType == JsonTokenType.EndObject)
{
return;
}
// Read method would have thrown if otherwise.
Debug.Assert(tokenType == JsonTokenType.PropertyName);
ReadOnlySpan<byte> unescapedPropertyName = JsonSerializer.GetPropertyName(ref state, ref reader, options, out bool isAlreadyReadMetadataProperty);
if (isAlreadyReadMetadataProperty)
{
Debug.Assert(options.AllowOutOfOrderMetadataProperties);
reader.SkipWithVerify();
state.Current.EndProperty();
continue;
}
if (TryLookupConstructorParameter(
unescapedPropertyName,
ref state,
options,
out JsonPropertyInfo jsonPropertyInfo,
out JsonParameterInfo? jsonParameterInfo))
{
// Set the property value.
reader.ReadWithVerify();
if (!jsonParameterInfo.ShouldDeserialize)
{
// The Utf8JsonReader.Skip() method will fail fast if it detects that we're reading
// from a partially read buffer, regardless of whether the next value is available.
// This can result in erroneous failures in cases where a custom converter is calling
// into a built-in converter (cf. https://github.com/dotnet/runtime/issues/74108).
// For this reason we need to call the TrySkip() method instead -- the serializer
// should guarantee sufficient read-ahead has been performed for the current object.
bool success = reader.TrySkip();
Debug.Assert(success, "Serializer should guarantee sufficient read-ahead has been done.");
state.Current.EndConstructorParameter();
continue;
}
Debug.Assert(jsonParameterInfo.MatchingProperty is not null);
ReadAndCacheConstructorArgument(ref state, ref reader, jsonParameterInfo);
state.Current.EndConstructorParameter();
}
else
{
if (jsonPropertyInfo.CanDeserialize)
{
ArgumentState argumentState = state.Current.CtorArgumentState!;
if (argumentState.FoundProperties is null)
{
argumentState.FoundProperties =
ArrayPool<FoundProperty>.Shared.Rent(Math.Max(1, state.Current.JsonTypeInfo.PropertyCache.Length));
}
else if (argumentState.FoundPropertyCount == argumentState.FoundProperties.Length)
{
// Rare case where we can't fit all the JSON properties in the rented pool; we have to grow.
// This could happen if there are duplicate properties in the JSON.
var newCache = ArrayPool<FoundProperty>.Shared.Rent(argumentState.FoundProperties.Length * 2);
argumentState.FoundProperties.CopyTo(newCache, 0);
FoundProperty[] toReturn = argumentState.FoundProperties;
argumentState.FoundProperties = newCache!;
ArrayPool<FoundProperty>.Shared.Return(toReturn, clearArray: true);
}
argumentState.FoundProperties[argumentState.FoundPropertyCount++] = (
jsonPropertyInfo,
reader.CurrentState,
reader.BytesConsumed,
state.Current.JsonPropertyName,
state.Current.JsonPropertyNameAsString);
}
reader.SkipWithVerify();
state.Current.EndProperty();
}
}
}
private bool ReadConstructorArgumentsWithContinuation(scoped ref ReadStack state, ref Utf8JsonReader reader, JsonSerializerOptions options)
{
// Process all properties.
while (true)
{
// Determine the property.
if (state.Current.PropertyState == StackFramePropertyState.None)
{
if (!reader.Read())
{
return false;
}
state.Current.PropertyState = StackFramePropertyState.ReadName;
}
JsonParameterInfo? jsonParameterInfo;
JsonPropertyInfo? jsonPropertyInfo;
if (state.Current.PropertyState < StackFramePropertyState.Name)
{
JsonTokenType tokenType = reader.TokenType;
if (tokenType == JsonTokenType.EndObject)
{
return true;
}
// Read method would have thrown if otherwise.
Debug.Assert(tokenType == JsonTokenType.PropertyName);
ReadOnlySpan<byte> unescapedPropertyName = JsonSerializer.GetPropertyName(ref state, ref reader, options, out bool isAlreadyReadMetadataProperty);
if (isAlreadyReadMetadataProperty)
{
Debug.Assert(options.AllowOutOfOrderMetadataProperties);
reader.SkipWithVerify();
state.Current.EndProperty();
continue;
}
if (TryLookupConstructorParameter(
unescapedPropertyName,
ref state,
options,
out jsonPropertyInfo,
out jsonParameterInfo))
{
jsonPropertyInfo = null;
}
state.Current.PropertyState = StackFramePropertyState.Name;
}
else
{
jsonParameterInfo = state.Current.CtorArgumentState!.JsonParameterInfo;
jsonPropertyInfo = state.Current.JsonPropertyInfo;
}
if (jsonParameterInfo is not null)
{
Debug.Assert(jsonPropertyInfo is null);
if (!HandleConstructorArgumentWithContinuation(ref state, ref reader, jsonParameterInfo))
{
return false;
}
}
else
{
if (!HandlePropertyWithContinuation(ref state, ref reader, jsonPropertyInfo!))
{
return false;
}
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool HandleConstructorArgumentWithContinuation(
scoped ref ReadStack state,
ref Utf8JsonReader reader,
JsonParameterInfo jsonParameterInfo)
{
if (state.Current.PropertyState < StackFramePropertyState.ReadValue)
{
if (!jsonParameterInfo.ShouldDeserialize)
{
if (!reader.TrySkipPartial(targetDepth: state.Current.OriginalDepth + 1))
{
return false;
}
state.Current.EndConstructorParameter();
return true;
}
if (!reader.TryAdvanceWithOptionalReadAhead(jsonParameterInfo.EffectiveConverter.RequiresReadAhead))
{
return false;
}
state.Current.PropertyState = StackFramePropertyState.ReadValue;
}
if (!ReadAndCacheConstructorArgument(ref state, ref reader, jsonParameterInfo))
{
return false;
}
state.Current.EndConstructorParameter();
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool HandlePropertyWithContinuation(
scoped ref ReadStack state,
ref Utf8JsonReader reader,
JsonPropertyInfo jsonPropertyInfo)
{
if (state.Current.PropertyState < StackFramePropertyState.ReadValue)
{
if (!jsonPropertyInfo.CanDeserialize)
{
if (!reader.TrySkipPartial(targetDepth: state.Current.OriginalDepth + 1))
{
return false;
}
state.Current.EndProperty();
return true;
}
if (!ReadAheadPropertyValue(ref state, ref reader, jsonPropertyInfo))
{
return false;
}
state.Current.PropertyState = StackFramePropertyState.ReadValue;
}
object? propValue;
if (state.Current.UseExtensionProperty)
{
if (!jsonPropertyInfo.ReadJsonExtensionDataValue(ref state, ref reader, out propValue))
{
return false;
}
}
else
{
if (!jsonPropertyInfo.ReadJsonAsObject(ref state, ref reader, out propValue))
{
return false;
}
}
Debug.Assert(jsonPropertyInfo.CanDeserialize);
// Ensure that the cache has enough capacity to add this property.
ArgumentState argumentState = state.Current.CtorArgumentState!;
if (argumentState.FoundPropertiesAsync is null)
{
argumentState.FoundPropertiesAsync = ArrayPool<FoundPropertyAsync>.Shared.Rent(Math.Max(1, state.Current.JsonTypeInfo.PropertyCache.Length));
}
else if (argumentState.FoundPropertyCount == argumentState.FoundPropertiesAsync!.Length)
{
// Rare case where we can't fit all the JSON properties in the rented pool; we have to grow.
// This could happen if there are duplicate properties in the JSON.
var newCache = ArrayPool<FoundPropertyAsync>.Shared.Rent(argumentState.FoundPropertiesAsync!.Length * 2);
argumentState.FoundPropertiesAsync!.CopyTo(newCache, 0);
FoundPropertyAsync[] toReturn = argumentState.FoundPropertiesAsync!;
argumentState.FoundPropertiesAsync = newCache!;
ArrayPool<FoundPropertyAsync>.Shared.Return(toReturn, clearArray: true);
}
// Cache the property name and value.
argumentState.FoundPropertiesAsync![argumentState.FoundPropertyCount++] = (
jsonPropertyInfo,
propValue,
state.Current.JsonPropertyNameAsString);
state.Current.EndProperty();
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void BeginRead(scoped ref ReadStack state, JsonSerializerOptions options)
{
JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo;
jsonTypeInfo.ValidateCanBeUsedForPropertyMetadataSerialization();
if (jsonTypeInfo.ParameterCount != jsonTypeInfo.ParameterCache.Length)
{
ThrowHelper.ThrowInvalidOperationException_ConstructorParameterIncompleteBinding(Type);
}
state.Current.InitializePropertiesValidationState(jsonTypeInfo);
// Set current JsonPropertyInfo to null to avoid conflicts on push.
state.Current.JsonPropertyInfo = null;
Debug.Assert(state.Current.CtorArgumentState is not null);
InitializeConstructorArgumentCaches(ref state, options);
}
/// <summary>
/// Lookup the constructor parameter given its name in the reader.
/// </summary>
protected static bool TryLookupConstructorParameter(
scoped ReadOnlySpan<byte> unescapedPropertyName,
scoped ref ReadStack state,
JsonSerializerOptions options,
out JsonPropertyInfo jsonPropertyInfo,
[NotNullWhen(true)] out JsonParameterInfo? jsonParameterInfo)
{
Debug.Assert(state.Current.JsonTypeInfo.Kind is JsonTypeInfoKind.Object);
Debug.Assert(state.Current.CtorArgumentState is not null);
jsonPropertyInfo = JsonSerializer.LookupProperty(
obj: null,
unescapedPropertyName,
ref state,
options,
out bool useExtensionProperty,
createExtensionProperty: false);
// Mark the property as read from the payload if it is mapped to a non-extension member.
if (!useExtensionProperty && jsonPropertyInfo != JsonPropertyInfo.s_missingProperty)
{
state.Current.MarkPropertyAsRead(jsonPropertyInfo);
}
jsonParameterInfo = jsonPropertyInfo.AssociatedParameter;
if (jsonParameterInfo is not null)
{
state.Current.JsonPropertyInfo = null;
state.Current.CtorArgumentState!.JsonParameterInfo = jsonParameterInfo;
state.Current.NumberHandling = jsonParameterInfo.NumberHandling;
return true;
}
else
{
state.Current.UseExtensionProperty = useExtensionProperty;
return false;
}
}
}
}