<Compile Include="System\Text\Json\Serialization\JsonSerializer.Write.Node.cs" />
<Compile Include="System\Text\Json\Serialization\JsonSerializerContext.cs" />
<Compile Include="System\Text\Json\Serialization\JsonSerializerOptions.Caching.cs" />
+ <Compile Include="System\Text\Json\Serialization\PolymorphicSerializationState.cs" />
<Compile Include="System\Text\Json\Serialization\ReferenceEqualsWrapper.cs" />
<Compile Include="System\Text\Json\Serialization\ConverterStrategy.cs" />
<Compile Include="System\Text\Json\Serialization\ConverterList.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Node\JsonValueConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Object\JsonObjectConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Object\KeyValuePairConverter.cs" />
+ <Compile Include="System\Text\Json\Serialization\Converters\Object\ObjectConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Object\ObjectConverterFactory.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Object\ObjectDefaultConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Object\ObjectWithParameterizedConstructorConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Value\JsonElementConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Value\NullableConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Value\NullableConverterFactory.cs" />
- <Compile Include="System\Text\Json\Serialization\Converters\Value\ObjectConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Value\SByteConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Value\SingleConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Value\StringConverter.cs" />
/// </remarks>
internal enum ConverterStrategy : byte
{
- // Default - no class type.
+ /// <summary>
+ /// Default value; not used by any converter.
+ /// </summary>
None = 0x0,
- // JsonObjectConverter<> - objects with properties.
+ /// <summary>
+ /// Objects with properties.
+ /// </summary>
Object = 0x1,
- // JsonConverter<> - simple values.
+ /// <summary>
+ /// Simple values or user-provided custom converters.
+ /// </summary>
Value = 0x2,
- // JsonIEnumerableConverter<> - all enumerable collections except dictionaries.
+ /// <summary>
+ /// Enumerable collections except dictionaries.
+ /// </summary>
Enumerable = 0x8,
- // JsonDictionaryConverter<,> - dictionary types.
+ /// <summary>
+ /// Dictionary types.
+ /// </summary>
Dictionary = 0x10,
}
}
{
state.Current.PropertyState = StackFramePropertyState.ReadValue;
- if (!SingleValueReadWithReadAhead(elementConverter.ConverterStrategy, ref reader, ref state))
+ if (!SingleValueReadWithReadAhead(elementConverter.RequiresReadAhead, ref reader, ref state))
{
value = default;
return false;
state.Current.ProcessedStartToken = true;
if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve)
{
- MetadataPropertyName metadata = JsonSerializer.WriteReferenceForCollection(this, value, ref state, writer);
- if (metadata == MetadataPropertyName.Ref)
- {
- return true;
- }
-
+ MetadataPropertyName metadata = JsonSerializer.WriteReferenceForCollection(this, ref state, writer);
+ Debug.Assert(metadata != MetadataPropertyName.Ref);
state.Current.MetadataPropertyName = metadata;
}
else
{
state.Current.PropertyState = StackFramePropertyState.ReadValue;
- if (!SingleValueReadWithReadAhead(_valueConverter.ConverterStrategy, ref reader, ref state))
+ if (!SingleValueReadWithReadAhead(_valueConverter.RequiresReadAhead, ref reader, ref state))
{
state.Current.DictionaryKey = key;
value = default;
writer.WriteStartObject();
if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve)
{
- if (JsonSerializer.WriteReferenceForObject(this, dictionary, ref state, writer) == MetadataPropertyName.Ref)
- {
- return true;
- }
+ MetadataPropertyName propertyName = JsonSerializer.WriteReferenceForObject(this, ref state, writer);
+ Debug.Assert(propertyName != MetadataPropertyName.Ref);
}
state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
_optionValueGetter = FSharpCoreReflectionProxy.Instance.CreateFSharpOptionValueGetter<TOption, TElement>();
_optionConstructor = FSharpCoreReflectionProxy.Instance.CreateFSharpOptionSomeConstructor<TOption, TElement>();
- // temporary workaround for JsonConverter base constructor needing to access
- // ConverterStrategy when calculating `CanUseDirectReadOrWrite`.
- // TODO move `CanUseDirectReadOrWrite` from JsonConverter to JsonTypeInfo.
- _converterStrategy = _elementConverter.ConverterStrategy;
- CanUseDirectReadOrWrite = _converterStrategy == ConverterStrategy.Value;
+ // Workaround for the base constructor depending on the (still unset) ConverterStrategy
+ // to derive the CanUseDirectReadOrWrite and RequiresReadAhead values.
+ _converterStrategy = elementConverter.ConverterStrategy;
+ CanUseDirectReadOrWrite = elementConverter.CanUseDirectReadOrWrite;
+ RequiresReadAhead = elementConverter.RequiresReadAhead;
}
internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out TOption? value)
_optionValueGetter = FSharpCoreReflectionProxy.Instance.CreateFSharpValueOptionValueGetter<TValueOption, TElement>();
_optionConstructor = FSharpCoreReflectionProxy.Instance.CreateFSharpValueOptionSomeConstructor<TValueOption, TElement>();
- // temporary workaround for JsonConverter base constructor needing to access
- // ConverterStrategy when calculating `CanUseDirectReadOrWrite`.
- // TODO move `CanUseDirectReadOrWrite` from JsonConverter to JsonTypeInfo.
- _converterStrategy = _elementConverter.ConverterStrategy;
- CanUseDirectReadOrWrite = _converterStrategy == ConverterStrategy.Value;
+ // Workaround for the base constructor depending on the (still unset) ConverterStrategy
+ // to derive the CanUseDirectReadOrWrite and RequiresReadAhead values.
+ _converterStrategy = elementConverter.ConverterStrategy;
+ CanUseDirectReadOrWrite = elementConverter.CanUseDirectReadOrWrite;
+ RequiresReadAhead = elementConverter.RequiresReadAhead;
}
internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out TValueOption value)
{
internal sealed class ObjectConverter : JsonConverter<object?>
{
+ internal override ConverterStrategy ConverterStrategy => ConverterStrategy.Object;
+
+ public ObjectConverter()
+ {
+ CanBePolymorphic = true;
+ // JsonElement/JsonNode parsing does not support async; force read ahead for now.
+ RequiresReadAhead = true;
+ }
+
public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (options.UnknownTypeHandling == JsonUnknownTypeHandling.JsonElement)
return JsonElement.ParseValue(ref reader);
}
+ Debug.Assert(options.UnknownTypeHandling == JsonUnknownTypeHandling.JsonNode);
return JsonNodeConverter.Instance.Read(ref reader, typeToConvert, options);
}
public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerOptions options)
{
Debug.Assert(value?.GetType() == typeof(object));
-
writer.WriteStartObject();
writer.WriteEndObject();
}
+ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out object? value)
+ {
+ 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))
+ {
+ value = referenceValue;
+ }
+ else
+ {
+ value = element;
+ }
+
+ return true;
+ }
+
+ Debug.Assert(options.UnknownTypeHandling == JsonUnknownTypeHandling.JsonNode);
+ value = JsonNodeConverter.Instance.Read(ref reader, typeToConvert, options)!;
+ // TODO reference lookup for JsonNode deserialization.
+ return true;
+ }
+
internal override object ReadAsPropertyNameCore(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
ThrowHelper.ThrowNotSupportedException_DictionaryKeyTypeNotSupported(TypeToConvert, this);
writer.WriteStartObject();
if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve)
{
- if (JsonSerializer.WriteReferenceForObject(this, obj, ref state, writer) == MetadataPropertyName.Ref)
- {
- return true;
- }
+ MetadataPropertyName propertyName = JsonSerializer.WriteReferenceForObject(this, ref state, writer);
+ Debug.Assert(propertyName != MetadataPropertyName.Ref);
}
if (obj is IJsonOnSerializing onSerializing)
writer.WriteStartObject();
if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve)
{
- if (JsonSerializer.WriteReferenceForObject(this, obj, ref state, writer) == MetadataPropertyName.Ref)
- {
- return true;
- }
+ MetadataPropertyName propertyName = JsonSerializer.WriteReferenceForObject(this, ref state, writer);
+ Debug.Assert(propertyName != MetadataPropertyName.Ref);
}
if (obj is IJsonOnSerializing onSerializing)
if (!jsonPropertyInfo.GetMemberAndWriteJson(obj!, ref state, writer))
{
- Debug.Assert(jsonPropertyInfo.ConverterBase.ConverterStrategy != ConverterStrategy.Value ||
- jsonPropertyInfo.ConverterBase.TypeToConvert == JsonTypeInfo.ObjectType);
-
+ Debug.Assert(jsonPropertyInfo.ConverterBase.ConverterStrategy != ConverterStrategy.Value);
return false;
}
if (!state.Current.UseExtensionProperty)
{
- if (!SingleValueReadWithReadAhead(jsonPropertyInfo.ConverterBase.ConverterStrategy, ref reader, ref state))
+ if (!SingleValueReadWithReadAhead(jsonPropertyInfo.ConverterBase.RequiresReadAhead, ref reader, ref state))
{
return false;
}
else
{
// The actual converter is JsonElement, so force a read-ahead.
- if (!SingleValueReadWithReadAhead(ConverterStrategy.Value, ref reader, ref state))
+ if (!SingleValueReadWithReadAhead(requiresReadAhead: true, ref reader, ref state))
{
return false;
}
// Returning false below will cause the read-ahead functionality to finish the read.
state.Current.PropertyState = StackFramePropertyState.ReadValue;
- if (!SingleValueReadWithReadAhead(jsonParameterInfo.ConverterBase.ConverterStrategy, ref reader, ref state))
+ if (!SingleValueReadWithReadAhead(jsonParameterInfo.ConverterBase.RequiresReadAhead, ref reader, ref state))
{
return false;
}
public NullableConverter(JsonConverter<T> elementConverter)
{
_elementConverter = elementConverter;
- ConverterStrategy = elementConverter.ConverterStrategy;
IsInternalConverterForNumberType = elementConverter.IsInternalConverterForNumberType;
- // temporary workaround for JsonConverter base constructor needing to access
- // ConverterStrategy when calculating `CanUseDirectReadOrWrite`.
- CanUseDirectReadOrWrite = elementConverter.ConverterStrategy == ConverterStrategy.Value;
+
+ // Workaround for the base constructor depending on the (still unset) ConverterStrategy
+ // to derive the CanUseDirectReadOrWrite and RequiresReadAhead values.
+ ConverterStrategy = elementConverter.ConverterStrategy;
+ CanUseDirectReadOrWrite = elementConverter.CanUseDirectReadOrWrite;
+ RequiresReadAhead = elementConverter.RequiresReadAhead;
}
internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out T? value)
// AggressiveInlining used since this method is on a hot path and short. The optionally called
// method DoSingleValueReadWithReadAhead is not inlined.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal static bool SingleValueReadWithReadAhead(ConverterStrategy converterStrategy, ref Utf8JsonReader reader, ref ReadStack state)
+ internal static bool SingleValueReadWithReadAhead(bool requiresReadAhead, ref Utf8JsonReader reader, ref ReadStack state)
{
- bool readAhead = state.ReadAhead && converterStrategy == ConverterStrategy.Value;
+ bool readAhead = requiresReadAhead && state.ReadAhead;
if (!readAhead)
{
return reader.Read();
/// </summary>
internal virtual bool CanHaveIdMetadata => false;
+ /// <summary>
+ /// The converter supports polymorphic writes; only reserved for System.Object types.
+ /// </summary>
internal bool CanBePolymorphic { get; set; }
/// <summary>
+ /// The serializer must read ahead all contents of the next JSON value
+ /// before calling into the converter for deserialization.
+ /// </summary>
+ internal bool RequiresReadAhead { get; set; }
+
+ /// <summary>
/// Used to support JsonObject as an extension property in a loosely-typed, trimmable manner.
/// </summary>
internal virtual object CreateObject(JsonSerializerOptions options)
{
if (!state.IsContinuation)
{
- if (!SingleValueReadWithReadAhead(ConverterStrategy, ref reader, ref state))
+ if (!SingleValueReadWithReadAhead(RequiresReadAhead, ref reader, ref state))
{
if (state.SupportContinuation)
{
{
// For a continuation, read ahead here to avoid having to build and then tear
// down the call stack if there is more than one buffer fetch necessary.
- if (!SingleValueReadWithReadAhead(ConverterStrategy.Value, ref reader, ref state))
+ if (!SingleValueReadWithReadAhead(requiresReadAhead: true, ref reader, ref state))
{
state.BytesConsumed += reader.BytesConsumed;
return default;
JsonSerializerOptions options,
ref WriteStack state)
{
- if (IsValueType)
+ if (
+#if NET5_0_OR_GREATER
+ // Short-circuit the check against "is not null"; treated as a constant by recent versions of the JIT.
+ typeof(T).IsValueType)
+#else
+ IsValueType)
+#endif
{
// Value types can never have a null except for Nullable<T>.
if (value == null && Nullable.GetUnderlyingType(TypeToConvert) == null)
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
-using System.Runtime.CompilerServices;
using System.Text.Json.Serialization.Converters;
using System.Text.Json.Serialization.Metadata;
/// </summary>
protected internal JsonConverter()
{
+ IsValueType = typeof(T).IsValueType;
IsInternalConverter = GetType().Assembly == typeof(JsonConverter).Assembly;
- // Today only the internal JsonConverter<object> can have polymorphic writes.
- CanBePolymorphic = IsInternalConverter && TypeToConvert == JsonTypeInfo.ObjectType;
- IsValueType = TypeToConvert.IsValueType;
- CanBeNull = default(T) is null;
if (HandleNull)
{
HandleNullOnRead = true;
HandleNullOnWrite = true;
}
+ else
+ {
+ // For the HandleNull == false case, either:
+ // 1) The default values are assigned in this type's virtual HandleNull property
+ // or
+ // 2) A converter overrode HandleNull and returned false so HandleNullOnRead and HandleNullOnWrite
+ // will be their default values of false.
+ }
- // For the HandleNull == false case, either:
- // 1) The default values are assigned in this type's virtual HandleNull property
- // or
- // 2) A converter overroad HandleNull and returned false so HandleNullOnRead and HandleNullOnWrite
- // will be their default values of false.
-
- CanUseDirectReadOrWrite = !CanBePolymorphic && IsInternalConverter && ConverterStrategy == ConverterStrategy.Value;
+ CanUseDirectReadOrWrite = ConverterStrategy == ConverterStrategy.Value && IsInternalConverter;
+ RequiresReadAhead = ConverterStrategy == ConverterStrategy.Value;
}
/// <summary>
// If the type doesn't support null, allow the converter a chance to modify.
// These semantics are backwards compatible with 3.0.
- HandleNullOnRead = !CanBeNull;
+ HandleNullOnRead = default(T) is not null;
// The framework handles null automatically on writes.
HandleNullOnWrite = false;
/// </summary>
internal bool HandleNullOnWrite { get; private set; }
- /// <summary>
- /// Can <see langword="null"/> be assigned to <see cref="TypeToConvert"/>?
- /// </summary>
- internal bool CanBeNull { get; }
-
// This non-generic API is sealed as it just forwards to the generic version.
internal sealed override bool TryWriteAsObject(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, ref WriteStack state)
{
internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out T? value)
{
- if (ConverterStrategy == ConverterStrategy.Value)
+ // For perf and converter simplicity, handle null here instead of forwarding to the converter.
+ if (reader.TokenType == JsonTokenType.Null && !HandleNullOnRead && !state.IsContinuation)
{
- // A value converter should never be within a continuation.
- Debug.Assert(!state.IsContinuation);
-
- // For perf and converter simplicity, handle null here instead of forwarding to the converter.
- if (reader.TokenType == JsonTokenType.Null && !HandleNullOnRead)
+ if (default(T) is not null)
{
- if (!CanBeNull)
- {
- ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert);
- }
-
- value = default;
- return true;
+ ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert);
}
+ value = default;
+ return true;
+ }
+
+ if (ConverterStrategy == ConverterStrategy.Value)
+ {
+ // A value converter should never be within a continuation.
+ Debug.Assert(!state.IsContinuation);
#if !DEBUG
// For performance, only perform validation on internal converters on debug builds.
if (IsInternalConverter)
ref reader);
}
- if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve &&
- CanBePolymorphic && value is JsonElement element)
- {
- // Edge case where we want to lookup for a reference when parsing into typeof(object)
- // instead of return `value` as a JsonElement.
- Debug.Assert(TypeToConvert == typeof(object));
-
- if (JsonSerializer.TryGetReferenceFromJsonElement(ref state, element, out object? referenceValue))
- {
- value = (T?)referenceValue;
- }
- }
-
return true;
}
+ Debug.Assert(IsInternalConverter);
+ bool isContinuation = state.IsContinuation;
bool success;
- // Remember if we were a continuation here since Push() may affect IsContinuation.
- bool wasContinuation = state.IsContinuation;
-
#if DEBUG
// DEBUG: ensure push/pop operations preserve stack integrity
JsonTypeInfo originalJsonTypeInfo = state.Current.JsonTypeInfo;
state.Push();
Debug.Assert(TypeToConvert == state.Current.JsonTypeInfo.Type);
-#if !DEBUG
+#if DEBUG
// For performance, only perform validation on internal converters on debug builds.
- if (IsInternalConverter)
+ if (!isContinuation)
{
- if (reader.TokenType == JsonTokenType.Null && !HandleNullOnRead && !wasContinuation)
- {
- if (!CanBeNull)
- {
- ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert);
- }
+ Debug.Assert(state.Current.OriginalTokenType == JsonTokenType.None);
+ state.Current.OriginalTokenType = reader.TokenType;
- // For perf and converter simplicity, handle null here instead of forwarding to the converter.
- value = default;
- success = true;
- }
- else
- {
- success = OnTryRead(ref reader, typeToConvert, options, ref state, out value);
- }
+ Debug.Assert(state.Current.OriginalDepth == 0);
+ state.Current.OriginalDepth = reader.CurrentDepth;
}
- else
#endif
+ success = OnTryRead(ref reader, typeToConvert, options, ref state, out value);
+#if DEBUG
+ if (success)
{
- if (!wasContinuation)
+ if (state.IsContinuation)
{
- // For perf and converter simplicity, handle null here instead of forwarding to the converter.
- if (reader.TokenType == JsonTokenType.Null && !HandleNullOnRead)
- {
- if (!CanBeNull)
- {
- ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert);
- }
-
- value = default;
- state.Pop(true);
-#if DEBUG
- Debug.Assert(ReferenceEquals(originalJsonTypeInfo, state.Current.JsonTypeInfo));
-#endif
- return true;
- }
-
- Debug.Assert(state.Current.OriginalTokenType == JsonTokenType.None);
- state.Current.OriginalTokenType = reader.TokenType;
-
- Debug.Assert(state.Current.OriginalDepth == 0);
- state.Current.OriginalDepth = reader.CurrentDepth;
+ // The resumable converter did not forward to the next converter that previously returned false.
+ ThrowHelper.ThrowJsonException_SerializationConverterRead(this);
}
- success = OnTryRead(ref reader, typeToConvert, options, ref state, out value);
- if (success)
- {
- if (state.IsContinuation)
- {
- // The resumable converter did not forward to the next converter that previously returned false.
- ThrowHelper.ThrowJsonException_SerializationConverterRead(this);
- }
-
- VerifyRead(
- state.Current.OriginalTokenType,
- state.Current.OriginalDepth,
- bytesConsumed: 0,
- isValueConverter: false,
- ref reader);
+ VerifyRead(
+ state.Current.OriginalTokenType,
+ state.Current.OriginalDepth,
+ bytesConsumed: 0,
+ isValueConverter: false,
+ ref reader);
- // No need to clear state.Current.* since a stack pop will occur.
- }
+ // No need to clear state.Current.* since a stack pop will occur.
}
+#endif
state.Pop(success);
#if DEBUG
/// <summary>
/// Performance optimization.
/// The 'in' modifier in 'TryWrite(in T Value)' causes boxing for Nullable{T}, so this helper avoids that.
- /// TODO: Remove this work-around once #50915 is addressed.
+ /// TODO: Remove this work-around once https://github.com/dotnet/runtime/issues/50915 is addressed.
/// </summary>
private static bool IsNull(T value) => value is null;
return true;
}
- bool ignoreCyclesPopReference = false;
+ if (ConverterStrategy == ConverterStrategy.Value)
+ {
+ Debug.Assert(!state.IsContinuation);
+
+ int originalPropertyDepth = writer.CurrentDepth;
+
+ if (state.Current.NumberHandling != null && IsInternalConverterForNumberType)
+ {
+ WriteNumberWithCustomHandling(writer, value, state.Current.NumberHandling.Value);
+ }
+ else
+ {
+ Write(writer, value, options);
+ }
+
+ VerifyWrite(originalPropertyDepth, writer);
+ return true;
+ }
+
+ Debug.Assert(IsInternalConverter);
+ bool isContinuation = state.IsContinuation;
+ bool success;
if (
#if NET5_0_OR_GREATER
#else
!IsValueType &&
#endif
- // Since we may have checked for a null value above we may have a redundant check here,
- // but this seems to be better than trying to cache that value when considering all permutations:
- // int?, int?(null value), int, object, object(null value)
- value is not null)
+ value is not null &&
+ // Do not handle objects that have already been
+ // handled by a polymorphic converter for a base type.
+ state.Current.PolymorphicSerializationState != PolymorphicSerializationState.PolymorphicReEntryStarted)
{
+ JsonConverter? polymorphicConverter = CanBePolymorphic ?
+ state.Current.ResolvePolymorphicConverter(value, TypeToConvert, options) :
+ null;
- if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.IgnoreCycles &&
- // .NET types that are serialized as JSON primitive values don't need to be tracked for cycle detection e.g: string.
- ConverterStrategy != ConverterStrategy.Value)
+ Debug.Assert(polymorphicConverter is null || state.CurrentDepth > 0,
+ "root-level polymorphic converters should not be handled here.");
+
+ if (!isContinuation)
{
- // Custom (user) converters shall not track references
- // it is responsibility of the user to break cycles in case there's any
- // if we compare against Preserve, objects don't get preserved when a custom converter exists
- // given that the custom converter executes prior to the preserve logic.
- Debug.Assert(IsInternalConverter);
+ switch (options.ReferenceHandlingStrategy)
+ {
+ case ReferenceHandlingStrategy.IgnoreCycles:
+ ReferenceResolver resolver = state.ReferenceResolver;
+ if (resolver.ContainsReferenceForCycleDetection(value))
+ {
+ writer.WriteNullValue();
+ return true;
+ }
- ReferenceResolver resolver = state.ReferenceResolver;
+ resolver.PushReferenceForCycleDetection(value);
+ // WriteStack reuses root-level stackframes for its children as a performance optimization;
+ // we want to avoid writing any data for the root-level object to avoid corrupting the stack.
+ // This is fine since popping the root object at the end of serialization is not essential.
+ state.Current.IsPushedReferenceForCycleDetection = state.CurrentDepth > 0;
+ break;
- // Write null to break reference cycles.
- if (resolver.ContainsReferenceForCycleDetection(value))
- {
- writer.WriteNullValue();
- return true;
- }
+ case ReferenceHandlingStrategy.Preserve:
+ bool canHaveIdMetata = polymorphicConverter?.CanHaveIdMetadata ?? CanHaveIdMetadata;
+ if (canHaveIdMetata && JsonSerializer.TryGetReferenceForValue(value, ref state, writer))
+ {
+ // We found a repeating reference and wrote the relevant metadata; serialization complete.
+ return true;
+ }
+ break;
- // For boxed reference types: do not push when boxed in order to avoid false positives
- // when we run the ContainsReferenceForCycleDetection check for the converter of the unboxed value.
- Debug.Assert(!CanBePolymorphic);
- resolver.PushReferenceForCycleDetection(value);
- ignoreCyclesPopReference = true;
+ default:
+ Debug.Assert(options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.None);
+ break;
+ }
}
- if (CanBePolymorphic)
+ if (polymorphicConverter is not null)
{
- Debug.Assert(IsInternalConverter);
+ Debug.Assert(!polymorphicConverter.CanBePolymorphic, "Only ObjectConverter supports polymorphism.");
- Type type = value.GetType();
+ state.Current.EnterPolymorphicConverter();
+ success = polymorphicConverter.TryWriteAsObject(writer, value, options, ref state);
+ state.Current.ExitPolymorphicConverter(success);
- if (type != TypeToConvert)
+ if (success)
{
- // For internal converter only: Handle polymorphic case and get the new converter.
- // Custom converter, even though polymorphic converter, get called for reading AND writing.
- JsonConverter jsonConverter = state.Current.InitializeReEntry(type, options);
- Debug.Assert(jsonConverter != this);
-
- // For boxed value types: invoke the reference handler
- // before the instance gets unboxed by the subtype converter.
- if (jsonConverter.IsValueType)
- {
- switch (options.ReferenceHandlingStrategy)
- {
- case ReferenceHandlingStrategy.Preserve when (jsonConverter.CanHaveIdMetadata && !state.IsContinuation):
- if (JsonSerializer.TryWriteReferenceForBoxedStruct(value, ref state, writer))
- {
- return true;
- }
- break;
-
- case ReferenceHandlingStrategy.IgnoreCycles:
- state.ReferenceResolver.PushReferenceForCycleDetection(value);
- ignoreCyclesPopReference = true;
- break;
- default:
- break;
- }
- }
-
- // We found a different converter; forward to that.
- bool success2 = jsonConverter.TryWriteAsObject(writer, value, options, ref state);
-
- if (ignoreCyclesPopReference)
+ if (state.Current.IsPushedReferenceForCycleDetection)
{
state.ReferenceResolver.PopReferenceForCycleDetection();
+ state.Current.IsPushedReferenceForCycleDetection = false;
}
-
- return success2;
}
- }
- }
-
- if (ConverterStrategy == ConverterStrategy.Value)
- {
- Debug.Assert(!state.IsContinuation);
-
- int originalPropertyDepth = writer.CurrentDepth;
-
- if (state.Current.NumberHandling != null && IsInternalConverterForNumberType)
- {
- WriteNumberWithCustomHandling(writer, value, state.Current.NumberHandling.Value);
- }
- else
- {
- Write(writer, value, options);
- }
-
- VerifyWrite(originalPropertyDepth, writer);
- if (
-#if NET5_0_OR_GREATER
- // Short-circuit the check against ignoreCyclesPopReference; treated as a constant by recent versions of the JIT.
- !typeof(T).IsValueType &&
-#endif
- ignoreCyclesPopReference)
- {
- // Should only be entered if we're serializing instances
- // of type object using the internal object converter.
- Debug.Assert(value?.GetType() == typeof(object) && IsInternalConverter);
- state.ReferenceResolver.PopReferenceForCycleDetection();
+ return success;
}
-
- return true;
}
- bool isContinuation = state.IsContinuation;
-
#if DEBUG
// DEBUG: ensure push/pop operations preserve stack integrity
JsonTypeInfo originalJsonTypeInfo = state.Current.JsonTypeInfo;
state.Push();
Debug.Assert(TypeToConvert == state.Current.JsonTypeInfo.Type);
+#if DEBUG
+ // For performance, only perform validation on internal converters on debug builds.
if (!isContinuation)
{
Debug.Assert(state.Current.OriginalDepth == 0);
state.Current.OriginalDepth = writer.CurrentDepth;
}
-
- bool success = OnTryWrite(writer, value, options, ref state);
+#endif
+ success = OnTryWrite(writer, value, options, ref state);
+#if DEBUG
if (success)
{
VerifyWrite(state.Current.OriginalDepth, writer);
- // No need to clear state.Current.OriginalDepth since a stack pop will occur.
}
-
- state.Pop(success);
-#if DEBUG
- Debug.Assert(ReferenceEquals(originalJsonTypeInfo, state.Current.JsonTypeInfo));
#endif
+ state.Pop(success);
- if (ignoreCyclesPopReference)
+ if (success && state.Current.IsPushedReferenceForCycleDetection)
{
state.ReferenceResolver.PopReferenceForCycleDetection();
+ state.Current.IsPushedReferenceForCycleDetection = false;
}
-
+#if DEBUG
+ Debug.Assert(ReferenceEquals(originalJsonTypeInfo, state.Current.JsonTypeInfo));
+#endif
return success;
}
internal void VerifyRead(JsonTokenType tokenType, int depth, long bytesConsumed, bool isValueConverter, ref Utf8JsonReader reader)
{
+ Debug.Assert(isValueConverter == (ConverterStrategy == ConverterStrategy.Value));
+
switch (tokenType)
{
case JsonTokenType.StartArray:
break;
default:
- // A non-value converter (object or collection) should always have Start and End tokens
- if (!isValueConverter)
+ if (isValueConverter)
{
- // with the exception of converters that support null value reads
- if (!HandleNullOnRead || tokenType != JsonTokenType.Null)
+ // A value converter should not make any reads.
+ if (reader.BytesConsumed != bytesConsumed)
{
ThrowHelper.ThrowJsonException_SerializationConverterRead(this);
}
}
- // A value converter should not make any reads.
- else if (reader.BytesConsumed != bytesConsumed)
+ else
{
- ThrowHelper.ThrowJsonException_SerializationConverterRead(this);
+ // A non-value converter (object or collection) should always have Start and End tokens
+ // unless it is polymorphic or supports null value reads.
+ if (!CanBePolymorphic && !(HandleNullOnRead && tokenType == JsonTokenType.Null))
+ {
+ ThrowHelper.ThrowJsonException_SerializationConverterRead(this);
+ }
}
// Should not be possible to change token type.
Debug.Assert(reader.TokenType == tokenType);
-
break;
}
}
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
internal static bool TryGetReferenceFromJsonElement(
ref ReadStack state,
JsonElement element,
- out object? referenceValue)
+ [NotNullWhen(true)] out object? referenceValue)
{
bool refMetadataFound = false;
referenceValue = default;
internal static MetadataPropertyName WriteReferenceForObject(
JsonConverter jsonConverter,
- object currentValue,
ref WriteStack state,
Utf8JsonWriter writer)
{
- MetadataPropertyName writtenMetadataName;
-
- if (state.BoxedStructReferenceId != null)
+ if (state.NewReferenceId != null)
{
- // We're serializing a struct that has been handled by a polymorphic converter;
- // emit the reference id that was recorded for the boxed instance.
-
- Debug.Assert(jsonConverter.IsValueType && jsonConverter.CanHaveIdMetadata);
- writer.WriteString(s_metadataId, state.BoxedStructReferenceId);
- writtenMetadataName = MetadataPropertyName.Id;
- state.BoxedStructReferenceId = null;
- }
- else if (!jsonConverter.CanHaveIdMetadata || jsonConverter.IsValueType)
- {
- // If the jsonConverter supports immutable dictionaries or value types, don't write any metadata
- writtenMetadataName = MetadataPropertyName.NoMetadata;
- }
- else
- {
- string referenceId = state.ReferenceResolver.GetReference(currentValue, out bool alreadyExists);
- Debug.Assert(referenceId != null);
-
- if (alreadyExists)
- {
- writer.WriteString(s_metadataRef, referenceId);
- writer.WriteEndObject();
- writtenMetadataName = MetadataPropertyName.Ref;
- }
- else
- {
- writer.WriteString(s_metadataId, referenceId);
- writtenMetadataName = MetadataPropertyName.Id;
- }
+ Debug.Assert(jsonConverter.CanHaveIdMetadata);
+ writer.WriteString(s_metadataId, state.NewReferenceId);
+ state.NewReferenceId = null;
+ return MetadataPropertyName.Id;
}
- return writtenMetadataName;
+ return MetadataPropertyName.NoMetadata;
}
internal static MetadataPropertyName WriteReferenceForCollection(
JsonConverter jsonConverter,
- object currentValue,
ref WriteStack state,
Utf8JsonWriter writer)
{
- MetadataPropertyName writtenMetadataName;
-
- if (state.BoxedStructReferenceId != null)
+ if (state.NewReferenceId != null)
{
- // We're serializing a struct that has been handled by a polymorphic converter;
- // emit the reference id that was recorded for the boxed instance.
-
- Debug.Assert(jsonConverter.IsValueType && jsonConverter.CanHaveIdMetadata);
-
+ Debug.Assert(jsonConverter.CanHaveIdMetadata);
writer.WriteStartObject();
- writer.WriteString(s_metadataId, state.BoxedStructReferenceId);
+ writer.WriteString(s_metadataId, state.NewReferenceId);
writer.WriteStartArray(s_metadataValues);
- writtenMetadataName = MetadataPropertyName.Id;
- state.BoxedStructReferenceId = null;
- }
- else if (!jsonConverter.CanHaveIdMetadata || jsonConverter.IsValueType)
- {
- // If the jsonConverter supports immutable enumerables or value type collections, don't write any metadata
- writer.WriteStartArray();
- writtenMetadataName = MetadataPropertyName.NoMetadata;
- }
- else
- {
- string referenceId = state.ReferenceResolver.GetReference(currentValue, out bool alreadyExists);
- Debug.Assert(referenceId != null);
-
- if (alreadyExists)
- {
- writer.WriteStartObject();
- writer.WriteString(s_metadataRef, referenceId);
- writer.WriteEndObject();
- writtenMetadataName = MetadataPropertyName.Ref;
- }
- else
- {
- writer.WriteStartObject();
- writer.WriteString(s_metadataId, referenceId);
- writer.WriteStartArray(s_metadataValues);
- writtenMetadataName = MetadataPropertyName.Id;
- }
+ state.NewReferenceId = null;
+ return MetadataPropertyName.Id;
}
- return writtenMetadataName;
+ // If the jsonConverter supports immutable enumerables or value type collections, don't write any metadata
+ writer.WriteStartArray();
+ return MetadataPropertyName.NoMetadata;
}
/// <summary>
- /// Used by polymorphic converters that are handling references for values that are boxed structs.
+ /// Compute reference id for the next value to be serialized.
/// </summary>
- internal static bool TryWriteReferenceForBoxedStruct(object currentValue, ref WriteStack state, Utf8JsonWriter writer)
+ internal static bool TryGetReferenceForValue(object currentValue, ref WriteStack state, Utf8JsonWriter writer)
{
- Debug.Assert(state.BoxedStructReferenceId == null);
- Debug.Assert(currentValue.GetType().IsValueType);
+ Debug.Assert(state.NewReferenceId == null);
string referenceId = state.ReferenceResolver.GetReference(currentValue, out bool alreadyExists);
Debug.Assert(referenceId != null);
if (alreadyExists)
{
+ // Instance already serialized, write as { "$ref" : "referenceId" }
writer.WriteStartObject();
writer.WriteString(s_metadataRef, referenceId);
writer.WriteEndObject();
}
else
{
- // Since we cannot run `ReferenceResolver.GetReference` twice for newly encountered instances,
- // need to store the reference id for use by the subtype converter we're dispatching to.
- state.BoxedStructReferenceId = referenceId;
+ // New instance, store computed reference id in the state
+ state.NewReferenceId = referenceId;
}
return alreadyExists;
#endif
Options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.IgnoreCycles &&
value is not null &&
+ !state.IsContinuation &&
// .NET types that are serialized as JSON primitive values don't need to be tracked for cycle detection e.g: string.
- // However JsonConverter<object> that uses ConverterStrategy == Value is an exception.
- (Converter.CanBePolymorphic || ConverterStrategy != ConverterStrategy.Value) &&
+ ConverterStrategy != ConverterStrategy.Value &&
state.ReferenceResolver.ContainsReferenceForCycleDetection(value))
{
// If a reference cycle is detected, treat value as null.
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Text.Json
+{
+ internal enum PolymorphicSerializationState : byte
+ {
+ None,
+
+ /// <summary>
+ /// Dispatch to a polymorphic converter has been initiated.
+ /// </summary>
+ PolymorphicReEntryStarted,
+
+ /// <summary>
+ /// Current frame is a continuation using a suspended polymorphic converter.
+ /// </summary>
+ PolymorphicReEntrySuspended
+ }
+}
{
if (_count == 0)
{
- // The first stack frame is held in Current.
+ // Performance optimization: reuse the first stackframe on the first push operation.
+ // NB need to be careful when making writes to Current _before_ the first `Push`
+ // operation is performed.
_count = 1;
}
else
// Stores the non-string dictionary keys for continuation.
public object? DictionaryKey;
+#if DEBUG
// Validation state.
public int OriginalDepth;
public JsonTokenType OriginalTokenType;
+#endif
// Current object (POCO or IEnumerable).
public object? ReturnValue; // The current return value used for re-entry.
[DebuggerDisplay("Path:{PropertyPath()} Current: ConverterStrategy.{ConverterStrategy.JsonTypeInfo.PropertyInfoForTypeInfo.ConverterStrategy}, {Current.JsonTypeInfo.Type.Name}")]
internal struct WriteStack
{
+ public int CurrentDepth => _count;
+
/// <summary>
/// Exposes the stackframe that is currently active.
/// </summary>
public bool SupportContinuation;
/// <summary>
- /// Stores a reference id that has been calculated by a polymorphic converter handling a newly encountered boxed struct.
+ /// Stores a reference id that has been calculated for a newly serialized object.
/// </summary>
- public string? BoxedStructReferenceId;
+ public string? NewReferenceId;
private void EnsurePushCapacity()
{
public JsonConverter Initialize(Type type, JsonSerializerOptions options, bool supportContinuation)
{
JsonTypeInfo jsonTypeInfo = options.GetOrAddJsonTypeInfoForRootType(type);
- Debug.Assert(options == jsonTypeInfo.Options);
return Initialize(jsonTypeInfo, supportContinuation);
}
{
if (_count == 0)
{
- // The first stack frame is held in Current.
+ // Performance optimization: reuse the first stackframe on the first push operation.
+ // NB need to be careful when making writes to Current _before_ the first `Push`
+ // operation is performed.
_count = 1;
}
else
{
- JsonTypeInfo jsonTypeInfo = Current.GetPolymorphicJsonPropertyInfo().JsonTypeInfo;
+ JsonTypeInfo jsonTypeInfo = Current.GetNestedJsonTypeInfo();
JsonNumberHandling? numberHandling = Current.NumberHandling;
EnsurePushCapacity();
// Preserve Reference
public MetadataPropertyName MetadataPropertyName;
- /// <summary>
- /// The run-time JsonPropertyInfo that contains the TypeInfo and ConverterBase for polymorphic scenarios.
- /// </summary>
- /// <remarks>
- /// For objects, it is the <see cref="JsonTypeInfo.PropertyInfoForTypeInfo"/> for the class and current property.
- /// For collections, it is the <see cref="JsonTypeInfo.PropertyInfoForTypeInfo"/> for the class and current element.
- /// </remarks>
- private JsonPropertyInfo? PolymorphicJsonPropertyInfo;
+ // Serialization state for the child value serialized by the current frame.
+ public PolymorphicSerializationState PolymorphicSerializationState;
+ // Holds the entered polymorphic type info and acts as an LRU cache for element/field serializations.
+ private JsonPropertyInfo? PolymorphicJsonTypeInfo;
// Whether to use custom number handling.
public JsonNumberHandling? NumberHandling;
+ public bool IsPushedReferenceForCycleDetection;
+
public void EndDictionaryElement()
{
PropertyState = StackFramePropertyState.None;
{
JsonPropertyInfo = null!;
JsonPropertyNameAsString = null;
- PolymorphicJsonPropertyInfo = null;
PropertyState = StackFramePropertyState.None;
}
/// <summary>
- /// Return the property that contains the correct polymorphic properties including
- /// the ConverterStrategy and ConverterBase.
+ /// Returns the JsonTypeInfo instance for the nested value we are trying to access.
/// </summary>
- public JsonPropertyInfo GetPolymorphicJsonPropertyInfo()
+ public JsonTypeInfo GetNestedJsonTypeInfo()
{
- return PolymorphicJsonPropertyInfo ?? JsonPropertyInfo!;
+ JsonPropertyInfo? propInfo =
+ PolymorphicSerializationState == PolymorphicSerializationState.PolymorphicReEntryStarted ?
+ PolymorphicJsonTypeInfo :
+ JsonPropertyInfo;
+
+ return propInfo!.JsonTypeInfo;
}
/// <summary>
/// Initializes the state for polymorphic cases and returns the appropriate converter.
/// </summary>
- public JsonConverter InitializeReEntry(Type type, JsonSerializerOptions options)
+ public JsonConverter? ResolvePolymorphicConverter(object value, Type typeToConvert, JsonSerializerOptions options)
{
- // For perf, avoid the dictionary lookup in GetOrAddClass() for every element of a collection
+ Debug.Assert(value != null);
+ Debug.Assert(PolymorphicSerializationState != PolymorphicSerializationState.PolymorphicReEntryStarted);
+
+ if (PolymorphicSerializationState == PolymorphicSerializationState.PolymorphicReEntrySuspended)
+ {
+ // Quickly retrieve the polymorphic converter in case of a re-entrant continuation
+ Debug.Assert(PolymorphicJsonTypeInfo != null && value.GetType() == PolymorphicJsonTypeInfo.PropertyType);
+ return PolymorphicJsonTypeInfo.ConverterBase;
+ }
+
+ Type runtimeType = value.GetType();
+ if (runtimeType == typeToConvert)
+ {
+ return null;
+ }
+
+ // For perf, avoid the dictionary lookup in GetOrAddJsonTypeInfo() for every element of a collection
// if the current element is the same type as the previous element.
- if (PolymorphicJsonPropertyInfo?.PropertyType != type)
+ if (PolymorphicJsonTypeInfo?.PropertyType != runtimeType)
{
- JsonTypeInfo typeInfo = options.GetOrAddJsonTypeInfo(type);
- PolymorphicJsonPropertyInfo = typeInfo.PropertyInfoForTypeInfo;
+ JsonTypeInfo typeInfo = options.GetOrAddJsonTypeInfo(runtimeType);
+ PolymorphicJsonTypeInfo = typeInfo.PropertyInfoForTypeInfo;
}
- return PolymorphicJsonPropertyInfo.ConverterBase;
+ return PolymorphicJsonTypeInfo.ConverterBase;
+ }
+
+ public void EnterPolymorphicConverter()
+ {
+ Debug.Assert(PolymorphicSerializationState != PolymorphicSerializationState.PolymorphicReEntryStarted);
+ PolymorphicSerializationState = PolymorphicSerializationState.PolymorphicReEntryStarted;
+ }
+
+ public void ExitPolymorphicConverter(bool success)
+ {
+ Debug.Assert(PolymorphicSerializationState == PolymorphicSerializationState.PolymorphicReEntryStarted);
+ PolymorphicSerializationState = success ? PolymorphicSerializationState.None : PolymorphicSerializationState.PolymorphicReEntrySuspended;
}
}
}