File: System\Text\Json\Serialization\JsonConverterOfT.cs
Web Access
Project: src\src\libraries\System.Text.Json\src\System.Text.Json.csproj (System.Text.Json)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization.Converters;
using System.Text.Json.Serialization.Metadata;
 
namespace System.Text.Json.Serialization
{
    /// <summary>
    /// Converts an object or value to or from JSON.
    /// </summary>
    /// <typeparam name="T">The <see cref="System.Type"/> to convert.</typeparam>
    public abstract partial class JsonConverter<T> : JsonConverter
    {
        /// <summary>
        /// When overridden, constructs a new <see cref="JsonConverter{T}"/> instance.
        /// </summary>
        protected internal JsonConverter()
        {
            IsValueType = typeof(T).IsValueType;
 
            if (HandleNull)
            {
                HandleNullOnRead = true;
                HandleNullOnWrite = true;
            }
            else if (UsesDefaultHandleNull)
            {
                // If the type doesn't support null, allow the converter a chance to modify.
                // These semantics are backwards compatible with 3.0.
                HandleNullOnRead = default(T) is not null;
 
                // The framework handles null automatically on writes.
                HandleNullOnWrite = false;
            }
        }
 
        /// <summary>
        /// Determines whether the type can be converted.
        /// </summary>
        /// <remarks>
        /// The default implementation is to return True when <paramref name="typeToConvert"/> equals typeof(T).
        /// </remarks>
        /// <param name="typeToConvert"></param>
        /// <returns>True if the type can be converted, False otherwise.</returns>
        public override bool CanConvert(Type typeToConvert)
        {
            return typeToConvert == typeof(T);
        }
 
        private protected override ConverterStrategy GetDefaultConverterStrategy() => ConverterStrategy.Value;
 
        internal sealed override JsonTypeInfo CreateJsonTypeInfo(JsonSerializerOptions options)
        {
            return new JsonTypeInfo<T>(this, options);
        }
 
        /// <summary>
        /// Indicates whether <see langword="null"/> should be passed to the converter on serialization,
        /// and whether <see cref="JsonTokenType.Null"/> should be passed on deserialization.
        /// </summary>
        /// <remarks>
        /// The default value is <see langword="true"/> for converters based on value types, and <see langword="false"/> for converters based on reference types.
        /// </remarks>
        public virtual bool HandleNull
        {
            get
            {
                UsesDefaultHandleNull = true;
                return false;
            }
        }
 
        // This non-generic API is sealed as it just forwards to the generic version.
        internal sealed override void WriteAsObject(Utf8JsonWriter writer, object? value, JsonSerializerOptions options)
        {
            T valueOfT = JsonSerializer.UnboxOnWrite<T>(value)!;
            Write(writer, valueOfT, options);
        }
 
        // This non-generic API is sealed as it just forwards to the generic version.
        internal sealed override bool OnTryWriteAsObject(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, ref WriteStack state)
        {
            T valueOfT = JsonSerializer.UnboxOnWrite<T>(value)!;
            return OnTryWrite(writer, valueOfT, options, ref state);
        }
 
        // This non-generic API is sealed as it just forwards to the generic version.
        internal sealed override void WriteAsPropertyNameAsObject(Utf8JsonWriter writer, object? value, JsonSerializerOptions options)
        {
            T valueOfT = JsonSerializer.UnboxOnWrite<T>(value)!;
            WriteAsPropertyName(writer, valueOfT, options);
        }
 
        internal sealed override void WriteAsPropertyNameCoreAsObject(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, bool isWritingExtensionDataProperty)
        {
            T valueOfT = JsonSerializer.UnboxOnWrite<T>(value)!;
            WriteAsPropertyNameCore(writer, valueOfT, options, isWritingExtensionDataProperty);
        }
 
        internal sealed override void WriteNumberWithCustomHandlingAsObject(Utf8JsonWriter writer, object? value, JsonNumberHandling handling)
        {
            T valueOfT = JsonSerializer.UnboxOnWrite<T>(value)!;
            WriteNumberWithCustomHandling(writer, valueOfT, handling);
        }
 
        // 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)
        {
            T valueOfT = JsonSerializer.UnboxOnWrite<T>(value)!;
            return TryWrite(writer, valueOfT, options, ref state);
        }
 
