|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.IO.Pipelines;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Text.Json.Reflection;
using System.Text.Json.Serialization.Converters;
using System.Threading;
using System.Threading.Tasks;
namespace System.Text.Json.Serialization.Metadata
{
/// <summary>
/// Provides JSON serialization-related metadata about a type.
/// </summary>
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public abstract partial class JsonTypeInfo
{
internal const string MetadataFactoryRequiresUnreferencedCode = "JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.";
internal const string JsonObjectTypeName = "System.Text.Json.Nodes.JsonObject";
internal delegate T ParameterizedConstructorDelegate<T, TArg0, TArg1, TArg2, TArg3>(TArg0? arg0, TArg1? arg1, TArg2? arg2, TArg3? arg3);
/// <summary>
/// Negated bitmask of the required properties, indexed by <see cref="JsonPropertyInfo.PropertyIndex"/>.
/// </summary>
internal BitArray? OptionalPropertiesMask { get; private set; }
internal bool ShouldTrackRequiredProperties => OptionalPropertiesMask is not null;
private Action<object>? _onSerializing;
private Action<object>? _onSerialized;
private Action<object>? _onDeserializing;
private Action<object>? _onDeserialized;
internal JsonTypeInfo(Type type, JsonConverter converter, JsonSerializerOptions options)
{
Type = type;
Options = options;
Converter = converter;
Kind = GetTypeInfoKind(type, converter);
PropertyInfoForTypeInfo = CreatePropertyInfoForTypeInfo();
ElementType = converter.ElementType;
KeyType = converter.KeyType;
}
/// <summary>
/// Gets the element type corresponding to an enumerable, dictionary or optional type.
/// </summary>
/// <remarks>
/// Returns the element type for enumerable types, the value type for dictionary types,
/// and the underlying type for <see cref="Nullable{T}"/> or F# optional types.
///
/// Returns <see langword="null"/> for all other types or types using custom converters.
/// </remarks>
public Type? ElementType { get; }
/// <summary>
/// Gets the key type corresponding to a dictionary type.
/// </summary>
/// <remarks>
/// Returns the key type for dictionary types.
///
/// Returns <see langword="null"/> for all other types or types using custom converters.
/// </remarks>
public Type? KeyType { get; }
/// <summary>
/// Gets or sets a parameterless factory to be used on deserialization.
/// </summary>
/// <exception cref="InvalidOperationException">
/// The <see cref="JsonTypeInfo"/> instance has been locked for further modification.
///
/// -or-
///
/// A parameterless factory is not supported for the current metadata <see cref="Kind"/>.
/// </exception>
/// <remarks>
/// If set to <see langword="null" />, any attempt to deserialize instances of the given type will result in an exception.
///
/// For contracts originating from <see cref="DefaultJsonTypeInfoResolver"/> or <see cref="JsonSerializerContext"/>,
/// types with a single default constructor or default constructors annotated with <see cref="JsonConstructorAttribute"/>
/// will be mapped to this delegate.
/// </remarks>
public Func<object>? CreateObject
{
get => _createObject;
set
{
SetCreateObject(value);
}
}
private protected abstract void SetCreateObject(Delegate? createObject);
private protected Func<object>? _createObject;
internal Func<object>? CreateObjectForExtensionDataProperty { get; set; }
/// <summary>
/// Gets or sets a callback to be invoked before serialization occurs.
/// </summary>
/// <exception cref="InvalidOperationException">
/// The <see cref="JsonTypeInfo"/> instance has been locked for further modification.
///
/// -or-
///
/// Serialization callbacks are only supported for <see cref="JsonTypeInfoKind.Object"/> metadata.
/// </exception>
/// <remarks>
/// For contracts originating from <see cref="DefaultJsonTypeInfoResolver"/> or <see cref="JsonSerializerContext"/>,
/// the value of this callback will be mapped from any <see cref="IJsonOnSerializing"/> implementation on the type.
/// </remarks>
public Action<object>? OnSerializing
{
get => _onSerializing;
set
{
VerifyMutable();
if (Kind is not (JsonTypeInfoKind.Object or JsonTypeInfoKind.Enumerable or JsonTypeInfoKind.Dictionary))
{
ThrowHelper.ThrowInvalidOperationException_JsonTypeInfoOperationNotPossibleForKind(Kind);
}
_onSerializing = value;
}
}
/// <summary>
/// Gets or sets a callback to be invoked after serialization occurs.
/// </summary>
/// <exception cref="InvalidOperationException">
/// The <see cref="JsonTypeInfo"/> instance has been locked for further modification.
///
/// -or-
///
/// Serialization callbacks are only supported for <see cref="JsonTypeInfoKind.Object"/> metadata.
/// </exception>
/// <remarks>
/// For contracts originating from <see cref="DefaultJsonTypeInfoResolver"/> or <see cref="JsonSerializerContext"/>,
/// the value of this callback will be mapped from any <see cref="IJsonOnSerialized"/> implementation on the type.
/// </remarks>
public Action<object>? OnSerialized
{
get => _onSerialized;
set
{
VerifyMutable();
if (Kind is not (JsonTypeInfoKind.Object or JsonTypeInfoKind.Enumerable or JsonTypeInfoKind.Dictionary))
{
ThrowHelper.ThrowInvalidOperationException_JsonTypeInfoOperationNotPossibleForKind(Kind);
}
_onSerialized = value;
}
}
/// <summary>
/// Gets or sets a callback to be invoked before deserialization occurs.
/// </summary>
/// <exception cref="InvalidOperationException">
/// The <see cref="JsonTypeInfo"/> instance has been locked for further modification.
///
/// -or-
///
/// Serialization callbacks are only supported for <see cref="JsonTypeInfoKind.Object"/> metadata.
/// </exception>
/// <remarks>
/// For contracts originating from <see cref="DefaultJsonTypeInfoResolver"/> or <see cref="JsonSerializerContext"/>,
/// the value of this callback will be mapped from any <see cref="IJsonOnDeserializing"/> implementation on the type.
/// </remarks>
public Action<object>? OnDeserializing
{
get => _onDeserializing;
set
{
VerifyMutable();
if (Kind is not (JsonTypeInfoKind.Object or JsonTypeInfoKind.Enumerable or JsonTypeInfoKind.Dictionary))
{
ThrowHelper.ThrowInvalidOperationException_JsonTypeInfoOperationNotPossibleForKind(Kind);
}
if (Converter.IsConvertibleCollection)
{
// The values for convertible collections aren't available at the start of deserialization.
ThrowHelper.ThrowInvalidOperationException_JsonTypeInfoOnDeserializingCallbacksNotSupported(Type);
}
_onDeserializing = value;
}
}
/// <summary>
/// Gets or sets a callback to be invoked after deserialization occurs.
/// </summary>
/// <exception cref="InvalidOperationException">
/// The <see cref="JsonTypeInfo"/> instance has been locked for further modification.
///
/// -or-
///
/// Serialization callbacks are only supported for <see cref="JsonTypeInfoKind.Object"/> metadata.
/// </exception>
/// <remarks>
/// For contracts originating from <see cref="DefaultJsonTypeInfoResolver"/> or <see cref="JsonSerializerContext"/>,
/// the value of this callback will be mapped from any <see cref="IJsonOnDeserialized"/> implementation on the type.
/// </remarks>
public Action<object>? OnDeserialized
{
get => _onDeserialized;
set
{
VerifyMutable();
if (Kind is not (JsonTypeInfoKind.Object or JsonTypeInfoKind.Enumerable or JsonTypeInfoKind.Dictionary))
{
ThrowHelper.ThrowInvalidOperationException_JsonTypeInfoOperationNotPossibleForKind(Kind);
}
_onDeserialized = value;
}
}
/// <summary>
/// Gets the list of <see cref="JsonPropertyInfo"/> metadata corresponding to the current type.
/// </summary>
/// <remarks>
/// Property is only applicable to metadata of kind <see cref="JsonTypeInfoKind.Object"/>.
/// For other kinds an empty, read-only list will be returned.
///
/// The order of <see cref="JsonPropertyInfo"/> entries in the list determines the serialization order,
/// unless either of the entries specifies a non-zero <see cref="JsonPropertyInfo.Order"/> value,
/// in which case the properties will be stable sorted by <see cref="JsonPropertyInfo.Order"/>.
///
/// It is required that added <see cref="JsonPropertyInfo"/> entries are unique up to <see cref="JsonPropertyInfo.Name"/>,
/// however this will only be validated on serialization, once the metadata instance gets locked for further modification.
/// </remarks>
public IList<JsonPropertyInfo> Properties => PropertyList;
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
internal JsonPropertyInfoList PropertyList
{
get
{
return _properties ?? CreatePropertyList();
JsonPropertyInfoList CreatePropertyList()
{
var list = new JsonPropertyInfoList(this);
if (_sourceGenDelayedPropertyInitializer is { } propInit)
{
// .NET 6 source gen backward compatibility -- ensure that the
// property initializer delegate is invoked lazily.
JsonMetadataServices.PopulateProperties(this, list, propInit);
}
JsonPropertyInfoList? result = Interlocked.CompareExchange(ref _properties, list, null);
_sourceGenDelayedPropertyInitializer = null;
return result ?? list;
}
}
}
/// <summary>
/// Stores the .NET 6-style property initialization delegate for delayed evaluation.
/// </summary>
internal Func<JsonSerializerContext, JsonPropertyInfo[]>? SourceGenDelayedPropertyInitializer
{
get => _sourceGenDelayedPropertyInitializer;
set
{
Debug.Assert(!IsReadOnly);
Debug.Assert(_properties is null, "must not be set if a property list has been initialized.");
_sourceGenDelayedPropertyInitializer = value;
}
}
private Func<JsonSerializerContext, JsonPropertyInfo[]>? _sourceGenDelayedPropertyInitializer;
private JsonPropertyInfoList? _properties;
/// <summary>
/// Gets or sets a configuration object specifying polymorphism metadata.
/// </summary>
/// <exception cref="ArgumentException">
/// <paramref name="value" /> has been associated with a different <see cref="JsonTypeInfo"/> instance.
/// </exception>
/// <exception cref="InvalidOperationException">
/// The <see cref="JsonTypeInfo"/> instance has been locked for further modification.
///
/// -or-
///
/// Polymorphic serialization is not supported for the current metadata <see cref="Kind"/>.
/// </exception>
/// <remarks>
/// For contracts originating from <see cref="DefaultJsonTypeInfoResolver"/> or <see cref="JsonSerializerContext"/>,
/// the configuration of this setting will be mapped from any <see cref="JsonDerivedTypeAttribute"/> or <see cref="JsonPolymorphicAttribute"/> annotations.
/// </remarks>
public JsonPolymorphismOptions? PolymorphismOptions
{
get => _polymorphismOptions;
set
{
VerifyMutable();
if (value != null)
{
if (Kind == JsonTypeInfoKind.None)
{
ThrowHelper.ThrowInvalidOperationException_JsonTypeInfoOperationNotPossibleForKind(Kind);
}
if (value.DeclaringTypeInfo != null && value.DeclaringTypeInfo != this)
{
ThrowHelper.ThrowArgumentException_JsonPolymorphismOptionsAssociatedWithDifferentJsonTypeInfo(nameof(value));
}
value.DeclaringTypeInfo = this;
}
_polymorphismOptions = value;
}
}
internal void SetPolymorphismOptions(JsonPolymorphismOptions options)
{
Debug.Assert(!IsReadOnly);
Debug.Assert(options.DeclaringTypeInfo is null || options.DeclaringTypeInfo == this);
options.DeclaringTypeInfo = this;
_polymorphismOptions = options;
}
/// <summary>
/// Specifies whether the current instance has been locked for modification.
/// </summary>
/// <remarks>
/// A <see cref="JsonTypeInfo"/> instance can be locked either if
/// it has been passed to one of the <see cref="JsonSerializer"/> methods,
/// has been associated with a <see cref="JsonSerializerContext"/> instance,
/// or a user explicitly called the <see cref="MakeReadOnly"/> method on the instance.
/// </remarks>
public bool IsReadOnly { get; private set; }
/// <summary>
/// Locks the current instance for further modification.
/// </summary>
/// <remarks>This method is idempotent.</remarks>
public void MakeReadOnly() => IsReadOnly = true;
private protected JsonPolymorphismOptions? _polymorphismOptions;
/// <summary>
/// Gets the list of union case type metadata for this type.
/// </summary>
/// <remarks>
/// <para>
/// This property is only meaningful when <see cref="Kind"/> is <see cref="JsonTypeInfoKind.Union"/>.
/// The list is mutable during configuration and frozen at finalization time.
/// </para>
/// <para>
/// For types recognized as unions via <c>System.Runtime.CompilerServices.UnionAttribute</c>,
/// the list is automatically populated. For contract customization, users can
/// populate this list manually.
/// </para>
/// </remarks>
public IList<JsonUnionCaseInfo> UnionCases => UnionCaseList;
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
internal JsonUnionCaseInfoList UnionCaseList
{
get
{
return _unionCases ?? CreateUnionCaseList();
JsonUnionCaseInfoList CreateUnionCaseList()
{
var list = new JsonUnionCaseInfoList(this);
JsonUnionCaseInfoList? result = Interlocked.CompareExchange(ref _unionCases, list, null);
return result ?? list;
}
}
}
private JsonUnionCaseInfoList? _unionCases;
internal sealed class JsonUnionCaseInfoList : ConfigurationList<JsonUnionCaseInfo>
{
private readonly JsonTypeInfo _parent;
public JsonUnionCaseInfoList(JsonTypeInfo parent, IEnumerable<JsonUnionCaseInfo>? source = null) : base(source)
{
_parent = parent;
}
public override bool IsReadOnly => _parent.IsReadOnly;
protected override void OnCollectionModifying() => _parent.VerifyMutable();
}
/// <summary>
/// Gets or sets the delegate that classifies JSON payloads to determine the target type
/// during deserialization.
/// </summary>
/// <remarks>
/// <para>
/// This is the shared classification property for both polymorphic types and union types.
/// </para>
/// <para>
/// For <strong>polymorphic types</strong>: when set, bypasses the standard discriminator-based
/// type resolution (scanning for a <c>$type</c> property). When <see langword="null"/>
/// (default), the existing discriminator-based approach is used unchanged.
/// </para>
/// <para>
/// For <strong>union types</strong>: determines which case type matches the JSON payload.
/// When set by the user, replaces the default token-based matching.
/// </para>
/// </remarks>
public JsonTypeClassifier? TypeClassifier
{
get
{
if (_typeClassifierResolutionPending)
{
ConfigureTypeClassifier();
}
return _typeClassifier;
}
set
{
VerifyMutable();
// Explicit user assignment wins; discard any pending factory.
_typeClassifier = value;
_typeClassifierFactory = null;
_typeClassifierResolutionPending = false;
}
}
private JsonTypeClassifier? _typeClassifier;
/// <summary>
/// Gets or sets the weakly-typed delegate that deconstructs a union instance into
/// its case type and case value.
/// </summary>
/// <remarks>
/// <para>
/// The delegate combines object classification and value extraction in a single call,
/// returning a tuple of <c>(Type? CaseType, object? CaseValue)</c>.
/// </para>
/// <para>
/// <c>CaseType</c> doubles as the discriminator for the union's null state:
/// </para>
/// <list type="bullet">
/// <item>
/// <description>
/// A non-<see langword="null"/> <c>CaseType</c> instructs the converter to serialize
/// <c>CaseValue</c> using the <see cref="JsonTypeInfo"/> registered for that case
/// type. <c>CaseValue</c> may be <see langword="null"/>, in which case the JSON
/// output is <c>null</c> rendered through that case type's contract (this preserves
/// per-case null annotations). The <c>CaseType</c> must be one of the union's
/// declared case types; returning a runtime type that is more derived than any
/// declared case violates STJ contract invariants.
/// </description>
/// </item>
/// <item>
/// <description>
/// A <see langword="null"/> <c>CaseType</c> signals that the instance is the
/// canonical null union value. The converter writes JSON <c>null</c> directly,
/// bypassing all per-case contracts; <c>CaseValue</c> is ignored.
/// </description>
/// </item>
/// </list>
/// <para>
/// Prefer setting the strongly-typed <see cref="JsonTypeInfo{T}.UnionDeconstructor"/>
/// on <see cref="JsonTypeInfo{T}"/> to avoid boxing when the union type is a value type.
/// </para>
/// </remarks>
public Func<object, (Type? CaseType, object? CaseValue)>? UnionDeconstructor
{
get => _unionDeconstructor;
set
{
VerifyMutable();
SetUnionDeconstructor(value);
}
}
private protected virtual void SetUnionDeconstructor(Delegate? deconstructor)
{
Debug.Assert(deconstructor is null or Func<object, (Type?, object?)>);
_unionDeconstructor = (Func<object, (Type?, object?)>?)deconstructor;
}
private protected Func<object, (Type?, object?)>? _unionDeconstructor;
/// <summary>
/// Gets or sets the weakly-typed delegate that constructs a union instance from
/// a case type and case value.
/// </summary>
/// <remarks>
/// <para>
/// The delegate takes a <see cref="Type"/> parameter naming the resolved case type
/// (as produced by classification) and an <c>object?</c> case value, returning the
/// constructed union instance. The <c>caseType</c> parameter is always non-<see langword="null"/>
/// and disambiguates among overlapping case types (e.g., <c>Labrador : Dog</c>).
/// </para>
/// <para>
/// The delegate also encapsulates the union's null-handling policy. When the
/// converter encounters a JSON <see cref="JsonTokenType.Null"/> token, it bypasses
/// any configured <see cref="TypeClassifier"/> and invokes the delegate with a
/// <see langword="null"/> case value. Implementations should produce the canonical
/// null-holding union instance when at least one case is nullable, and otherwise
/// throw a <see cref="JsonException"/>. On the null path the <c>caseType</c> argument
/// is set to one of the declared nullable case types but its specific value should
/// be ignored; per union semantics, all nullable cases collapse to the same null
/// instance.
/// </para>
/// <para>
/// Prefer setting the strongly-typed <see cref="JsonTypeInfo{T}.UnionConstructor"/>
/// on <see cref="JsonTypeInfo{T}"/> to avoid boxing when the union type is a value type.
/// </para>
/// </remarks>
public Func<Type, object?, object>? UnionConstructor
{
get => _unionConstructor;
set
{
VerifyMutable();
SetUnionConstructor(value);
}
}
private protected virtual void SetUnionConstructor(Delegate? constructor)
{
Debug.Assert(constructor is null or Func<Type, object?, object>);
_unionConstructor = (Func<Type, object?, object>?)constructor;
}
private protected Func<Type, object?, object>? _unionConstructor;
/// <summary>
/// Pre-built JSON value shape to type map for default union deserialization (no custom classifier).
/// Maps each <see cref="JsonValueType"/> to the first-declared case type that
/// serializes as that value shape. Populated at configuration time from <see cref="UnionCases"/>.
/// </summary>
internal Dictionary<JsonValueType, Type>? UnionValueTypeMap { get; set; }
/// <summary>
/// Bitmask of <see cref="JsonValueType"/> shapes that two or more declared case types both
/// serialize as. Populated alongside <see cref="UnionValueTypeMap"/>; used at deserialize
/// time to surface a precise "ambiguous case" error when one of these value shapes appears.
/// <see cref="JsonValueType.None"/> means no ambiguous shapes were detected.
/// </summary>
internal JsonValueType UnionAmbiguousValueTypes { get; set; }
/// <summary>
/// First nullable union case type, if any. Populated at configuration time from <see cref="UnionCases"/>.
/// </summary>
internal Type? UnionNullableCaseType { get; set; }
/// <summary>
/// <see langword="true"/> when at least one declared union case carries a user-defined
/// <see cref="JsonConverter"/>. A custom converter can serialize as any JSON value type,
/// so deserialization without a custom classifier is unsafe and must fail with a clear
/// message.
/// </summary>
internal bool UnionHasCustomConverterCase { get; set; }
/// <summary>
/// Optional per-type factory captured during metadata creation (resolver phase)
/// from <see cref="JsonUnionAttribute.TypeClassifier"/> or <see cref="JsonPolymorphicAttribute.TypeClassifier"/>.
/// </summary>
internal JsonTypeClassifierFactory? TypeClassifierFactory
{
get => _typeClassifierFactory;
set
{
Debug.Assert(!IsReadOnly);
_typeClassifierFactory = value;
}
}
/// <summary>
/// Indicates whether type classifier resolution should run lazily from the
/// <see cref="TypeClassifier"/> getter or during metadata configuration.
/// </summary>
internal bool TypeClassifierResolutionPending
{
get => _typeClassifierResolutionPending;
set
{
Debug.Assert(!IsReadOnly);
_typeClassifierResolutionPending = value;
}
}
private JsonTypeClassifierFactory? _typeClassifierFactory;
private volatile bool _typeClassifierResolutionPending;
internal object? CreateObjectWithArgs { get; set; }
// Add method delegate for non-generic Stack and Queue; and types that derive from them.
internal object? AddMethodDelegate { get; set; }
internal JsonPropertyInfo? ExtensionDataProperty { get; private set; }
internal PolymorphicTypeResolver? PolymorphicTypeResolver { get; private set; }
// Indicates that SerializeHandler is populated.
internal bool HasSerializeHandler { get; private protected set; }
// Indicates that SerializeHandler is populated and is compatible with the associated contract metadata.
internal bool CanUseSerializeHandler { get; private set; }
// Configure would normally have thrown why initializing properties for source gen but type had SerializeHandler
// so it is allowed to be used for fast-path serialization but it will throw if used for metadata-based serialization
internal bool PropertyMetadataSerializationNotSupported { get; set; }
internal bool IsNullable => Converter.NullableElementConverter is not null;
internal bool CanBeNull => PropertyInfoForTypeInfo.PropertyTypeCanBeNull;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void ValidateCanBeUsedForPropertyMetadataSerialization()
{
if (PropertyMetadataSerializationNotSupported)
{
ThrowHelper.ThrowInvalidOperationException_NoMetadataForTypeProperties(Options.TypeInfoResolver, Type);
}
}
/// <summary>
/// Return the JsonTypeInfo for the element type, or null if the type is not an enumerable or dictionary.
/// </summary>
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
internal JsonTypeInfo? ElementTypeInfo
{
get
{
Debug.Assert(IsConfigured);
Debug.Assert(_elementTypeInfo is null or { IsConfigurationStarted: true });
// Even though this instance has already been configured,
// it is possible for contending threads to call the property
// while the wider JsonTypeInfo graph is still being configured.
// Call EnsureConfigured() to force synchronization if necessary.
JsonTypeInfo? elementTypeInfo = _elementTypeInfo;
elementTypeInfo?.EnsureConfigured();
return elementTypeInfo;
}
set
{
Debug.Assert(!IsReadOnly);
Debug.Assert(value is null || value.Type == ElementType);
_elementTypeInfo = value;
}
}
/// <summary>
/// Return the JsonTypeInfo for the key type, or null if the type is not a dictionary.
/// </summary>
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
internal JsonTypeInfo? KeyTypeInfo
{
get
{
Debug.Assert(IsConfigured);
Debug.Assert(_keyTypeInfo is null or { IsConfigurationStarted: true });
// Even though this instance has already been configured,
// it is possible for contending threads to call the property
// while the wider JsonTypeInfo graph is still being configured.
// Call EnsureConfigured() to force synchronization if necessary.
JsonTypeInfo? keyTypeInfo = _keyTypeInfo;
keyTypeInfo?.EnsureConfigured();
return keyTypeInfo;
}
set
{
Debug.Assert(!IsReadOnly);
Debug.Assert(value is null || value.Type == KeyType);
_keyTypeInfo = value;
}
}
private JsonTypeInfo? _elementTypeInfo;
private JsonTypeInfo? _keyTypeInfo;
/// <summary>
/// Gets the <see cref="JsonSerializerOptions"/> value associated with the current <see cref="JsonTypeInfo" /> instance.
/// </summary>
public JsonSerializerOptions Options { get; }
/// <summary>
/// Gets the <see cref="Type"/> for which the JSON serialization contract is being defined.
/// </summary>
public Type Type { get; }
/// <summary>
/// Gets the <see cref="JsonConverter"/> associated with the current type.
/// </summary>
/// <remarks>
/// The <see cref="JsonConverter"/> associated with the type determines the value of <see cref="Kind"/>,
/// and by extension the types of metadata that are configurable in the current JSON contract.
/// As such, the value of the converter cannot be changed once a <see cref="JsonTypeInfo"/> instance has been created.
/// </remarks>
public JsonConverter Converter { get; }
/// <summary>
/// Determines the kind of contract metadata that the current instance is specifying.
/// </summary>
/// <remarks>
/// The value of <see cref="Kind"/> determines what aspects of the JSON contract are configurable.
/// For example, it is only possible to configure the <see cref="Properties"/> list for metadata
/// of kind <see cref="JsonTypeInfoKind.Object"/>.
///
/// The value of <see cref="Kind"/> is determined exclusively by the <see cref="JsonConverter"/>
/// resolved for the current type, and cannot be changed once resolution has happened.
/// User-defined custom converters (specified either via <see cref="JsonConverterAttribute"/> or <see cref="JsonSerializerOptions.Converters"/>)
/// are metadata-agnostic and thus always resolve to <see cref="JsonTypeInfoKind.None"/>.
/// </remarks>
public JsonTypeInfoKind Kind { get; }
/// <summary>
/// Dummy <see cref="JsonPropertyInfo"/> instance corresponding to the declaring type of this <see cref="JsonTypeInfo"/>.
/// </summary>
/// <remarks>
/// Used as convenience in cases where we want to serialize property-like values that do not define property metadata, such as:
/// 1. a collection element type,
/// 2. a dictionary key or value type or,
/// 3. the property metadata for the root-level value.
/// For example, for a property returning <see cref="List{T}"/> where T is a string,
/// a JsonTypeInfo will be created with .Type=typeof(string) and .PropertyInfoForTypeInfo=JsonPropertyInfo{string}.
/// </remarks>
internal JsonPropertyInfo PropertyInfoForTypeInfo { get; }
private protected abstract JsonPropertyInfo CreatePropertyInfoForTypeInfo();
/// <summary>
/// Gets or sets the type-level <see cref="JsonSerializerOptions.NumberHandling"/> override.
/// </summary>
/// <exception cref="InvalidOperationException">
/// The <see cref="JsonTypeInfo"/> instance has been locked for further modification.
/// </exception>
/// <exception cref="ArgumentOutOfRangeException">
/// Specified an invalid <see cref="JsonNumberHandling"/> value.
/// </exception>
/// <remarks>
/// For contracts originating from <see cref="DefaultJsonTypeInfoResolver"/> or <see cref="JsonSerializerContext"/>,
/// the value of this callback will be mapped from any <see cref="JsonNumberHandlingAttribute"/> annotations.
/// </remarks>
public JsonNumberHandling? NumberHandling
{
get => _numberHandling;
set
{
VerifyMutable();
if (value is not null && !JsonSerializer.IsValidNumberHandlingValue(value.Value))
{
throw new ArgumentOutOfRangeException(nameof(value));
}
_numberHandling = value;
}
}
internal JsonNumberHandling EffectiveNumberHandling => _numberHandling ?? Options.NumberHandling;
private JsonNumberHandling? _numberHandling;
/// <summary>
/// Gets or sets the type-level <see cref="JsonUnmappedMemberHandling"/> override.
/// </summary>
/// <exception cref="InvalidOperationException">
/// The <see cref="JsonTypeInfo"/> instance has been locked for further modification.
///
/// -or-
///
/// Unmapped member handling only supported for <see cref="JsonTypeInfoKind.Object"/>.
/// </exception>
/// <exception cref="ArgumentOutOfRangeException">
/// Specified an invalid <see cref="JsonUnmappedMemberHandling"/> value.
/// </exception>
/// <remarks>
/// For contracts originating from <see cref="DefaultJsonTypeInfoResolver"/> or <see cref="JsonSerializerContext"/>,
/// the value of this callback will be mapped from any <see cref="JsonUnmappedMemberHandlingAttribute"/> annotations.
/// </remarks>
public JsonUnmappedMemberHandling? UnmappedMemberHandling
{
get => _unmappedMemberHandling;
set
{
VerifyMutable();
if (Kind != JsonTypeInfoKind.Object)
{
ThrowHelper.ThrowInvalidOperationException_JsonTypeInfoOperationNotPossibleForKind(Kind);
}
if (value is not null && !JsonSerializer.IsValidUnmappedMemberHandlingValue(value.Value))
{
throw new ArgumentOutOfRangeException(nameof(value));
}
_unmappedMemberHandling = value;
}
}
private JsonUnmappedMemberHandling? _unmappedMemberHandling;
internal JsonUnmappedMemberHandling EffectiveUnmappedMemberHandling { get; private set; }
private JsonObjectCreationHandling? _preferredPropertyObjectCreationHandling;
/// <summary>
/// Gets or sets the preferred <see cref="JsonObjectCreationHandling"/> value for properties contained in the type.
/// </summary>
/// <exception cref="InvalidOperationException">
/// The <see cref="JsonTypeInfo"/> instance has been locked for further modification.
///
/// -or-
///
/// Unmapped member handling only supported for <see cref="JsonTypeInfoKind.Object"/>.
/// </exception>
/// <exception cref="ArgumentOutOfRangeException">
/// Specified an invalid <see cref="JsonObjectCreationHandling"/> value.
/// </exception>
/// <remarks>
/// For contracts originating from <see cref="DefaultJsonTypeInfoResolver"/> or <see cref="JsonSerializerContext"/>,
/// the value of this callback will be mapped from <see cref="JsonObjectCreationHandlingAttribute"/> annotations on types.
/// </remarks>
public JsonObjectCreationHandling? PreferredPropertyObjectCreationHandling
{
get => _preferredPropertyObjectCreationHandling;
set
{
VerifyMutable();
if (Kind != JsonTypeInfoKind.Object)
{
ThrowHelper.ThrowInvalidOperationException_JsonTypeInfoOperationNotPossibleForKind(Kind);
}
if (value is not null && !JsonSerializer.IsValidCreationHandlingValue(value.Value))
{
throw new ArgumentOutOfRangeException(nameof(value));
}
_preferredPropertyObjectCreationHandling = value;
}
}
/// <summary>
/// Gets or sets the <see cref="IJsonTypeInfoResolver"/> from which this metadata instance originated.
/// </summary>
/// <exception cref="InvalidOperationException">
/// The <see cref="JsonTypeInfo"/> instance has been locked for further modification.
/// </exception>
/// <remarks>
/// Metadata used to determine the <see cref="JsonSerializerContext.GeneratedSerializerOptions"/>
/// configuration for the current metadata instance.
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Never)]
public IJsonTypeInfoResolver? OriginatingResolver
{
get => _originatingResolver;
set
{
VerifyMutable();
if (value is JsonSerializerContext)
{
// The source generator uses this property setter to brand the metadata instance as user-unmodified.
// Even though users could call the same property setter to unset this flag, this is generally speaking fine.
// This flag is only used to determine fast-path invalidation, worst case scenario this would lead to a false negative.
IsCustomized = false;
}
_originatingResolver = value;
}
}
private IJsonTypeInfoResolver? _originatingResolver;
/// <summary>
/// Gets or sets an attribute provider corresponding to the deserialization constructor.
/// </summary>
/// <exception cref="InvalidOperationException">
/// The <see cref="JsonPropertyInfo"/> instance has been locked for further modification.
/// </exception>
/// <remarks>
/// When resolving metadata via the built-in resolvers this will be populated with
/// the underlying <see cref="ConstructorInfo" /> of the serialized property or field.
/// </remarks>
public ICustomAttributeProvider? ConstructorAttributeProvider
{
get
{
Func<ICustomAttributeProvider>? ctorAttrProviderFactory = Volatile.Read(ref ConstructorAttributeProviderFactory);
ICustomAttributeProvider? ctorAttrProvider = _constructorAttributeProvider;
if (ctorAttrProvider is null && ctorAttrProviderFactory is not null)
{
_constructorAttributeProvider = ctorAttrProvider = ctorAttrProviderFactory();
Volatile.Write(ref ConstructorAttributeProviderFactory, null);
}
return ctorAttrProvider;
}
internal set
{
Debug.Assert(!IsReadOnly);
_constructorAttributeProvider = value;
Volatile.Write(ref ConstructorAttributeProviderFactory, null);
}
}
// Metadata emanating from the source generator use delayed attribute provider initialization
// ensuring that reflection metadata resolution remains pay-for-play and is trimmable.
internal Func<ICustomAttributeProvider>? ConstructorAttributeProviderFactory;
private ICustomAttributeProvider? _constructorAttributeProvider;
internal void VerifyMutable()
{
if (IsReadOnly)
{
ThrowHelper.ThrowInvalidOperationException_TypeInfoImmutable();
}
IsCustomized = true;
}
/// <summary>
/// Indicates that the current JsonTypeInfo might contain user modifications.
/// Defaults to true, and is only unset by the built-in contract resolvers.
/// </summary>
internal bool IsCustomized { get; set; } = true;
internal bool IsConfigured => _configurationState == ConfigurationState.Configured;
internal bool IsConfigurationStarted => _configurationState is not ConfigurationState.NotConfigured;
private volatile ConfigurationState _configurationState;
private enum ConfigurationState : byte
{
NotConfigured = 0,
Configuring = 1,
Configured = 2
};
private ExceptionDispatchInfo? _cachedConfigureError;
internal void EnsureConfigured()
{
if (!IsConfigured)
ConfigureSynchronized();
void ConfigureSynchronized()
{
Options.MakeReadOnly();
MakeReadOnly();
_cachedConfigureError?.Throw();
lock (Options.CacheContext)
{
if (_configurationState != ConfigurationState.NotConfigured)
{
// The value of _configurationState is either
// 'Configuring': recursive instance configured by this thread or
// 'Configured' : instance already configured by another thread.
// We can safely yield the configuration operation in both cases.
return;
}
_cachedConfigureError?.Throw();
try
{
_configurationState = ConfigurationState.Configuring;
Configure();
_configurationState = ConfigurationState.Configured;
}
catch (Exception e)
{
_cachedConfigureError = ExceptionDispatchInfo.Capture(e);
_configurationState = ConfigurationState.NotConfigured;
throw;
}
}
}
}
private void Configure()
{
Debug.Assert(Monitor.IsEntered(Options.CacheContext), "Configure called directly, use EnsureConfigured which synchronizes access to this method");
Debug.Assert(Options.IsReadOnly);
Debug.Assert(IsReadOnly);
PropertyInfoForTypeInfo.Configure();
if (PolymorphismOptions != null)
{
// This needs to be done before ConfigureProperties() is called
// JsonPropertyInfo.Configure() must have this value available in order to detect Polymoprhic + cyclic class case
PolymorphicTypeResolver = new PolymorphicTypeResolver(Options, PolymorphismOptions, Type, Converter.CanHaveMetadata);
ConfigureTypeClassifier();
}
if (Kind == JsonTypeInfoKind.Object)
{
ConfigureProperties();
if (DetermineUsesParameterizedConstructor())
{
ConfigureConstructorParameters();
}
}
if (ElementType != null)
{
_elementTypeInfo ??= Options.GetTypeInfoInternal(ElementType);
_elementTypeInfo.EnsureConfigured();
}
if (KeyType != null)
{
_keyTypeInfo ??= Options.GetTypeInfoInternal(KeyType);
_keyTypeInfo.EnsureConfigured();
}
DetermineIsCompatibleWithCurrentOptions();
CanUseSerializeHandler = HasSerializeHandler && IsCompatibleWithCurrentOptions;
// Validate union metadata before sealing. By this point, every modifier has run
// and any contract customization that the user wanted to apply has been applied,
// so the final shape of the JsonTypeInfo can be checked authoritatively.
if (Kind is JsonTypeInfoKind.Union)
{
ValidateUnionContract();
CacheUnionNullableCaseType();
ConfigureTypeClassifier();
}
// Build the union dispatch map after modifiers have had a chance to install
// a custom TypeClassifier. The classifier path bypasses the value-shape map entirely.
if (Kind is JsonTypeInfoKind.Union &&
_typeClassifier is null &&
UnionCases.Count > 0)
{
BuildUnionValueTypeMap(UnionCases, Options, this);
}
}
/// <summary>
/// Validates that a union <see cref="JsonTypeInfo"/> has a well-formed shape after
/// all modifiers have run: at least one declared case, plus both the constructor
/// and the deconstructor delegates.
/// </summary>
private void ValidateUnionContract()
{
Debug.Assert(Kind is JsonTypeInfoKind.Union);
if (UnionCases.Count == 0)
{
ThrowHelper.ThrowInvalidOperationException_UnionCasesNotPopulated(Type);
}
if (UnionConstructor is null)
{
ThrowHelper.ThrowInvalidOperationException_UnionCannotCreateValue(Type);
}
if (UnionDeconstructor is null)
{
ThrowHelper.ThrowInvalidOperationException_UnionCannotReadValue(Type);
}
}
private void CacheUnionNullableCaseType()
{
Debug.Assert(Kind is JsonTypeInfoKind.Union);
foreach (JsonUnionCaseInfo unionCase in UnionCases)
{
if (unionCase.IsNullable)
{
UnionNullableCaseType = unionCase.CaseType;
return;
}
}
UnionNullableCaseType = null;
}
private void ConfigureTypeClassifier()
{
Debug.Assert(Kind is JsonTypeInfoKind.Union || PolymorphismOptions is not null);
// The classifier factory may throw, which is fine --
// If invoked by the getter ahead of configure we rethrow directly,
// but if invoked by the configuration pipeline it captures and caches the exception.
JsonTypeClassifierFactory? factory = Volatile.Read(ref _typeClassifierFactory);
if (!_typeClassifierResolutionPending)
{
return;
}
JsonTypeClassifierContext ctx;
if (Kind is JsonTypeInfoKind.Union)
{
Debug.Assert(UnionCases.Count > 0);
ctx = new JsonTypeClassifierContext(
JsonTypeClassifierKind.Union,
Type,
new List<JsonUnionCaseInfo>(UnionCases),
Array.Empty<JsonDerivedType>(),
typeDiscriminatorPropertyName: null);
}
else
{
JsonPolymorphismOptions? polymorphismOptions = PolymorphismOptions;
Debug.Assert(polymorphismOptions is not null);
ctx = new JsonTypeClassifierContext(
JsonTypeClassifierKind.PolymorphicType,
Type,
Array.Empty<JsonUnionCaseInfo>(),
new List<JsonDerivedType>(polymorphismOptions.DerivedTypes),
polymorphismOptions.TypeDiscriminatorPropertyName);
}
if (factory is not null)
{
if (!factory.CanClassify(ctx))
{
ThrowHelper.ThrowInvalidOperationException_TypeClassifierNotSupported(factory.GetType(), Type);
}
}
else
{
factory = Options.GetTypeClassifierFromList(ctx);
}
if (factory is not null)
{
JsonTypeClassifier classifier = factory.CreateJsonClassifier(ctx, Options);
Interlocked.CompareExchange(ref _typeClassifier, classifier, null);
}
_typeClassifierFactory = null;
_typeClassifierResolutionPending = false;
}
/// <summary>
/// Builds the union dispatch metadata: a value-shape-to-type map for default deserialization
/// plus diagnostic state for cases that cannot be unambiguously classified by value shape.
/// </summary>
/// <remarks>
/// <para>
/// This helper is intentionally lenient at configuration time so that union types are
/// always serializable (serialization is driven by <see cref="UnionDeconstructor"/> and
/// doesn't need the value-shape map). Diagnostic state is recorded on the type info and
/// surfaced as a <see cref="JsonException"/> only at deserialization time when a token
/// is actually encountered that cannot be unambiguously routed to a single case.
/// </para>
/// <para>
/// Each case is categorized by its supported JSON value shapes via
/// <see cref="JsonConverter.GetSupportedJsonValueTypes"/>; built-in object/enumerable
/// converters that decline to advertise fall back to a <see cref="ConverterStrategy"/>-derived
/// default. Custom (user-defined) converters return <see cref="JsonValueType.None"/> from
/// <c>GetSupportedJsonValueTypes</c>; cases that use such converters are excluded from
/// the map and tracked via <see cref="UnionHasCustomConverterCase"/> because a custom
/// converter can produce any JSON value shape.
/// </para>
/// <para>
/// This helper operates purely on already-resolved <see cref="JsonTypeInfo"/> /
/// <see cref="JsonConverter"/> metadata and does not perform any reflection, so it is
/// safe to call from the trim-safe configuration pipeline.
/// </para>
/// </remarks>
private static void BuildUnionValueTypeMap(IList<JsonUnionCaseInfo> unionCases, JsonSerializerOptions options, JsonTypeInfo target)
{
var map = new Dictionary<JsonValueType, Type>();
JsonValueType ambiguousValueTypes = JsonValueType.None;
bool hasCustomConverterCase = false;
foreach (JsonUnionCaseInfo info in unionCases)
{
Type caseType = info.CaseType;
JsonTypeInfo caseTypeInfo = options.GetTypeInfoInternal(caseType);
JsonNumberHandling effectiveNumberHandling =
caseTypeInfo.NumberHandling ?? options.NumberHandling;
JsonConverter converter = caseTypeInfo.Converter;
JsonValueType valueTypes = converter.GetSupportedJsonValueTypes(effectiveNumberHandling);
if (valueTypes is JsonValueType.None)
{
if (!converter.IsInternalConverter)
{
// User-defined converter: any JSON value shape could be valid for this case.
// We can't safely include it in the dispatch map. Record the fact so
// that deserialization without a classifier fails with a precise error.
hasCustomConverterCase = true;
continue;
}
valueTypes = converter.ConverterStrategy switch
{
ConverterStrategy.Enumerable => JsonValueType.Array,
_ => JsonValueType.Object,
};
}
AddUnionValueTypes(valueTypes, caseType, map, ref ambiguousValueTypes);
}
target.UnionValueTypeMap = map;
target.UnionAmbiguousValueTypes = ambiguousValueTypes;
target.UnionHasCustomConverterCase = hasCustomConverterCase;
static void AddUnionValueTypes(
JsonValueType valueTypes,
Type caseType,
Dictionary<JsonValueType, Type> map,
ref JsonValueType ambiguousValueTypes)
{
ReadOnlySpan<JsonValueType> allValueTypes =
[
JsonValueType.Object,
JsonValueType.Array,
JsonValueType.String,
JsonValueType.Number,
JsonValueType.Boolean,
JsonValueType.Null,
];
foreach (JsonValueType valueType in allValueTypes)
{
if ((valueTypes & valueType) == 0)
{
continue;
}
if (!map.TryAdd(valueType, caseType))
{
// Two declared cases share this JSON value shape. First-wins for the dispatch
// map (so unrelated values still deserialize), but record the ambiguity
// so deserialize-time can throw a precise error if this shape shows up.
ambiguousValueTypes |= valueType;
}
}
}
}
/// <summary>
/// Gets any ancestor polymorphic types that declare
/// a type discriminator for the current type. Consulted
/// when serializing polymorphic values as objects.
/// </summary>
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
internal JsonTypeInfo? AncestorPolymorphicType
{
get
{
Debug.Assert(IsConfigured);
Debug.Assert(Type != typeof(object));
if (!_isAncestorPolymorphicTypeResolved)
{
_ancestorPolymorhicType = PolymorphicTypeResolver.FindNearestPolymorphicBaseType(this);
_isAncestorPolymorphicTypeResolved = true;
}
return _ancestorPolymorhicType;
}
}
private JsonTypeInfo? _ancestorPolymorhicType;
private volatile bool _isAncestorPolymorphicTypeResolved;
/// <summary>
/// Determines if the transitive closure of all JsonTypeInfo metadata referenced
/// by the current type (property types, key types, element types, ...) are
/// compatible with the settings as specified in JsonSerializerOptions.
/// </summary>
private void DetermineIsCompatibleWithCurrentOptions()
{
// Defines a recursive algorithm validating that the `IsCurrentNodeCompatible`
// predicate is valid for every node in the type graph. This method only checks
// the immediate children, with recursion being driven by the Configure() method.
// Therefore, this method must be called _after_ the child nodes have been configured.
Debug.Assert(IsReadOnly);
Debug.Assert(!IsConfigured);
if (!IsCurrentNodeCompatible())
{
IsCompatibleWithCurrentOptions = false;
return;
}
if (_properties != null)
{
foreach (JsonPropertyInfo property in _properties)
{
Debug.Assert(property.IsConfigured);
if (!property.IsPropertyTypeInfoConfigured)
{
// Either an ignored property or property is part of a cycle.
// In both cases we can ignore these instances.
continue;
}
if (!property.JsonTypeInfo.IsCompatibleWithCurrentOptions)
{
IsCompatibleWithCurrentOptions = false;
return;
}
}
}
if (_elementTypeInfo?.IsCompatibleWithCurrentOptions == false ||
_keyTypeInfo?.IsCompatibleWithCurrentOptions == false)
{
IsCompatibleWithCurrentOptions = false;
return;
}
Debug.Assert(IsCompatibleWithCurrentOptions);
// Defines the core predicate that must be checked for every node in the type graph.
bool IsCurrentNodeCompatible()
{
if (IsCustomized)
{
// Return false if we have detected contract customization by the user.
return false;
}
if (Options.CanUseFastPathSerializationLogic)
{
// Simple case/backward compatibility: options uses a combination of compatible built-in converters.
return true;
}
return OriginatingResolver.IsCompatibleWithOptions(Options);
}
}
/// <summary>
/// Holds the result of the above algorithm -- NB must default to true
/// to establish a base case for recursive types and any JsonIgnored property types.
/// </summary>
private bool IsCompatibleWithCurrentOptions { get; set; } = true;
/// <summary>
/// Determine if the current configuration is compatible with using a parameterized constructor.
/// </summary>
internal bool DetermineUsesParameterizedConstructor()
=> Converter.ConstructorIsParameterized && CreateObject is null;
/// <summary>
/// Creates a blank <see cref="JsonTypeInfo{T}"/> instance.
/// </summary>
/// <typeparam name="T">The type for which contract metadata is specified.</typeparam>
/// <param name="options">The <see cref="JsonSerializerOptions"/> instance the metadata is associated with.</param>
/// <returns>A blank <see cref="JsonTypeInfo{T}"/> instance.</returns>
/// <exception cref="ArgumentNullException"><paramref name="options"/> is null.</exception>
/// <remarks>
/// The returned <see cref="JsonTypeInfo{T}"/> will be blank, with the exception of the
/// <see cref="Converter"/> property which will be resolved either from
/// <see cref="JsonSerializerOptions.Converters"/> or the built-in converters for the type.
/// Any converters specified via <see cref="JsonConverterAttribute"/> on the type declaration
/// will not be resolved by this method.
///
/// What converter does get resolved influences the value of <see cref="Kind"/>,
/// which constrains the type of metadata that can be modified in the <see cref="JsonTypeInfo"/> instance.
/// </remarks>
[RequiresUnreferencedCode(MetadataFactoryRequiresUnreferencedCode)]
[RequiresDynamicCode(MetadataFactoryRequiresUnreferencedCode)]
public static JsonTypeInfo<T> CreateJsonTypeInfo<T>(JsonSerializerOptions options)
{
ArgumentNullException.ThrowIfNull(options);
JsonConverter converter = DefaultJsonTypeInfoResolver.GetConverterForType(typeof(T), options, resolveJsonConverterAttribute: false);
return new JsonTypeInfo<T>(converter, options);
}
/// <summary>
/// Creates a blank <see cref="JsonTypeInfo"/> instance.
/// </summary>
/// <param name="type">The type for which contract metadata is specified.</param>
/// <param name="options">The <see cref="JsonSerializerOptions"/> instance the metadata is associated with.</param>
/// <returns>A blank <see cref="JsonTypeInfo"/> instance.</returns>
/// <exception cref="ArgumentNullException"><paramref name="type"/> or <paramref name="options"/> is null.</exception>
/// <exception cref="ArgumentException"><paramref name="type"/> cannot be used for serialization.</exception>
/// <remarks>
/// The returned <see cref="JsonTypeInfo"/> will be blank, with the exception of the
/// <see cref="Converter"/> property which will be resolved either from
/// <see cref="JsonSerializerOptions.Converters"/> or the built-in converters for the type.
/// Any converters specified via <see cref="JsonConverterAttribute"/> on the type declaration
/// will not be resolved by this method.
///
/// What converter does get resolved influences the value of <see cref="Kind"/>,
/// which constrains the type of metadata that can be modified in the <see cref="JsonTypeInfo"/> instance.
/// </remarks>
[RequiresUnreferencedCode(MetadataFactoryRequiresUnreferencedCode)]
[RequiresDynamicCode(MetadataFactoryRequiresUnreferencedCode)]
public static JsonTypeInfo CreateJsonTypeInfo(Type type, JsonSerializerOptions options)
{
ArgumentNullException.ThrowIfNull(type);
ArgumentNullException.ThrowIfNull(options);
if (IsInvalidForSerialization(type))
{
ThrowHelper.ThrowArgumentException_CannotSerializeInvalidType(nameof(type), type, null, null);
}
JsonConverter converter = DefaultJsonTypeInfoResolver.GetConverterForType(type, options, resolveJsonConverterAttribute: false);
return CreateJsonTypeInfo(type, converter, options);
}
[RequiresUnreferencedCode(MetadataFactoryRequiresUnreferencedCode)]
[RequiresDynamicCode(MetadataFactoryRequiresUnreferencedCode)]
internal static JsonTypeInfo CreateJsonTypeInfo(Type type, JsonConverter converter, JsonSerializerOptions options)
{
JsonTypeInfo jsonTypeInfo;
if (converter.Type == type)
{
// For performance, avoid doing a reflection-based instantiation
// if the converter type matches that of the declared type.
jsonTypeInfo = converter.CreateJsonTypeInfo(options);
}
else
{
Type jsonTypeInfoType = typeof(JsonTypeInfo<>).MakeGenericType(type);
jsonTypeInfo = (JsonTypeInfo)jsonTypeInfoType.CreateInstanceNoWrapExceptions(
parameterTypes: [typeof(JsonConverter), typeof(JsonSerializerOptions)],
parameters: new object[] { converter, options })!;
}
Debug.Assert(jsonTypeInfo.Type == type);
return jsonTypeInfo;
}
/// <summary>
/// Creates a blank <see cref="JsonPropertyInfo"/> instance for the current <see cref="JsonTypeInfo"/>.
/// </summary>
/// <param name="propertyType">The declared type for the property.</param>
/// <param name="name">The property name used in JSON serialization and deserialization.</param>
/// <returns>A blank <see cref="JsonPropertyInfo"/> instance.</returns>
/// <exception cref="ArgumentNullException"><paramref name="propertyType"/> or <paramref name="name"/> is null.</exception>
/// <exception cref="ArgumentException"><paramref name="propertyType"/> cannot be used for serialization.</exception>
/// <exception cref="InvalidOperationException">The <see cref="JsonTypeInfo"/> instance has been locked for further modification.</exception>
[RequiresUnreferencedCode(MetadataFactoryRequiresUnreferencedCode)]
[RequiresDynamicCode(MetadataFactoryRequiresUnreferencedCode)]
public JsonPropertyInfo CreateJsonPropertyInfo(Type propertyType, string name)
{
ArgumentNullException.ThrowIfNull(propertyType);
ArgumentNullException.ThrowIfNull(name);
if (IsInvalidForSerialization(propertyType))
{
ThrowHelper.ThrowArgumentException_CannotSerializeInvalidType(nameof(propertyType), propertyType, Type, name);
}
VerifyMutable();
JsonPropertyInfo propertyInfo = CreatePropertyUsingReflection(propertyType, declaringType: null);
propertyInfo.Name = name;
return propertyInfo;
}
[RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
[RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
internal JsonPropertyInfo CreatePropertyUsingReflection(Type propertyType, Type? declaringType)
{
JsonPropertyInfo jsonPropertyInfo;
if (Options.TryGetTypeInfoCached(propertyType, out JsonTypeInfo? jsonTypeInfo))
{
// If a JsonTypeInfo has already been cached for the property type,
// avoid reflection-based initialization by delegating construction
// of JsonPropertyInfo<T> construction to the property type metadata.
jsonPropertyInfo = jsonTypeInfo.CreateJsonPropertyInfo(declaringTypeInfo: this, declaringType, Options);
}
else
{
// Metadata for `propertyType` has not been registered yet.
// Use reflection to instantiate the correct JsonPropertyInfo<T>
Type propertyInfoType = typeof(JsonPropertyInfo<>).MakeGenericType(propertyType);
jsonPropertyInfo = (JsonPropertyInfo)propertyInfoType.CreateInstanceNoWrapExceptions(
parameterTypes: [typeof(Type), typeof(JsonTypeInfo), typeof(JsonSerializerOptions)],
parameters: new object[] { declaringType ?? Type, this, Options })!;
}
Debug.Assert(jsonPropertyInfo.PropertyType == propertyType);
return jsonPropertyInfo;
}
/// <summary>
/// Creates a JsonPropertyInfo whose property type matches the type of this JsonTypeInfo instance.
/// </summary>
private protected abstract JsonPropertyInfo CreateJsonPropertyInfo(JsonTypeInfo declaringTypeInfo, Type? declaringType, JsonSerializerOptions options);
private protected Dictionary<ParameterLookupKey, JsonParameterInfoValues>? _parameterInfoValuesIndex;
// Untyped, root-level serialization methods
internal abstract void SerializeAsObject(Utf8JsonWriter writer, object? rootValue);
internal abstract Task SerializeAsObjectAsync(PipeWriter pipeWriter, object? rootValue, int flushThreshold, CancellationToken cancellationToken);
internal abstract Task SerializeAsObjectAsync(Stream utf8Json, object? rootValue, CancellationToken cancellationToken);
internal abstract Task SerializeAsObjectAsync(PipeWriter utf8Json, object? rootValue, CancellationToken cancellationToken);
internal abstract void SerializeAsObject(Stream utf8Json, object? rootValue);
// Untyped, root-level deserialization methods
internal abstract object? DeserializeAsObject(ref Utf8JsonReader reader, ref ReadStack state);
internal abstract ValueTask<object?> DeserializeAsObjectAsync(PipeReader utf8Json, CancellationToken cancellationToken);
internal abstract ValueTask<object?> DeserializeAsObjectAsync(Stream utf8Json, CancellationToken cancellationToken);
internal abstract object? DeserializeAsObject(Stream utf8Json);
internal ref struct PropertyHierarchyResolutionState(JsonSerializerOptions options)
{
public Dictionary<string, (JsonPropertyInfo, int index)> AddedProperties = new(options.PropertyNameCaseInsensitive ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal);
public Dictionary<string, JsonPropertyInfo>? IgnoredProperties;
public bool IsPropertyOrderSpecified;
}
private protected readonly struct ParameterLookupKey(Type type, string name) : IEquatable<ParameterLookupKey>
{
public Type Type { get; } = type;
public string Name { get; } = name;
public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Name);
public bool Equals(ParameterLookupKey other) => Type == other.Type && string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase);
public override bool Equals([NotNullWhen(true)] object? obj) => obj is ParameterLookupKey key && Equals(key);
}
internal void ConfigureProperties()
{
Debug.Assert(Kind == JsonTypeInfoKind.Object);
Debug.Assert(_propertyCache is null);
Debug.Assert(_propertyIndex is null);
Debug.Assert(ExtensionDataProperty is null);
JsonPropertyInfoList properties = PropertyList;
StringComparer comparer = Options.PropertyNameCaseInsensitive ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
Dictionary<string, JsonPropertyInfo> propertyIndex = new(properties.Count, comparer);
List<JsonPropertyInfo> propertyCache = new(properties.Count);
bool arePropertiesSorted = true;
int previousPropertyOrder = int.MinValue;
BitArray? requiredPropertiesMask = null;
for (int i = 0; i < properties.Count; i++)
{
JsonPropertyInfo property = properties[i];
Debug.Assert(property.DeclaringTypeInfo == this);
if (property.IsExtensionData)
{
if (UnmappedMemberHandling is JsonUnmappedMemberHandling.Disallow)
{
ThrowHelper.ThrowInvalidOperationException_ExtensionDataConflictsWithUnmappedMemberHandling(Type, property);
}
if (ExtensionDataProperty != null)
{
ThrowHelper.ThrowInvalidOperationException_SerializationDuplicateTypeAttribute(Type, typeof(JsonExtensionDataAttribute));
}
ExtensionDataProperty = property;
}
else
{
property.PropertyIndex = i;
if (property.IsRequired)
{
(requiredPropertiesMask ??= new BitArray(properties.Count))[i] = true;
}
if (arePropertiesSorted)
{
arePropertiesSorted = previousPropertyOrder <= property.Order;
previousPropertyOrder = property.Order;
}
if (!propertyIndex.TryAdd(property.Name, property))
{
ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNameConflict(Type, property.Name);
}
propertyCache.Add(property);
}
property.Configure();
}
if (!arePropertiesSorted)
{
// Properties have been configured by the user and require sorting.
properties.SortProperties();
propertyCache.StableSortByKey(static propInfo => propInfo.Order);
}
OptionalPropertiesMask = requiredPropertiesMask?.Not();
_propertyCache = propertyCache.ToArray();
_propertyIndex = propertyIndex;
// Override global UnmappedMemberHandling configuration
// if type specifies an extension data property.
EffectiveUnmappedMemberHandling = UnmappedMemberHandling ??
(ExtensionDataProperty is null
? Options.UnmappedMemberHandling
: JsonUnmappedMemberHandling.Skip);
}
internal void PopulateParameterInfoValues(JsonParameterInfoValues[] parameterInfoValues)
{
Dictionary<ParameterLookupKey, JsonParameterInfoValues> parameterIndex = new(parameterInfoValues.Length);
foreach (JsonParameterInfoValues parameterInfoValue in parameterInfoValues)
{
ParameterLookupKey paramKey = new(parameterInfoValue.ParameterType, parameterInfoValue.Name);
parameterIndex.TryAdd(paramKey, parameterInfoValue); // Ignore conflicts since they are reported at serialization time.
}
ParameterCount = parameterInfoValues.Length;
_parameterInfoValuesIndex = parameterIndex;
}
internal void ResolveMatchingParameterInfo(JsonPropertyInfo propertyInfo)
{
Debug.Assert(
CreateObjectWithArgs is null || _parameterInfoValuesIndex is not null,
"Metadata with parameterized constructors must have populated parameter info metadata.");
if (_parameterInfoValuesIndex is not { } index)
{
return;
}
string propertyName = propertyInfo.MemberName ?? propertyInfo.Name;
ParameterLookupKey propKey = new(propertyInfo.PropertyType, propertyName);
if (index.TryGetValue(propKey, out JsonParameterInfoValues? matchingParameterInfoValues))
{
propertyInfo.AddJsonParameterInfo(matchingParameterInfoValues);
}
}
internal void ConfigureConstructorParameters()
{
Debug.Assert(Kind == JsonTypeInfoKind.Object);
Debug.Assert(DetermineUsesParameterizedConstructor());
Debug.Assert(_propertyCache is not null);
Debug.Assert(_parameterCache is null);
List<JsonParameterInfo> parameterCache = new(ParameterCount);
Dictionary<ParameterLookupKey, JsonParameterInfo> parameterIndex = new(ParameterCount);
foreach (JsonPropertyInfo propertyInfo in _propertyCache)
{
JsonParameterInfo? parameterInfo = propertyInfo.AssociatedParameter;
if (parameterInfo is null)
{
continue;
}
string propertyName = propertyInfo.MemberName ?? propertyInfo.Name;
ParameterLookupKey paramKey = new(propertyInfo.PropertyType, propertyName);
if (!parameterIndex.TryAdd(paramKey, parameterInfo))
{
// Multiple object properties cannot bind to the same constructor parameter.
ThrowHelper.ThrowInvalidOperationException_MultiplePropertiesBindToConstructorParameters(
Type,
parameterInfo.Name,
propertyInfo.Name,
parameterIndex[paramKey].MatchingProperty.Name);
}
parameterCache.Add(parameterInfo);
}
if (ExtensionDataProperty is { AssociatedParameter: not null })
{
Debug.Assert(ExtensionDataProperty.MemberName != null, "Custom property info cannot be data extension property");
ThrowHelper.ThrowInvalidOperationException_ExtensionDataCannotBindToCtorParam(ExtensionDataProperty.MemberName, ExtensionDataProperty);
}
_parameterCache = parameterCache.ToArray();
_parameterInfoValuesIndex = null;
}
internal static void ValidateType(Type type)
{
if (IsInvalidForSerialization(type))
{
ThrowHelper.ThrowInvalidOperationException_CannotSerializeInvalidType(type, declaringType: null, memberInfo: null);
}
}
internal static bool IsInvalidForSerialization(Type type)
{
return type == typeof(void) || type.IsPointer || type.IsByRef || IsByRefLike(type) || type.ContainsGenericParameters;
}
internal void MapInterfaceTypesToCallbacks()
{
Debug.Assert(!IsReadOnly);
if (Kind is JsonTypeInfoKind.Object or JsonTypeInfoKind.Enumerable or JsonTypeInfoKind.Dictionary)
{
if (typeof(IJsonOnSerializing).IsAssignableFrom(Type))
{
OnSerializing = static obj => ((IJsonOnSerializing)obj).OnSerializing();
}
if (typeof(IJsonOnSerialized).IsAssignableFrom(Type))
{
OnSerialized = static obj => ((IJsonOnSerialized)obj).OnSerialized();
}
if (typeof(IJsonOnDeserializing).IsAssignableFrom(Type))
{
OnDeserializing = static obj => ((IJsonOnDeserializing)obj).OnDeserializing();
}
if (typeof(IJsonOnDeserialized).IsAssignableFrom(Type))
{
OnDeserialized = static obj => ((IJsonOnDeserialized)obj).OnDeserialized();
}
}
}
internal void SetCreateObjectIfCompatible(Delegate? createObject)
{
Debug.Assert(!IsReadOnly);
// Guard against the reflection resolver/source generator attempting to pass
// a CreateObject delegate to converters/metadata that do not support it.
if (Converter.SupportsCreateObjectDelegate && !Converter.ConstructorIsParameterized)
{
SetCreateObject(createObject);
}
}
private static bool IsByRefLike(Type type)
{
#if NET
return type.IsByRefLike;
#else
if (!type.IsValueType)
{
return false;
}
object[] attributes = type.GetCustomAttributes(inherit: false);
for (int i = 0; i < attributes.Length; i++)
{
if (attributes[i].GetType().FullName == "System.Runtime.CompilerServices.IsByRefLikeAttribute")
{
return true;
}
}
return false;
#endif
}
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
internal bool SupportsPolymorphicDeserialization
{
get
{
Debug.Assert(IsConfigurationStarted);
return PolymorphicTypeResolver?.UsesTypeDiscriminators == true;
}
}
internal static bool IsValidExtensionDataProperty(Type propertyType)
{
return typeof(IDictionary<string, object>).IsAssignableFrom(propertyType) ||
typeof(IDictionary<string, JsonElement>).IsAssignableFrom(propertyType) ||
propertyType == typeof(IReadOnlyDictionary<string, object>) ||
propertyType == typeof(IReadOnlyDictionary<string, JsonElement>) ||
// Avoid a reference to typeof(JsonNode) to support trimming.
(propertyType.FullName == JsonObjectTypeName && ReferenceEquals(propertyType.Assembly, typeof(JsonTypeInfo).Assembly));
}
private static JsonTypeInfoKind GetTypeInfoKind(Type type, JsonConverter converter)
{
if (type == typeof(object) && converter.CanBePolymorphic)
{
// System.Object is polymorphic and will not respect Properties
Debug.Assert(converter is ObjectConverter);
return JsonTypeInfoKind.None;
}
switch (converter.ConverterStrategy)
{
case ConverterStrategy.Value: return JsonTypeInfoKind.None;
case ConverterStrategy.Object: return JsonTypeInfoKind.Object;
case ConverterStrategy.Union:
// Nullable<TUnion> delegates to the underlying union converter, but the
// nullable wrapper does not own union case metadata.
return Nullable.GetUnderlyingType(type) is null ? JsonTypeInfoKind.Union : JsonTypeInfoKind.None;
case ConverterStrategy.Enumerable: return JsonTypeInfoKind.Enumerable;
case ConverterStrategy.Dictionary: return JsonTypeInfoKind.Dictionary;
case ConverterStrategy.None:
Debug.Assert(converter is JsonConverterFactory);
ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(type);
return default;
default:
Debug.Fail($"Unexpected class type: {converter.ConverterStrategy}");
throw new InvalidOperationException();
}
}
internal sealed class JsonPropertyInfoList : ConfigurationList<JsonPropertyInfo>
{
private readonly JsonTypeInfo _jsonTypeInfo;
public JsonPropertyInfoList(JsonTypeInfo jsonTypeInfo)
{
_jsonTypeInfo = jsonTypeInfo;
}
public override bool IsReadOnly => _jsonTypeInfo._properties == this && _jsonTypeInfo.IsReadOnly || _jsonTypeInfo.Kind != JsonTypeInfoKind.Object;
protected override void OnCollectionModifying()
{
if (_jsonTypeInfo._properties == this)
{
_jsonTypeInfo.VerifyMutable();
}
if (_jsonTypeInfo.Kind != JsonTypeInfoKind.Object)
{
ThrowHelper.ThrowInvalidOperationException_JsonTypeInfoOperationNotPossibleForKind(_jsonTypeInfo.Kind);
}
}
protected override void ValidateAddedValue(JsonPropertyInfo item)
{
item.EnsureChildOf(_jsonTypeInfo);
}
public void SortProperties()
{
_list.StableSortByKey(static propInfo => propInfo.Order);
}
/// <summary>
/// Used by the built-in resolvers to add property metadata applying conflict resolution.
/// </summary>
public void AddPropertyWithConflictResolution(JsonPropertyInfo jsonPropertyInfo, ref PropertyHierarchyResolutionState state)
{
Debug.Assert(!_jsonTypeInfo.IsConfigured);
Debug.Assert(jsonPropertyInfo.MemberName != null, "MemberName can be null in custom JsonPropertyInfo instances and should never be passed in this method");
// Algorithm should be kept in sync with the Roslyn equivalent in JsonSourceGenerator.Parser.cs
string memberName = jsonPropertyInfo.MemberName;
if (state.AddedProperties.TryAdd(jsonPropertyInfo.Name, (jsonPropertyInfo, Count)))
{
Add(jsonPropertyInfo);
state.IsPropertyOrderSpecified |= jsonPropertyInfo.Order != 0;
}
else
{
// The JsonPropertyNameAttribute or naming policy resulted in a collision.
(JsonPropertyInfo other, int index) = state.AddedProperties[jsonPropertyInfo.Name];
if (other.IsIgnored)
{
// Overwrite previously cached property since it has [JsonIgnore].
state.AddedProperties[jsonPropertyInfo.Name] = (jsonPropertyInfo, index);
this[index] = jsonPropertyInfo;
state.IsPropertyOrderSpecified |= jsonPropertyInfo.Order != 0;
}
else
{
bool ignoreCurrentProperty =
// Does the current property have `JsonIgnoreAttribute`?
jsonPropertyInfo.IsIgnored ||
// Is the current property hidden by the previously cached property
// (with `new` keyword, or by overriding)?
jsonPropertyInfo.IsOverriddenOrShadowedBy(other) ||
// Was a property with the same CLR name ignored? That property hid the current property,
// thus, if it was ignored, the current property should be ignored too.
(state.IgnoredProperties?.TryGetValue(memberName, out JsonPropertyInfo? ignored) == true && jsonPropertyInfo.IsOverriddenOrShadowedBy(ignored));
if (!ignoreCurrentProperty)
{
ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNameConflict(_jsonTypeInfo.Type, jsonPropertyInfo.Name);
}
}
}
if (jsonPropertyInfo.IsIgnored)
{
(state.IgnoredProperties ??= new())[memberName] = jsonPropertyInfo;
}
}
}
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private string DebuggerDisplay => $"Type = {Type.Name}, Kind = {Kind}";
}
}
|