public System.Text.Json.JsonNamingPolicy? PropertyNamingPolicy { get { throw null; } set { } }
public System.Text.Json.JsonCommentHandling ReadCommentHandling { get { throw null; } set { } }
public System.Text.Json.Serialization.ReferenceHandler? ReferenceHandler { get { throw null; } set { } }
+ [System.Diagnostics.CodeAnalysis.AllowNullAttribute]
public System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver TypeInfoResolver { [System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("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."), System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")] get { throw null; } set { } }
public System.Text.Json.Serialization.JsonUnknownTypeHandling UnknownTypeHandling { get { throw null; } set { } }
public bool WriteIndented { get { throw null; } set { } }
<value>The converter '{0}' is not compatible with the type '{1}'.</value>
</data>
<data name="ResolverTypeNotCompatible" xml:space="preserve">
- <value>TypeInfoResolver expected to return JsonTypeInfo of type '{0}' but returned JsonTypeInfo of type '{1}'.</value>
+ <value>The IJsonTypeInfoResolver returned an incompatible JsonTypeInfo instance of type '{0}', expected type '{1}'.</value>
</data>
<data name="ResolverTypeInfoOptionsNotCompatible" xml:space="preserve">
- <value>TypeInfoResolver expected to return JsonTypeInfo options bound to the JsonSerializerOptions provided in the argument.</value>
+ <value>The IJsonTypeInfoResolver returned a JsonTypeInfo instance whose JsonSerializerOptions setting does not match the provided argument.</value>
</data>
<data name="SerializationConverterWrite" xml:space="preserve">
<value>The converter '{0}' wrote too much or not enough.</value>
break;
}
- return converter!;
+ return converter;
}
internal sealed override object ReadCoreAsObject(
internal sealed override JsonConverter<TTarget> CreateCastingConverter<TTarget>()
{
+ JsonSerializerOptions.CheckConverterNullabilityIsSameAsPropertyType(this, typeof(TTarget));
return new CastingConverter<TTarget, T>(this);
}
Debug.Assert(runtimeType != null);
options ??= JsonSerializerOptions.Default;
- if (!options.IsInitializedForReflectionSerializer)
- {
- options.InitializeForReflectionSerializer();
- }
+ options.InitializeForReflectionSerializer();
- return options.GetOrAddJsonTypeInfoForRootType(runtimeType);
+ return options.GetJsonTypeInfoForRootType(runtimeType);
}
private static JsonTypeInfo GetTypeInfo(JsonSerializerContext context, Type type)
ThrowHelper.ThrowArgumentNullException(nameof(utf8Json));
}
- options ??= JsonSerializerOptions.Default;
- if (!options.IsInitializedForReflectionSerializer)
- {
- options.InitializeForReflectionSerializer();
- }
-
- JsonTypeInfo jsonTypeInfo = options.GetOrAddJsonTypeInfoForRootType(typeof(TValue));
+ JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, typeof(TValue));
return CreateAsyncEnumerableDeserializer(utf8Json, CreateQueueTypeInfo<TValue>(jsonTypeInfo), cancellationToken);
}
{
Debug.Assert(writer != null);
- Debug.Assert(!jsonTypeInfo.HasSerialize ||
- jsonTypeInfo is not JsonTypeInfo<TValue> ||
- jsonTypeInfo.Options.SerializerContext == null ||
- !jsonTypeInfo.Options.SerializerContext.CanUseSerializationLogic,
- "Incorrect method called. WriteUsingGeneratedSerializer() should have been called instead.");
+ // TODO unify method with WriteUsingGeneratedSerializer
WriteStack state = default;
jsonTypeInfo.EnsureConfigured();
{
private bool? _canUseSerializationLogic;
- internal JsonSerializerOptions? _options;
+ private JsonSerializerOptions? _options;
/// <summary>
/// Gets the run time specified options of the context. If no options were passed
/// when instanciating the context, then a new instance is bound and returned.
/// </summary>
/// <remarks>
- /// The instance cannot be mutated once it is bound to the context instance.
+ /// The options instance cannot be mutated once it is bound to the context instance.
/// </remarks>
- public JsonSerializerOptions Options => _options ??= new JsonSerializerOptions { TypeInfoResolver = this };
+ public JsonSerializerOptions Options
+ {
+ get => _options ??= new JsonSerializerOptions { TypeInfoResolver = this, IsLockedInstance = true };
+
+ internal set
+ {
+ Debug.Assert(!value.IsLockedInstance);
+ value.TypeInfoResolver = this;
+ value.IsLockedInstance = true;
+ _options = value;
+ }
+ }
/// <summary>
/// Indicates whether pre-generated serialization logic for types in the context
{
if (options != null)
{
- options.TypeInfoResolver = this;
- Debug.Assert(_options == options, "options.TypeInfoResolver setter did not assign options");
+ options.VerifyMutable();
+ Options = options;
}
}
JsonTypeInfo? IJsonTypeInfoResolver.GetTypeInfo(Type type, JsonSerializerOptions options)
{
- if (options != null && _options != options)
+ if (options != null && options != _options)
{
- // TODO is this the appropriate exception message to throw?
- ThrowHelper.ThrowInvalidOperationException_SerializerContextOptionsImmutable();
+ ThrowHelper.ThrowInvalidOperationException_ResolverTypeInfoOptionsNotCompatible();
}
return GetTypeInfo(type);
/// <summary>
/// This method returns configured non-null JsonTypeInfo
/// </summary>
- internal JsonTypeInfo GetOrAddJsonTypeInfo(Type type)
+ internal JsonTypeInfo GetJsonTypeInfoCached(Type type)
{
- if (_cachingContext == null)
+ JsonTypeInfo? typeInfo = null;
+
+ if (IsLockedInstance)
{
- InitializeCachingContext();
+ typeInfo = GetCachingContext()?.GetOrAddJsonTypeInfo(type);
}
- JsonTypeInfo? typeInfo = _cachingContext.GetOrAddJsonTypeInfo(type);
-
if (typeInfo == null)
{
ThrowHelper.ThrowNotSupportedException_NoMetadataForType(type);
}
typeInfo.EnsureConfigured();
-
return typeInfo;
}
- internal bool TryGetJsonTypeInfo(Type type, [NotNullWhen(true)] out JsonTypeInfo? typeInfo)
+ internal bool TryGetJsonTypeInfoCached(Type type, [NotNullWhen(true)] out JsonTypeInfo? typeInfo)
{
if (_cachingContext == null)
{
return _cachingContext.TryGetJsonTypeInfo(type, out typeInfo);
}
- internal bool IsJsonTypeInfoCached(Type type) => _cachingContext?.IsJsonTypeInfoCached(type) == true;
-
/// <summary>
/// Return the TypeInfo for root API calls.
/// This has an LRU cache that is intended only for public API calls that specify the root type.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal JsonTypeInfo GetOrAddJsonTypeInfoForRootType(Type type)
+ internal JsonTypeInfo GetJsonTypeInfoForRootType(Type type)
{
JsonTypeInfo? jsonTypeInfo = _lastTypeInfo;
if (jsonTypeInfo?.Type != type)
{
- jsonTypeInfo = GetOrAddJsonTypeInfo(type);
+ jsonTypeInfo = GetJsonTypeInfoCached(type);
_lastTypeInfo = jsonTypeInfo;
}
_lastTypeInfo = null;
}
- [MemberNotNull(nameof(_cachingContext))]
- private void InitializeCachingContext()
+ private CachingContext? GetCachingContext()
{
- _isLockedInstance = true;
- _cachingContext = TrackedCachingContexts.GetOrCreate(this);
+ Debug.Assert(IsLockedInstance);
+
+ if (_cachingContext is null && _typeInfoResolver is not null)
+ {
+ _cachingContext = TrackedCachingContexts.GetOrCreate(this);
+ }
+
+ return _cachingContext;
}
/// <summary>
/// </summary>
internal sealed class CachingContext
{
- private readonly ConcurrentDictionary<Type, JsonTypeInfo> _jsonTypeInfoCache = new();
+ private readonly ConcurrentDictionary<Type, JsonTypeInfo?> _jsonTypeInfoCache = new();
public CachingContext(JsonSerializerOptions options)
{
// If changing please ensure that src/ILLink.Descriptors.LibraryBuild.xml is up-to-date.
public int Count => _jsonTypeInfoCache.Count;
- public JsonTypeInfo? GetOrAddJsonTypeInfo(Type type)
- {
- if (_jsonTypeInfoCache.TryGetValue(type, out JsonTypeInfo? typeInfo))
- {
- return typeInfo;
- }
-
- typeInfo = Options.GetTypeInfoInternal(type);
- if (typeInfo != null)
- {
- return _jsonTypeInfoCache.GetOrAdd(type, _ => typeInfo);
- }
-
- return null;
- }
-
+ public JsonTypeInfo? GetOrAddJsonTypeInfo(Type type) => _jsonTypeInfoCache.GetOrAdd(type, Options.GetTypeInfoNoCaching);
public bool TryGetJsonTypeInfo(Type type, [NotNullWhen(true)] out JsonTypeInfo? typeInfo) => _jsonTypeInfoCache.TryGetValue(type, out typeInfo);
- public bool IsJsonTypeInfoCached(Type type) => _jsonTypeInfoCache.ContainsKey(type);
public void Clear()
{
new(concurrencyLevel: 1, capacity: MaxTrackedContexts, new EqualityComparer());
private const int EvictionCountHistory = 16;
- private static Queue<int> s_recentEvictionCounts = new(EvictionCountHistory);
+ private static readonly Queue<int> s_recentEvictionCounts = new(EvictionCountHistory);
private static int s_evictionRunsToSkip;
public static CachingContext GetOrCreate(JsonSerializerOptions options)
{
- Debug.Assert(options._isLockedInstance, "Cannot create caching contexts for mutable JsonSerializerOptions instances");
+ Debug.Assert(options.IsLockedInstance, "Cannot create caching contexts for mutable JsonSerializerOptions instances");
+ Debug.Assert(options._typeInfoResolver != null);
+
ConcurrentDictionary<JsonSerializerOptions, WeakReference<CachingContext>> cache = s_cache;
if (cache.TryGetValue(options, out WeakReference<CachingContext>? wr) && wr.TryGetTarget(out CachingContext? ctx))
// Use a defensive copy of the options instance as key to
// avoid capturing references to any caching contexts.
- var key = new JsonSerializerOptions(options)
- {
- // Copy fields ignored by the copy constructor
- // but are necessary to determine equivalence.
- _typeInfoResolver = options._typeInfoResolver,
- };
+ var key = new JsonSerializerOptions(options);
Debug.Assert(key._cachingContext == null);
ctx = new CachingContext(options);
left._includeFields == right._includeFields &&
left._propertyNameCaseInsensitive == right._propertyNameCaseInsensitive &&
left._writeIndented == right._writeIndented &&
- NormalizeResolver(left._typeInfoResolver) == NormalizeResolver(right._typeInfoResolver) &&
+ left._typeInfoResolver == right._typeInfoResolver &&
CompareLists(left._converters, right._converters);
static bool CompareLists<TValue>(ConfigurationList<TValue> left, ConfigurationList<TValue> right)
hc.Add(options._includeFields);
hc.Add(options._propertyNameCaseInsensitive);
hc.Add(options._writeIndented);
- hc.Add(NormalizeResolver(options._typeInfoResolver));
+ hc.Add(options._typeInfoResolver);
GetHashCode(ref hc, options._converters);
static void GetHashCode<TValue>(ref HashCode hc, ConfigurationList<TValue> list)
return hc.ToHashCode();
}
- // An options instance might be locked but not initialized for reflection serialization yet.
- private static IJsonTypeInfoResolver? NormalizeResolver(IJsonTypeInfoResolver? resolver)
- => resolver ?? DefaultJsonTypeInfoResolver.DefaultInstance;
-
#if !NETCOREAPP
/// <summary>
/// Polyfill for System.HashCode.
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
-using System.Reflection;
using System.Text.Json.Reflection;
using System.Text.Json.Serialization;
-using System.Text.Json.Serialization.Converters;
using System.Text.Json.Serialization.Metadata;
-using System.Threading;
namespace System.Text.Json
{
/// </remarks>
public IList<JsonConverter> Converters => _converters;
- // This may return factory converter
- internal JsonConverter? GetCustomConverterFromMember(Type typeToConvert, MemberInfo memberInfo)
- {
- Debug.Assert(memberInfo.DeclaringType != null, "Properties and fields always have a declaring type.");
- JsonConverter? converter = null;
-
- JsonConverterAttribute? converterAttribute = memberInfo.GetUniqueCustomAttribute<JsonConverterAttribute>(inherit: false);
- if (converterAttribute != null)
- {
- converter = GetConverterFromAttribute(converterAttribute, typeToConvert, memberInfo);
- }
-
- return converter;
- }
-
- /// <summary>
- /// Gets converter for type but does not use TypeInfoResolver
- /// </summary>
- internal JsonConverter GetConverterForType(Type typeToConvert)
- {
- JsonConverter converter = GetConverterFromOptionsOrReflectionConverter(typeToConvert);
- Debug.Assert(converter != null);
-
- converter = ExpandFactoryConverter(converter, typeToConvert);
-
- CheckConverterNullabilityIsSameAsPropertyType(converter, typeToConvert);
-
- return converter;
- }
-
- [return: NotNullIfNotNull("converter")]
- internal JsonConverter? ExpandFactoryConverter(JsonConverter? converter, Type typeToConvert)
- {
- if (converter is JsonConverterFactory factory)
- {
- converter = factory.GetConverterInternal(typeToConvert, this);
-
- // A factory cannot return null; GetConverterInternal checked for that.
- Debug.Assert(converter != null);
- }
-
- return converter;
- }
-
- internal static void CheckConverterNullabilityIsSameAsPropertyType(JsonConverter converter, Type propertyType)
- {
- // User has indicated that either:
- // a) a non-nullable-struct handling converter should handle a nullable struct type or
- // b) a nullable-struct handling converter should handle a non-nullable struct type.
- // User should implement a custom converter for the underlying struct and remove the unnecessary CanConvert method override.
- // The serializer will automatically wrap the custom converter with NullableConverter<T>.
- //
- // We also throw to avoid passing an invalid argument to setters for nullable struct properties,
- // which would cause an InvalidProgramException when the generated IL is invoked.
- if (propertyType.IsValueType && converter.IsValueType &&
- (propertyType.IsNullableOfT() ^ converter.TypeToConvert.IsNullableOfT()))
- {
- ThrowHelper.ThrowInvalidOperationException_ConverterCanConvertMultipleTypes(propertyType, converter);
- }
- }
-
/// <summary>
/// Returns the converter for the specified type.
/// </summary>
ThrowHelper.ThrowArgumentNullException(nameof(typeToConvert));
}
- DefaultJsonTypeInfoResolver.RootDefaultInstance();
+ _typeInfoResolver ??= DefaultJsonTypeInfoResolver.RootDefaultInstance();
return GetConverterFromTypeInfo(typeToConvert);
}
/// </summary>
internal JsonConverter GetConverterFromTypeInfo(Type typeToConvert)
{
- if (_cachingContext == null)
- {
- if (_isLockedInstance)
- {
- InitializeCachingContext();
- }
- else
- {
- // We do not want to lock options instance here but we need to return correct answer
- // which means we need to go through TypeInfoResolver but without caching because that's the
- // only place which will have correct converter for JsonSerializerContext and reflection
- // based resolver. It will also work correctly for combined resolvers.
- return GetTypeInfoInternal(typeToConvert)?.Converter
- ?? GetConverterFromOptionsOrReflectionConverter(typeToConvert);
+ JsonConverter? converter;
- }
+ if (IsLockedInstance)
+ {
+ converter = GetCachingContext()?.GetOrAddJsonTypeInfo(typeToConvert)?.Converter;
}
-
- JsonConverter? converter = _cachingContext.GetOrAddJsonTypeInfo(typeToConvert)?.Converter;
-
- // we can get here if resolver returned null but converter was added for the type
- converter ??= GetConverterFromOptions(typeToConvert);
-
- if (converter == null)
+ else
{
- ThrowHelper.ThrowNotSupportedException_BuiltInConvertersNotRooted(typeToConvert);
- return null!;
+ // We do not want to lock options instance here but we need to return correct answer
+ // which means we need to go through TypeInfoResolver but without caching because that's the
+ // only place which will have correct converter for JsonSerializerContext and reflection
+ // based resolver. It will also work correctly for combined resolvers.
+ converter = GetTypeInfoNoCaching(typeToConvert)?.Converter;
}
- return converter;
+ return converter ?? GetConverterFromListOrBuiltInConverter(typeToConvert);
}
- private JsonConverter? GetConverterFromOptions(Type typeToConvert)
+ internal JsonConverter? GetConverterFromList(Type typeToConvert)
{
foreach (JsonConverter item in _converters)
{
return null;
}
- private JsonConverter GetConverterFromOptionsOrReflectionConverter(Type typeToConvert)
+ internal JsonConverter GetConverterFromListOrBuiltInConverter(Type typeToConvert)
{
- Debug.Assert(typeToConvert != null);
+ JsonConverter? converter = GetConverterFromList(typeToConvert);
+ return GetCustomOrBuiltInConverter(typeToConvert, converter);
+ }
- // Priority 1: Attempt to get custom converter from the Converters list.
- JsonConverter? converter = GetConverterFromOptions(typeToConvert);
+ internal JsonConverter GetCustomOrBuiltInConverter(Type typeToConvert, JsonConverter? converter)
+ {
+ // Attempt to get built-in converter.
+ converter ??= DefaultJsonTypeInfoResolver.GetBuiltInConverter(typeToConvert);
+ // Expand potential convert converter factory.
+ converter = ExpandConverterFactory(converter, typeToConvert);
- // Priority 2: Attempt to get converter from [JsonConverter] on the type being converted.
- if (converter == null)
+ if (!converter.TypeToConvert.IsInSubtypeRelationshipWith(typeToConvert))
{
- JsonConverterAttribute? converterAttribute = typeToConvert.GetUniqueCustomAttribute<JsonConverterAttribute>(inherit: false);
- if (converterAttribute != null)
- {
- converter = GetConverterFromAttribute(converterAttribute, typeToConvert: typeToConvert, memberInfo: null);
- }
+ ThrowHelper.ThrowInvalidOperationException_SerializationConverterNotCompatible(converter.GetType(), converter.TypeToConvert);
}
- // Priority 3: Attempt to get built-in converter.
- converter ??= DefaultJsonTypeInfoResolver.GetDefaultConverter(typeToConvert);
+ CheckConverterNullabilityIsSameAsPropertyType(converter, typeToConvert);
+ return converter;
+ }
- // Allow redirection for generic types or the enum converter.
+ [return: NotNullIfNotNull("converter")]
+ internal JsonConverter? ExpandConverterFactory(JsonConverter? converter, Type typeToConvert)
+ {
if (converter is JsonConverterFactory factory)
{
converter = factory.GetConverterInternal(typeToConvert, this);
-
- // A factory cannot return null; GetConverterInternal checked for that.
- Debug.Assert(converter != null);
- }
-
- Type converterTypeToConvert = converter.TypeToConvert;
-
- if (!converterTypeToConvert.IsInSubtypeRelationshipWith(typeToConvert))
- {
- ThrowHelper.ThrowInvalidOperationException_SerializationConverterNotCompatible(converter.GetType(), typeToConvert);
}
return converter;
}
- // This suppression needs to be removed. https://github.com/dotnet/runtime/issues/68878
- [UnconditionalSuppressMessage("AotAnalysis", "IL3050:RequiresDynamicCode", Justification = "The factory constructors are only invoked in the context of reflection serialization code paths " +
- "and are marked RequiresDynamicCode")]
- private JsonConverter GetConverterFromAttribute(JsonConverterAttribute converterAttribute, Type typeToConvert, MemberInfo? memberInfo)
+ internal static void CheckConverterNullabilityIsSameAsPropertyType(JsonConverter converter, Type propertyType)
{
- JsonConverter? converter;
-
- Type declaringType = memberInfo?.DeclaringType ?? typeToConvert;
- Type? converterType = converterAttribute.ConverterType;
- if (converterType == null)
- {
- // Allow the attribute to create the converter.
- converter = converterAttribute.CreateConverter(typeToConvert);
- if (converter == null)
- {
- ThrowHelper.ThrowInvalidOperationException_SerializationConverterOnAttributeNotCompatible(declaringType, memberInfo, typeToConvert);
- }
- }
- else
- {
- ConstructorInfo? ctor = converterType.GetConstructor(Type.EmptyTypes);
- if (!typeof(JsonConverter).IsAssignableFrom(converterType) || ctor == null || !ctor.IsPublic)
- {
- ThrowHelper.ThrowInvalidOperationException_SerializationConverterOnAttributeInvalid(declaringType, memberInfo);
- }
-
- converter = (JsonConverter)Activator.CreateInstance(converterType)!;
- }
-
- Debug.Assert(converter != null);
- if (!converter.CanConvert(typeToConvert))
+ // User has indicated that either:
+ // a) a non-nullable-struct handling converter should handle a nullable struct type or
+ // b) a nullable-struct handling converter should handle a non-nullable struct type.
+ // User should implement a custom converter for the underlying struct and remove the unnecessary CanConvert method override.
+ // The serializer will automatically wrap the custom converter with NullableConverter<T>.
+ //
+ // We also throw to avoid passing an invalid argument to setters for nullable struct properties,
+ // which would cause an InvalidProgramException when the generated IL is invoked.
+ if (propertyType.IsValueType && converter.IsValueType &&
+ (propertyType.IsNullableOfT() ^ converter.TypeToConvert.IsNullableOfT()))
{
- Type? underlyingType = Nullable.GetUnderlyingType(typeToConvert);
- if (underlyingType != null && converter.CanConvert(underlyingType))
- {
- if (converter is JsonConverterFactory converterFactory)
- {
- converter = converterFactory.GetConverterInternal(underlyingType, this);
- }
-
- // Allow nullable handling to forward to the underlying type's converter.
- return NullableConverterFactory.CreateValueConverter(underlyingType, converter);
- }
-
- ThrowHelper.ThrowInvalidOperationException_SerializationConverterOnAttributeNotCompatible(declaringType, memberInfo, typeToConvert);
+ ThrowHelper.ThrowInvalidOperationException_ConverterCanConvertMultipleTypes(propertyType, converter);
}
-
- return converter;
}
}
}
private bool _propertyNameCaseInsensitive;
private bool _writeIndented;
- private bool _isLockedInstance;
+ private volatile bool _isLockedInstance;
/// <summary>
/// Constructs a new <see cref="JsonSerializerOptions"/> instance.
_includeFields = options._includeFields;
_propertyNameCaseInsensitive = options._propertyNameCaseInsensitive;
_writeIndented = options._writeIndented;
- // Preserve backward compatibility with .NET 6
- // This should almost certainly be changed, cf. https://github.com/dotnet/aspnetcore/issues/38720
- _typeInfoResolver = options._typeInfoResolver is JsonSerializerContext ? null : options._typeInfoResolver;
+ _typeInfoResolver = options._typeInfoResolver;
EffectiveMaxDepth = options.EffectiveMaxDepth;
ReferenceHandlingStrategy = options.ReferenceHandlingStrategy;
/// Binds current <see cref="JsonSerializerOptions"/> instance with a new instance of the specified <see cref="Serialization.JsonSerializerContext"/> type.
/// </summary>
/// <typeparam name="TContext">The generic definition of the specified context type.</typeparam>
- /// <remarks>When serializing and deserializing types using the options
+ /// <remarks>
+ /// When serializing and deserializing types using the options
/// instance, metadata for the types will be fetched from the context instance.
/// </remarks>
public void AddContext<TContext>() where TContext : JsonSerializerContext, new()
{
VerifyMutable();
TContext context = new();
- _typeInfoResolver = context;
- _isLockedInstance = true;
- context._options = this;
+ context.Options = this;
}
/// <summary>
- /// Gets or sets JsonTypeInfo resolver.
+ /// Gets or sets a <see cref="JsonTypeInfo"/> contract resolver.
/// </summary>
+ /// <remarks>
+ /// A <see langword="null"/> setting is equivalent to using the reflection-based <see cref="DefaultJsonTypeInfoResolver"/>.
+ /// </remarks>
+ [AllowNull]
public IJsonTypeInfoResolver TypeInfoResolver
{
[RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
[RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
get
{
- return _typeInfoResolver ?? DefaultJsonTypeInfoResolver.RootDefaultInstance();
+ return _typeInfoResolver ??= DefaultJsonTypeInfoResolver.RootDefaultInstance();
}
set
{
VerifyMutable();
-
- if (value is null)
- {
- ThrowHelper.ThrowArgumentNullException(nameof(value));
- }
-
- if (value is JsonSerializerContext ctx)
- {
- if (ctx._options != null && ctx._options != this)
- {
- // TODO evaluate if this is the appropriate behaviour;
- ThrowHelper.ThrowInvalidOperationException_SerializerContextOptionsImmutable();
- }
-
- // Associate options instance with context and lock for further modification
- ctx._options = this;
- _isLockedInstance = true;
- }
-
_typeInfoResolver = value;
}
}
}
}
- internal bool IsInitializedForReflectionSerializer { get; private set; }
- // Effective resolver, populated when enacting reflection-based fallback
- // Should not be taken into account when calculating options equality.
- private IJsonTypeInfoResolver? _effectiveJsonTypeInfoResolver;
+ internal bool IsLockedInstance
+ {
+ get => _isLockedInstance;
+ set
+ {
+ Debug.Assert(value, "cannot unlock options instances");
+ _isLockedInstance = true;
+ }
+ }
/// <summary>
/// Initializes the converters for the reflection-based serializer.
[RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
internal void InitializeForReflectionSerializer()
{
- if (_typeInfoResolver is JsonSerializerContext ctx)
+ if (!_isInitializedForReflectionSerializer)
{
- // .NET 6 backward compatibility; use fallback to reflection serialization
- // TODO: Consider removing this behaviour (needs to be filed as a breaking change).
- _effectiveJsonTypeInfoResolver = JsonTypeInfoResolver.Combine(ctx, DefaultJsonTypeInfoResolver.RootDefaultInstance());
- }
- else
- {
- _typeInfoResolver ??= DefaultJsonTypeInfoResolver.RootDefaultInstance();
- }
+ DefaultJsonTypeInfoResolver defaultResolver = DefaultJsonTypeInfoResolver.RootDefaultInstance();
+ _typeInfoResolver ??= defaultResolver;
+ IsLockedInstance = true;
- if (_cachingContext != null && _cachingContext.Options != this)
- {
- // We're using a shared caching context deriving from a different options instance;
- // for coherence ensure that it has been opted in for reflection-based serialization as well.
- _cachingContext.Options.InitializeForReflectionSerializer();
- }
+ CachingContext? context = GetCachingContext();
+ Debug.Assert(context != null);
+
+ if (context.Options != this)
+ {
+ // We're using a shared caching context deriving from a different options instance;
+ // for coherence ensure that it has been opted in for reflection-based serialization as well.
+ context.Options.InitializeForReflectionSerializer();
+ }
- IsInitializedForReflectionSerializer = true;
+ _isInitializedForReflectionSerializer = true;
+ }
}
- internal bool IsInitializedForMetadataGeneration { get; private set; }
+ private volatile bool _isInitializedForReflectionSerializer;
+
internal void InitializeForMetadataGeneration()
{
- IJsonTypeInfoResolver? resolver = _effectiveJsonTypeInfoResolver ?? _typeInfoResolver;
- if (resolver == null)
+ if (!_isInitializedForMetadataGeneration)
{
- ThrowHelper.ThrowInvalidOperationException_JsonTypeInfoUsedButTypeInfoResolverNotSet();
- }
+ if (_typeInfoResolver is null)
+ {
+ ThrowHelper.ThrowInvalidOperationException_JsonTypeInfoUsedButTypeInfoResolverNotSet();
+ }
- _isLockedInstance = true;
- IsInitializedForMetadataGeneration = true;
+ IsLockedInstance = true;
+ _isInitializedForMetadataGeneration = true;
+ }
}
- private JsonTypeInfo? GetTypeInfoInternal(Type type)
+ private volatile bool _isInitializedForMetadataGeneration;
+
+ private JsonTypeInfo? GetTypeInfoNoCaching(Type type)
{
- IJsonTypeInfoResolver? resolver = _effectiveJsonTypeInfoResolver ?? _typeInfoResolver;
- JsonTypeInfo? info = resolver?.GetTypeInfo(type, this);
+ JsonTypeInfo? info = _typeInfoResolver?.GetTypeInfo(type, this);
if (info != null)
{
_options = options;
}
- protected override bool IsLockedInstance => _options._isLockedInstance;
+ protected override bool IsLockedInstance => _options.IsLockedInstance;
protected override void VerifyMutable() => _options.VerifyMutable();
}
private static JsonSerializerOptions CreateDefaultImmutableInstance()
{
- var options = new JsonSerializerOptions { _isLockedInstance = true };
+ var options = new JsonSerializerOptions { IsLockedInstance = true };
return options;
}
}
/// Creates serialization metadata for a type using a simple converter.
/// </summary>
internal CustomJsonTypeInfo(JsonSerializerOptions options)
- : base(options.GetConverterForType(typeof(T)), options)
+ : base(options.GetConverterFromListOrBuiltInConverter(typeof(T)), options)
{
}
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using System.Text.Json.Reflection;
using System.Text.Json.Serialization.Converters;
namespace System.Text.Json.Serialization.Metadata
converters.Add(converter.TypeToConvert, converter);
}
- internal static JsonConverter GetDefaultConverter(Type typeToConvert)
+ internal static JsonConverter GetBuiltInConverter(Type typeToConvert)
{
if (s_defaultSimpleConverters == null || s_defaultFactoryConverters == null)
{
return s_defaultSimpleConverters.TryGetValue(typeToConvert, out converter);
}
+
+ // This method gets the runtime information for a given type or property.
+ // The runtime information consists of the following:
+ // - class type,
+ // - element type (if the type is a collection),
+ // - the converter (either native or custom), if one exists.
+ [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
+ [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
+ internal static JsonConverter GetConverterForMember(
+ Type typeToConvert,
+ MemberInfo memberInfo,
+ JsonSerializerOptions options,
+ out JsonConverter? customConverter)
+ {
+ Debug.Assert(memberInfo is FieldInfo or PropertyInfo);
+ Debug.Assert(typeToConvert != null);
+
+ JsonConverterAttribute? converterAttribute = memberInfo.GetUniqueCustomAttribute<JsonConverterAttribute>(inherit: false);
+ customConverter = converterAttribute is null ? null : GetConverterFromAttribute(converterAttribute, typeToConvert, memberInfo, options);
+
+ return options.TryGetJsonTypeInfoCached(typeToConvert, out JsonTypeInfo? typeInfo)
+ ? typeInfo.Converter
+ : GetConverterForType(typeToConvert, options);
+ }
+
+ [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
+ [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
+ internal static JsonConverter GetConverterForType(Type typeToConvert, JsonSerializerOptions options)
+ {
+ // Priority 1: Attempt to get custom converter from the Converters list.
+ JsonConverter? converter = options.GetConverterFromList(typeToConvert);
+
+ // Priority 2: Attempt to get converter from [JsonConverter] on the type being converted.
+ if (converter == null)
+ {
+ JsonConverterAttribute? converterAttribute = typeToConvert.GetUniqueCustomAttribute<JsonConverterAttribute>(inherit: false);
+ if (converterAttribute != null)
+ {
+ converter = GetConverterFromAttribute(converterAttribute, typeToConvert: typeToConvert, memberInfo: null, options);
+ }
+ }
+
+ // Priority 3: Fall back to built-in converters and validate result
+ return options.GetCustomOrBuiltInConverter(typeToConvert, converter);
+ }
+
+ [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
+ [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
+ private static JsonConverter GetConverterFromAttribute(JsonConverterAttribute converterAttribute, Type typeToConvert, MemberInfo? memberInfo, JsonSerializerOptions options)
+ {
+ JsonConverter? converter;
+
+ Type declaringType = memberInfo?.DeclaringType ?? typeToConvert;
+ Type? converterType = converterAttribute.ConverterType;
+ if (converterType == null)
+ {
+ // Allow the attribute to create the converter.
+ converter = converterAttribute.CreateConverter(typeToConvert);
+ if (converter == null)
+ {
+ ThrowHelper.ThrowInvalidOperationException_SerializationConverterOnAttributeNotCompatible(declaringType, memberInfo, typeToConvert);
+ }
+ }
+ else
+ {
+ ConstructorInfo? ctor = converterType.GetConstructor(Type.EmptyTypes);
+ if (!typeof(JsonConverter).IsAssignableFrom(converterType) || ctor == null || !ctor.IsPublic)
+ {
+ ThrowHelper.ThrowInvalidOperationException_SerializationConverterOnAttributeInvalid(declaringType, memberInfo);
+ }
+
+ converter = (JsonConverter)Activator.CreateInstance(converterType)!;
+ }
+
+ Debug.Assert(converter != null);
+ if (!converter.CanConvert(typeToConvert))
+ {
+ Type? underlyingType = Nullable.GetUnderlyingType(typeToConvert);
+ if (underlyingType != null && converter.CanConvert(underlyingType))
+ {
+ if (converter is JsonConverterFactory converterFactory)
+ {
+ converter = converterFactory.GetConverterInternal(underlyingType, options);
+ }
+
+ // Allow nullable handling to forward to the underlying type's converter.
+ return NullableConverterFactory.CreateValueConverter(underlyingType, converter);
+ }
+
+ ThrowHelper.ThrowInvalidOperationException_SerializationConverterOnAttributeNotCompatible(declaringType, memberInfo, typeToConvert);
+ }
+
+ return converter;
+ }
}
}
{
Debug.Assert(Options != null);
Debug.Assert(ShouldDeserialize);
- return _jsonTypeInfo ??= Options.GetOrAddJsonTypeInfo(PropertyType);
+ return _jsonTypeInfo ??= Options.GetJsonTypeInfoCached(PropertyType);
}
set
{
Type parameterType = parameterInfo.ParameterType;
DefaultValueHolder holder;
- if (matchingProperty.Options.TryGetJsonTypeInfo(parameterType, out JsonTypeInfo? typeInfo))
+ if (matchingProperty.Options.TryGetJsonTypeInfoCached(parameterType, out JsonTypeInfo? typeInfo))
{
holder = typeInfo.DefaultValueHolder;
}
else
{
// GetOrAddJsonTypeInfo already ensures it's configured.
- _jsonTypeInfo = Options.GetOrAddJsonTypeInfo(PropertyType);
+ _jsonTypeInfo = Options.GetJsonTypeInfoCached(PropertyType);
}
return _jsonTypeInfo;
JsonConverter? customConverter = CustomConverter;
if (customConverter != null)
{
- customConverter = Options.ExpandFactoryConverter(customConverter, PropertyType);
- JsonSerializerOptions.CheckConverterNullabilityIsSameAsPropertyType(customConverter, PropertyType);
+ customConverter = Options.ExpandConverterFactory(customConverter, PropertyType);
}
JsonConverter converter = customConverter ?? DefaultConverterForType ?? Options.GetConverterFromTypeInfo(PropertyType);
{
// GetOrAddJsonTypeInfo already ensures JsonTypeInfo is configured
// also see comment on JsonPropertyInfo.JsonTypeInfo
- _elementTypeInfo = Options.GetOrAddJsonTypeInfo(ElementType);
+ _elementTypeInfo = Options.GetJsonTypeInfoCached(ElementType);
}
}
else
// GetOrAddJsonTypeInfo already ensures JsonTypeInfo is configured
// also see comment on JsonPropertyInfo.JsonTypeInfo
- _keyTypeInfo = Options.GetOrAddJsonTypeInfo(KeyType);
+ _keyTypeInfo = Options.GetJsonTypeInfoCached(KeyType);
}
}
else
internal void EnsureConfigured()
{
+ Debug.Assert(!Monitor.IsEntered(_configureLock), "recursive locking detected.");
+
if (!_isConfigured)
ConfigureLocked();
internal virtual void Configure()
{
Debug.Assert(Monitor.IsEntered(_configureLock), "Configure called directly, use EnsureConfigured which locks this method");
-
- if (!Options.IsInitializedForMetadataGeneration)
- {
- Options.InitializeForMetadataGeneration();
- }
+ Options.InitializeForMetadataGeneration();
PropertyInfoForTypeInfo.EnsureChildOf(this);
PropertyInfoForTypeInfo.EnsureConfigured();
ThrowHelper.ThrowArgumentException_CannotSerializeInvalidType(nameof(propertyType), propertyType, Type, name);
}
- JsonConverter converter = Options.GetConverterForType(propertyType);
+ JsonConverter converter = Options.GetConverterFromListOrBuiltInConverter(propertyType);
JsonPropertyInfo propertyInfo = CreatePropertyUsingReflection(propertyType, converter);
propertyInfo.Name = name;
return new JsonPropertyDictionary<JsonPropertyInfo>(Options.PropertyNameCaseInsensitive, capacity);
}
- // This method gets the runtime information for a given type or property.
- // The runtime information consists of the following:
- // - class type,
- // - element type (if the type is a collection),
- // - the converter (either native or custom), if one exists.
- private protected static JsonConverter GetConverterFromMember(
- Type typeToConvert,
- MemberInfo memberInfo,
- JsonSerializerOptions options,
- out JsonConverter? customConverter)
- {
- Debug.Assert(typeToConvert != null);
- Debug.Assert(!IsInvalidForSerialization(typeToConvert), $"Type `{typeToConvert.FullName}` should already be validated.");
- customConverter = options.GetCustomConverterFromMember(typeToConvert, memberInfo);
- return options.GetConverterForType(typeToConvert);
- }
-
private static JsonParameterInfo CreateConstructorParameter(
JsonParameterInfoValues parameterInfo,
JsonPropertyInfo jsonPropertyInfo,
public Type DerivedType { get; }
public object? TypeDiscriminator { get; }
public JsonTypeInfo GetJsonTypeInfo(JsonSerializerOptions options)
- => _jsonTypeInfo ??= options.GetOrAddJsonTypeInfo(DerivedType);
+ => _jsonTypeInfo ??= options.GetJsonTypeInfoCached(DerivedType);
}
}
}
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text.Json.Reflection;
+using System.Text.Json.Serialization.Converters;
namespace System.Text.Json.Serialization.Metadata
{
[RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
[RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
internal ReflectionJsonTypeInfo(JsonSerializerOptions options)
- : this(options.GetConverterForType(typeof(T)), options)
+ : this(DefaultJsonTypeInfoResolver.GetConverterForType(typeof(T), options), options)
{
}
try
{
- converter = GetConverterFromMember(
+ converter = DefaultJsonTypeInfoResolver.GetConverterForMember(
typeToConvert,
memberInfo,
options,
public void Initialize(Type type, JsonSerializerOptions options, bool supportContinuation)
{
- JsonTypeInfo jsonTypeInfo = options.GetOrAddJsonTypeInfoForRootType(type);
+ JsonTypeInfo jsonTypeInfo = options.GetJsonTypeInfoForRootType(type);
Initialize(jsonTypeInfo, supportContinuation);
}
/// </summary>
public JsonConverter Initialize(Type type, JsonSerializerOptions options, bool supportContinuation, bool supportAsync)
{
- JsonTypeInfo jsonTypeInfo = options.GetOrAddJsonTypeInfoForRootType(type);
+ JsonTypeInfo jsonTypeInfo = options.GetJsonTypeInfoForRootType(type);
return Initialize(jsonTypeInfo, supportContinuation, supportAsync);
}
// if the current element is the same type as the previous element.
if (PolymorphicJsonTypeInfo?.PropertyType != runtimeType)
{
- JsonTypeInfo typeInfo = options.GetOrAddJsonTypeInfo(runtimeType);
+ JsonTypeInfo typeInfo = options.GetJsonTypeInfoCached(runtimeType);
PolymorphicJsonTypeInfo = typeInfo.PropertyInfoForTypeInfo;
}
[DoesNotReturn]
public static void ThrowInvalidOperationException_ResolverTypeNotCompatible(Type requestedType, Type actualType)
{
- throw new InvalidOperationException(SR.Format(SR.ResolverTypeNotCompatible, requestedType, actualType));
+ throw new InvalidOperationException(SR.Format(SR.ResolverTypeNotCompatible, actualType, requestedType));
}
[DoesNotReturn]
// since it can't resolve reflection-based metadata.
Assert.Throws<NotSupportedException>(() => converter.Write(writer, value, options));
Assert.Equal(0, writer.BytesCommitted + writer.BytesPending);
+ options.IncludeFields = false; // options should still be mutable
JsonSerializer.Serialize(42, options);
Assert.NotEqual(0, writer.BytesCommitted + writer.BytesPending);
writer.Reset();
+ Assert.Throws<InvalidOperationException>(() => options.IncludeFields = false);
+
// State change should not leak into unrelated options instances.
var options2 = new JsonSerializerOptions();
options2.AddContext<JsonContext>();
public int Value { get; set; }
public RecursiveType? Next { get; set; }
}
+
+ [Fact]
+ public static void CreateJsonTypeInfo_ClassWithConverterAttribute_ShouldNotResolveConverterAttribute()
+ {
+ JsonTypeInfo jsonTypeInfo = JsonTypeInfo.CreateJsonTypeInfo(typeof(ClassWithConverterAttribute), JsonSerializerOptions.Default);
+ Assert.Equal(typeof(ClassWithConverterAttribute), jsonTypeInfo.Type);
+ Assert.IsNotType<ClassWithConverterAttribute.CustomConverter>(jsonTypeInfo.Converter);
+ }
+
+ [Fact]
+ public static void DefaultJsonTypeInfoResolver_ClassWithConverterAttribute_ShouldResolveConverterAttribute()
+ {
+ var options = JsonSerializerOptions.Default;
+ JsonTypeInfo jsonTypeInfo = options.TypeInfoResolver.GetTypeInfo(typeof(ClassWithConverterAttribute), options);
+ Assert.Equal(typeof(ClassWithConverterAttribute), jsonTypeInfo.Type);
+ Assert.IsType<ClassWithConverterAttribute.CustomConverter>(jsonTypeInfo.Converter);
+ }
+
+ [JsonConverter(typeof(CustomConverter))]
+ public class ClassWithConverterAttribute
+ {
+ public class CustomConverter : JsonConverter<ClassWithConverterAttribute>
+ {
+ public override ClassWithConverterAttribute? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw new NotImplementedException();
+ public override void Write(Utf8JsonWriter writer, ClassWithConverterAttribute value, JsonSerializerOptions options) => throw new NotImplementedException();
+ }
+ }
}
}
CauseInvalidOperationException(() => options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase);
}
+ [Fact]
+ public void PassingImmutableOptionsThrowsException()
+ {
+ JsonSerializerOptions defaultOptions = JsonSerializerOptions.Default;
+ Assert.Throws<InvalidOperationException>(() => new MyJsonContext(defaultOptions));
+ }
+
+ [Fact]
+ public void PassingWrongOptionsInstanceToResolverThrowsException()
+ {
+ JsonSerializerOptions defaultOptions = JsonSerializerOptions.Default;
+ JsonSerializerOptions contextOptions = new();
+ IJsonTypeInfoResolver context = new EmptyContext(contextOptions);
+
+ Assert.IsAssignableFrom<JsonTypeInfo<int>>(context.GetTypeInfo(typeof(int), contextOptions));
+ Assert.IsAssignableFrom<JsonTypeInfo<int>>(context.GetTypeInfo(typeof(int), null));
+ Assert.Throws<InvalidOperationException>(() => context.GetTypeInfo(typeof(int), defaultOptions));
+ }
+
private class MyJsonContext : JsonSerializerContext
{
public MyJsonContext() : base(null) { }
public override JsonTypeInfo? GetTypeInfo(Type type) => throw new NotImplementedException();
protected override JsonSerializerOptions? GeneratedSerializerOptions => null;
}
+
+ private class EmptyContext : JsonSerializerContext
+ {
+ public EmptyContext(JsonSerializerOptions options) : base(options) { }
+ protected override JsonSerializerOptions? GeneratedSerializerOptions => null;
+ public override JsonTypeInfo? GetTypeInfo(Type type) => JsonTypeInfo.CreateJsonTypeInfo(type, Options);
+ }
}
}
using System.Reflection;
using System.Text.Encodings.Web;
using System.Text.Json.Serialization.Metadata;
+using System.Text.Json.Tests;
using System.Text.Unicode;
+using Microsoft.DotNet.RemoteExecutor;
using Xunit;
namespace System.Text.Json.Serialization.Tests
TestIListNonThrowingOperationsWhenMutable(options.Converters, () => new TestConverter());
- // Verify TypeInfoResolver throws on null resolver
- Assert.Throws<ArgumentNullException>(() => options.TypeInfoResolver = null);
-
// Verify default TypeInfoResolver throws
Action<JsonTypeInfo> tiModifier = (ti) => { };
Assert.Throws<InvalidOperationException>(() => (options.TypeInfoResolver as DefaultJsonTypeInfoResolver).Modifiers.Clear());
}
[Fact]
- public static void TypeInfoResolverCannotBeSetAfterContextIsSetThroughTypeInfoResolver()
+ public static void WhenAddingContext_SettingResolverToNullThrowsInvalidOperationException()
+ {
+ var options = new JsonSerializerOptions();
+ options.AddContext<JsonContext>();
+ Assert.Throws<InvalidOperationException>(() => options.TypeInfoResolver = null);
+ }
+
+ [Fact]
+ public static void TypeInfoResolverCanBeSetAfterContextIsSetThroughTypeInfoResolver()
{
var options = new JsonSerializerOptions();
IJsonTypeInfoResolver resolver = new JsonContext();
Assert.Same(resolver, options.TypeInfoResolver);
resolver = new DefaultJsonTypeInfoResolver();
- Assert.Throws<InvalidOperationException>(() => options.TypeInfoResolver = resolver);
+ options.TypeInfoResolver = resolver;
+ Assert.Same(resolver, options.TypeInfoResolver);
}
[Fact]
GenericObjectOrJsonElementConverterTestHelper<JsonElement>("JsonElementConverter", element, "[3]");
}
+ [Fact]
+ public static void Options_JsonSerializerContext_DoesNotFallbackToReflection()
+ {
+ var options = JsonContext.Default.Options;
+ JsonSerializer.Serialize(new WeatherForecastWithPOCOs(), options); // type supported by context should succeed serialization
+
+ var unsupportedValue = new MyClass();
+ Assert.Null(JsonContext.Default.GetTypeInfo(unsupportedValue.GetType()));
+ Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(unsupportedValue, unsupportedValue.GetType(), JsonContext.Default));
+ Assert.Throws<NotSupportedException>(() => JsonSerializer.Serialize(unsupportedValue, options));
+ }
+
+ [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)]
+ [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+ public static void Options_JsonSerializerContext_GetConverter_FallsBackToReflectionConverter()
+ {
+ RemoteExecutor.Invoke(static () =>
+ {
+ JsonContext context = JsonContext.Default;
+ var unsupportedValue = new MyClass();
+
+ // Default converters have not been rooted yet
+ Assert.Null(context.GetTypeInfo(typeof(MyClass)));
+ Assert.Throws<NotSupportedException>(() => context.Options.GetConverter(typeof(MyClass)));
+
+ // Root converters process-wide by calling a Serialize overload accepting options
+ Assert.Throws<NotSupportedException>(() => JsonSerializer.Serialize(unsupportedValue, context.Options));
+
+ // We still can't resolve metadata for MyClass, but we can now resolve a converter using the rooted converters.
+ Assert.Null(context.GetTypeInfo(typeof(MyClass)));
+ Assert.IsAssignableFrom<JsonConverter<MyClass>>(context.Options.GetConverter(typeof(MyClass)));
+
+ }).Dispose();
+ }
+
+ [Fact]
+ public static void Options_JsonSerializerContext_Combine_FallbackToReflection()
+ {
+ var options = new JsonSerializerOptions
+ {
+ TypeInfoResolver = JsonTypeInfoResolver.Combine(JsonContext.Default, new DefaultJsonTypeInfoResolver())
+ };
+
+ var value = new MyClass();
+ string json = JsonSerializer.Serialize(value, options);
+ JsonTestHelper.AssertJsonEqual("""{"Value":null,"Thing":null}""", json);
+ }
+
private static void GenericObjectOrJsonElementConverterTestHelper<T>(string converterName, object objectValue, string stringValue)
{
var options = new JsonSerializerOptions();
}
[Fact]
+ public static void CopyConstructor_CopiesJsonSerializerContext()
+ {
+ JsonSerializerOptions options = new JsonSerializerOptions();
+ options.AddContext<JsonContext>();
+ JsonContext original = Assert.IsType<JsonContext>(options.TypeInfoResolver);
+
+ // copy constructor copies the JsonSerializerContext
+ var newOptions = new JsonSerializerOptions(options);
+ Assert.Same(original, newOptions.TypeInfoResolver);
+
+ // resolving metadata returns metadata tied to the new options
+ JsonTypeInfo typeInfo = newOptions.TypeInfoResolver.GetTypeInfo(typeof(int), newOptions);
+ Assert.Same(typeInfo.Options, newOptions);
+
+ // it is possible to reset the resolver
+ newOptions.TypeInfoResolver = null;
+ Assert.IsType<DefaultJsonTypeInfoResolver>(newOptions.TypeInfoResolver);
+ }
+
+ [Fact]
public static void CopyConstructor_NullInput()
{
ArgumentNullException ex = Assert.Throws<ArgumentNullException>(() => new JsonSerializerOptions(null));