        // Provide a default implementation for value converters.
        internal virtual bool OnTryWrite(Utf8JsonWriter writer,
#nullable disable // T may or may not be nullable depending on the derived converter's HandleNull override.
            T value,
#nullable enable
            JsonSerializerOptions options,
            ref WriteStack state)
        {
            Write(writer, value, options);
            return true;
        }
 
        // Provide a default implementation for value converters.
        internal virtual bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, scoped ref ReadStack state, out T? value)
        {
            value = Read(ref reader, typeToConvert, options);
            return true;
        }
 
        /// <summary>
        /// Read and convert the JSON to T.
        /// </summary>
        /// <remarks>
        /// A converter may throw any Exception, but should throw <cref>JsonException</cref> when the JSON is invalid.
        /// </remarks>
        /// <param name="reader">The <see cref="Utf8JsonReader"/> to read from.</param>
        /// <param name="typeToConvert">The <see cref="System.Type"/> being converted.</param>
        /// <param name="options">The <see cref="JsonSerializerOptions"/> being used.</param>
        /// <returns>The value that was converted.</returns>
        /// <remarks>Note that the value of <seealso cref="HandleNull"/> determines if the converter handles null JSON tokens.</remarks>
        public abstract T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options);
 
        internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, scoped ref ReadStack state, out T? value, out bool isPopulatedValue)
        {
            // For perf and converter simplicity, handle null here instead of forwarding to the converter.
            if (reader.TokenType == JsonTokenType.Null && !HandleNullOnRead && !state.IsContinuation)
            {
                if (default(T) is not null)
                {
                    ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(Type);
                }
 
                value = default;
                isPopulatedValue = false;
                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)
                {
                    if (state.Current.NumberHandling != null && IsInternalConverterForNumberType)
                    {
                        value = ReadNumberWithCustomHandling(ref reader, state.Current.NumberHandling.Value, options);
                    }
                    else
                    {
                        value = Read(ref reader, typeToConvert, options);
                    }
                }
                else
#endif
                {
                    JsonTokenType originalPropertyTokenType = reader.TokenType;
                    int originalPropertyDepth = reader.CurrentDepth;
                    long originalPropertyBytesConsumed = reader.BytesConsumed;
 
                    if (state.Current.NumberHandling != null && IsInternalConverterForNumberType)
                    {
                        value = ReadNumberWithCustomHandling(ref reader, state.Current.NumberHandling.Value, options);
                    }
                    else
                    {
                        value = Read(ref reader, typeToConvert, options);
                    }
 
                    VerifyRead(
                        originalPropertyTokenType,
                        originalPropertyDepth,
                        originalPropertyBytesConsumed,
                        isValueConverter: true,
                        ref reader);
                }
 
                isPopulatedValue = false;
                return true;
            }
 
            Debug.Assert(IsInternalConverter);
            bool isContinuation = state.IsContinuation;
            bool success;
 
            if (
#if NET
                !typeof(T).IsValueType &&
#endif
                CanBePolymorphic)
            {
                // Special case object converters since they don't
                // require the expensive ReadStack.Push()/Pop() operations.
                Debug.Assert(this is ObjectConverter);
                success = OnTryRead(ref reader, typeToConvert, options, ref state, out value);
                Debug.Assert(success);
                isPopulatedValue = false;
                return true;
            }
 
            JsonPropertyInfo? propertyInfo = state.Current.JsonPropertyInfo;
            object? parentObj = state.Current.ReturnValue;
 
#if DEBUG
            // DEBUG: ensure push/pop operations preserve stack integrity
            JsonTypeInfo originalJsonTypeInfo = state.Current.JsonTypeInfo;
#endif
            state.Push();
            Debug.Assert(Type == state.Current.JsonTypeInfo.Type);
 
            if (!isContinuation)
            {
#if DEBUG
                // For performance, only perform token type validation of converters on debug builds.
                Debug.Assert(state.Current.OriginalTokenType == JsonTokenType.None);
                state.Current.OriginalTokenType = reader.TokenType;
#endif
                Debug.Assert(state.Current.OriginalDepth == 0);
                state.Current.OriginalDepth = reader.CurrentDepth;
            }
 
            if (parentObj != null && propertyInfo != null && !propertyInfo.IsForTypeInfo)
            {
                state.Current.HasParentObject = true;
            }
 
            success = OnTryRead(ref reader, typeToConvert, options, ref state, out value);
#if DEBUG
            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);
 
                // No need to clear state.Current.* since a stack pop will occur.
            }
#endif
 
            isPopulatedValue = state.Current.IsPopulating;
            state.Pop(success);
#if DEBUG
            Debug.Assert(ReferenceEquals(originalJsonTypeInfo, state.Current.JsonTypeInfo));
#endif
            return success;
        }
 
        internal sealed override bool OnTryReadAsObject(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, scoped ref ReadStack state, out object? value)
        {
            bool success = OnTryRead(ref reader, typeToConvert, options, ref state, out T? typedValue);
            value = typedValue;
            return success;
        }
 
        internal sealed override bool TryReadAsObject(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, scoped ref ReadStack state, out object? value)
        {
            bool success = TryRead(ref reader, typeToConvert, options, ref state, out T? typedValue, out _);
            value = typedValue;
            return success;
        }
 
        internal sealed override object? ReadAsObject(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            T? typedValue = Read(ref reader, typeToConvert, options);
            return typedValue;
        }
 
        internal sealed override object? ReadAsPropertyNameAsObject(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            T typedValue = ReadAsPropertyName(ref reader, typeToConvert, options);
            return typedValue;
        }
 
        internal sealed override object? ReadAsPropertyNameCoreAsObject(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            T typedValue = ReadAsPropertyNameCore(ref reader, typeToConvert, options);
            return typedValue;
        }
 
        internal sealed override object? ReadNumberWithCustomHandlingAsObject(ref Utf8JsonReader reader, JsonNumberHandling handling, JsonSerializerOptions options)
        {
            T typedValue = ReadNumberWithCustomHandling(ref reader, handling, options);
            return typedValue;
        }
 
        /// <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 https://github.com/dotnet/runtime/issues/50915 is addressed.
        /// </summary>
        private static bool IsNull(T? value) => value is null;
 
        internal bool TryWrite(Utf8JsonWriter writer, in T? value, JsonSerializerOptions options, ref WriteStack state)
        {
            if (writer.CurrentDepth >= options.EffectiveMaxDepth)
            {
                ThrowHelper.ThrowJsonException_SerializerCycleDetected(options.EffectiveMaxDepth);
            }
 
            if (default(T) is null && !HandleNullOnWrite && IsNull(value))
            {
                // We do not pass null values to converters unless HandleNullOnWrite is true. Null values for properties were
                // already handled in GetMemberAndWriteJson() so we don't need to check for IgnoreNullValues here.
                writer.WriteNullValue();
                return true;
            }
 
            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 NET
                // 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 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)
            {
                JsonTypeInfo jsonTypeInfo = state.PeekNestedJsonTypeInfo();
                Debug.Assert(jsonTypeInfo.Converter.Type == Type);
 
                bool canBePolymorphic = CanBePolymorphic || jsonTypeInfo.PolymorphicTypeResolver is not null;
                JsonConverter? polymorphicConverter = canBePolymorphic ?
                    ResolvePolymorphicConverter(value, jsonTypeInfo, options, ref state) :
                    null;
 
                if (!isContinuation && options.ReferenceHandlingStrategy != JsonKnownReferenceHandler.Unspecified &&
                    TryHandleSerializedObjectReference(writer, value, options, polymorphicConverter, ref state))
                {
                    // The reference handler wrote reference metadata, serialization complete.
                    return true;
                }
 
                if (polymorphicConverter is not null)
                {
                    success = polymorphicConverter.TryWriteAsObject(writer, value, options, ref state);
                    state.Current.ExitPolymorphicConverter(success);
 
                    if (success)
                    {
                        if (state.Current.IsPushedReferenceForCycleDetection)
                        {
                            state.ReferenceResolver.PopReferenceForCycleDetection();
                            state.Current.IsPushedReferenceForCycleDetection = false;
                        }
                    }
 
                    return success;
                }
            }
 
#if DEBUG
            // DEBUG: ensure push/pop operations preserve stack integrity
            JsonTypeInfo originalJsonTypeInfo = state.Current.JsonTypeInfo;
#endif
            state.Push();
            Debug.Assert(Type == 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;
            }
#endif
            success = OnTryWrite(writer, value, options, ref state);
#if DEBUG
            if (success)
            {
                VerifyWrite(state.Current.OriginalDepth, writer);
            }
#endif
            state.Pop(success);
 
            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 bool TryWriteDataExtensionProperty(Utf8JsonWriter writer, T value, JsonSerializerOptions options, ref WriteStack state)
        {
            Debug.Assert(value != null);
 
            if (!IsInternalConverter)
            {
                return TryWrite(writer, value, options, ref state);
            }
 
            JsonDictionaryConverter<T>? dictionaryConverter = this as JsonDictionaryConverter<T>
                ?? (this as JsonMetadataServicesConverter<T>)?.Converter as JsonDictionaryConverter<T>;
 
            if (dictionaryConverter == null)
            {
                // If not JsonDictionaryConverter<T> then we are JsonObject.
                // Avoid a type reference to JsonObject and its converter to support trimming.
                Debug.Assert(Type == typeof(Nodes.JsonObject));
                return TryWrite(writer, value, options, ref state);
            }
 
            if (writer.CurrentDepth >= options.EffectiveMaxDepth)
            {
                ThrowHelper.ThrowJsonException_SerializerCycleDetected(options.EffectiveMaxDepth);
            }
 
            bool isContinuation = state.IsContinuation;
            bool success;
 
            state.Push();
 
            if (!isContinuation)
            {
                Debug.Assert(state.Current.OriginalDepth == 0);
                state.Current.OriginalDepth = writer.CurrentDepth;
            }
 
            // Extension data properties change how dictionary key naming policies are applied.
            state.Current.IsWritingExtensionDataProperty = true;
            state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
 
            success = dictionaryConverter.OnWriteResume(writer, value, options, ref state);
            if (success)
            {
                VerifyWrite(state.Current.OriginalDepth, writer);
            }
 
            state.Pop(success);
 
            return success;
        }
 
        /// <inheritdoc/>
        public sealed override Type Type { get; } = typeof(T);
 
        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:
                    if (reader.TokenType != JsonTokenType.EndArray)
                    {
                        ThrowHelper.ThrowJsonException_SerializationConverterRead(this);
                    }
                    else if (depth != reader.CurrentDepth)
                    {
                        ThrowHelper.ThrowJsonException_SerializationConverterRead(this);
                    }
 
                    break;
 
                case JsonTokenType.StartObject:
                    if (reader.TokenType != JsonTokenType.EndObject)
                    {
                        ThrowHelper.ThrowJsonException_SerializationConverterRead(this);
                    }
                    else if (depth != reader.CurrentDepth)
                    {
                        ThrowHelper.ThrowJsonException_SerializationConverterRead(this);
                    }
 
                    break;
 
                case JsonTokenType.None:
                    Debug.Assert(IsRootLevelMultiContentStreamingConverter);
                    break;
 
                default:
                    if (isValueConverter)
                    {
                        // A value converter should not make any reads.
                        if (reader.BytesConsumed != bytesConsumed)
                        {
                            ThrowHelper.ThrowJsonException_SerializationConverterRead(this);
                        }
                    }
                    else
                    {
                        // 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;
            }
        }
 
        internal void VerifyWrite(int originalDepth, Utf8JsonWriter writer)
        {
            if (originalDepth != writer.CurrentDepth)
            {
                ThrowHelper.ThrowJsonException_SerializationConverterWrite(this);
            }
        }
 
        /// <summary>
        /// Write the value as JSON.
        /// </summary>
        /// <remarks>
        /// A converter may throw any Exception, but should throw <cref>JsonException</cref> when the JSON
        /// cannot be created.
        /// </remarks>
        /// <param name="writer">The <see cref="Utf8JsonWriter"/> to write to.</param>
        /// <param name="value">The value to convert. Note that the value of <seealso cref="HandleNull"/> determines if the converter handles <see langword="null" /> values.</param>
        /// <param name="options">The <see cref="JsonSerializerOptions"/> being used.</param>
        public abstract void Write(
            Utf8JsonWriter writer,
#nullable disable // T may or may not be nullable depending on the derived converter's HandleNull override.
            T value,
#nullable restore
            JsonSerializerOptions options);
 
        /// <summary>
        /// Reads a dictionary key from a JSON property name.
        /// </summary>
        /// <param name="reader">The <see cref="Utf8JsonReader"/> to read from.</param>
        /// <param name="typeToConvert">The <see cref="System.Type"/> being converted.</param>
        /// <param name="options">The <see cref="JsonSerializerOptions"/> being used.</param>
        /// <returns>The value that was converted.</returns>
        /// <remarks>Method should be overridden in custom converters of types used in deserialized dictionary keys.</remarks>
        public virtual T ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            // .NET 5 backward compatibility: hardcode the default converter for primitive key serialization.
            JsonConverter<T>? fallbackConverter = GetFallbackConverterForPropertyNameSerialization(options);
            if (fallbackConverter is null)
            {
                ThrowHelper.ThrowNotSupportedException_DictionaryKeyTypeNotSupported(Type, this);
            }
 
            return fallbackConverter.ReadAsPropertyNameCore(ref reader, typeToConvert, options);
        }
 
        internal virtual T ReadAsPropertyNameCore(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            Debug.Assert(reader.TokenType == JsonTokenType.PropertyName);
 
            long originalBytesConsumed = reader.BytesConsumed;
            T result = ReadAsPropertyName(ref reader, typeToConvert, options);
            if (reader.BytesConsumed != originalBytesConsumed)
            {
                ThrowHelper.ThrowJsonException_SerializationConverterRead(this);
            }
 
            return result;
        }
 
        /// <summary>
        /// Writes a dictionary key as a JSON property name.
        /// </summary>
        /// <param name="writer">The <see cref="Utf8JsonWriter"/> to write to.</param>
        /// <param name="value">The value to convert. Note that the value of <seealso cref="HandleNull"/> determines if the converter handles <see langword="null" /> values.</param>
        /// <param name="options">The <see cref="JsonSerializerOptions"/> being used.</param>
        /// <remarks>Method should be overridden in custom converters of types used in serialized dictionary keys.</remarks>
        public virtual void WriteAsPropertyName(Utf8JsonWriter writer, [DisallowNull] T value, JsonSerializerOptions options)
        {
            // .NET 5 backward compatibility: hardcode the default converter for primitive key serialization.
            JsonConverter<T>? fallbackConverter = GetFallbackConverterForPropertyNameSerialization(options);
            if (fallbackConverter is null)
            {
                ThrowHelper.ThrowNotSupportedException_DictionaryKeyTypeNotSupported(Type, this);
            }
 
            fallbackConverter.WriteAsPropertyNameCore(writer, value, options, isWritingExtensionDataProperty: false);
        }
 
        internal virtual void WriteAsPropertyNameCore(Utf8JsonWriter writer, [DisallowNull] T value, JsonSerializerOptions options, bool isWritingExtensionDataProperty)
        {
            if (value is null)
            {
                ThrowHelper.ThrowArgumentNullException(nameof(value));
            }
 
            if (isWritingExtensionDataProperty)
            {
                // Extension data is meant as mechanism to gather unused JSON properties;
                // do not apply any custom key conversions and hardcode the default behavior.
                Debug.Assert(!IsInternalConverter && Type == typeof(string));
                writer.WritePropertyName((string)(object)value!);
                return;
            }
 
            int originalDepth = writer.CurrentDepth;
            WriteAsPropertyName(writer, value, options);
            if (originalDepth != writer.CurrentDepth || writer.TokenType != JsonTokenType.PropertyName)
            {
                ThrowHelper.ThrowJsonException_SerializationConverterWrite(this);
            }
        }
 
        // .NET 5 backward compatibility: hardcode the default converter for primitive key serialization.
        private JsonConverter<T>? GetFallbackConverterForPropertyNameSerialization(JsonSerializerOptions options)
        {
            JsonConverter<T>? result = null;
 
            // For consistency do not return any default converters for options instances linked to a
            // JsonSerializerContext, even if the default converters might have been rooted.
            if (!IsInternalConverter && options.TypeInfoResolver is not JsonSerializerContext)
            {
                result = _fallbackConverterForPropertyNameSerialization;
 
                if (result is null && DefaultJsonTypeInfoResolver.TryGetDefaultSimpleConverter(Type, out JsonConverter? defaultConverter))
                {
                    Debug.Assert(defaultConverter != this);
                    _fallbackConverterForPropertyNameSerialization = result = (JsonConverter<T>)defaultConverter;
                }
            }
 
            return result;
        }
 
        private JsonConverter<T>? _fallbackConverterForPropertyNameSerialization;
 
        internal virtual T ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling, JsonSerializerOptions options)
            => throw new InvalidOperationException();
 
        internal virtual void WriteNumberWithCustomHandling(Utf8JsonWriter writer, T? value, JsonNumberHandling handling)
            => throw new InvalidOperationException();
    }
}