Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure JsonSerializerOptions always returns a JsonTypeInfo for object. #87093

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,32 @@

using System.Diagnostics;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization.Metadata;

namespace System.Text.Json.Serialization.Converters
{
internal sealed class ObjectConverter : JsonConverter<object?>
internal abstract class ObjectConverter : JsonConverter<object?>
{
private protected override ConverterStrategy GetDefaultConverterStrategy() => ConverterStrategy.Object;

public ObjectConverter()
{
CanBePolymorphic = true;
// JsonElement/JsonNode parsing does not support async; force read ahead for now.
RequiresReadAhead = true;
}

public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
public sealed override object ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (options.UnknownTypeHandling == JsonUnknownTypeHandling.JsonElement)
{
return JsonElement.ParseValue(ref reader);
}
ThrowHelper.ThrowNotSupportedException_DictionaryKeyTypeNotSupported(TypeToConvert, this);
return null!;
}

Debug.Assert(options.UnknownTypeHandling == JsonUnknownTypeHandling.JsonNode);
return JsonNodeConverter.Instance.Read(ref reader, typeToConvert, options);
internal sealed override object ReadAsPropertyNameCore(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
ThrowHelper.ThrowNotSupportedException_DictionaryKeyTypeNotSupported(TypeToConvert, this);
return null!;
}

public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerOptions options)
public sealed override void Write(Utf8JsonWriter writer, object? value, JsonSerializerOptions options)
{
if (value is null)
{
Expand All @@ -40,6 +40,73 @@ public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerO
writer.WriteEndObject();
}

public sealed override void WriteAsPropertyName(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
{
WriteAsPropertyNameCore(writer, value, options, isWritingExtensionDataProperty: false);
}

internal sealed override void WriteAsPropertyNameCore(Utf8JsonWriter writer, object value, JsonSerializerOptions options, bool isWritingExtensionDataProperty)
{
if (value is null)
{
ThrowHelper.ThrowArgumentNullException(nameof(value));
}

Type runtimeType = value.GetType();
if (runtimeType == TypeToConvert)
{
ThrowHelper.ThrowNotSupportedException_DictionaryKeyTypeNotSupported(runtimeType, this);
}

JsonConverter runtimeConverter = options.GetConverterInternal(runtimeType);
runtimeConverter.WriteAsPropertyNameCoreAsObject(writer, value, options, isWritingExtensionDataProperty);
}
}

/// <summary>
/// Defines an object converter that only supports (polymorphic) serialization but not deserialization.
/// This is done to avoid rooting dependencies to JsonNode/JsonElement necessary to drive object deserialization.
/// Source generator users need to explicitly declare support for object so that the derived converter gets used.
/// </summary>
internal sealed class SlimObjectConverter : ObjectConverter
{
// Keep track of the originating resolver so that the converter surfaces
// an accurate error message whenever deserialization is attempted.
private readonly IJsonTypeInfoResolver _originatingResolver;

public SlimObjectConverter(IJsonTypeInfoResolver originatingResolver)
=> _originatingResolver = originatingResolver;

public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
ThrowHelper.ThrowNotSupportedException_NoMetadataForType(typeToConvert, _originatingResolver);
return null;
}
}

/// <summary>
/// Defines an object converter that supports deserialization via JsonElement/JsonNode representations.
/// Used as the default in reflection or if object is declared in the JsonSerializerContext type graph.
/// </summary>
internal sealed class DeserializingObjectConverter : ObjectConverter
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
{
public DeserializingObjectConverter()
{
// JsonElement/JsonNode parsing does not support async; force read ahead for now.
RequiresReadAhead = true;
}

public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (options.UnknownTypeHandling == JsonUnknownTypeHandling.JsonElement)
{
return JsonElement.ParseValue(ref reader);
}

Debug.Assert(options.UnknownTypeHandling == JsonUnknownTypeHandling.JsonNode);
return JsonNodeConverter.Instance.Read(ref reader, typeToConvert, options);
}

internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, scoped ref ReadStack state, out object? value)
{
object? referenceValue;
Expand Down Expand Up @@ -78,64 +145,5 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert,

return true;
}

public override object ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
ThrowHelper.ThrowNotSupportedException_DictionaryKeyTypeNotSupported(TypeToConvert, this);
return null!;
}

internal override object ReadAsPropertyNameCore(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
ThrowHelper.ThrowNotSupportedException_DictionaryKeyTypeNotSupported(TypeToConvert, this);
return null!;
}

public override void WriteAsPropertyName(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
{
WriteAsPropertyNameCore(writer, value, options, isWritingExtensionDataProperty: false);
}

internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, object value, JsonSerializerOptions options, bool isWritingExtensionDataProperty)
{
if (value is null)
{
ThrowHelper.ThrowArgumentNullException(nameof(value));
}

Type runtimeType = value.GetType();
JsonConverter runtimeConverter = options.GetConverterInternal(runtimeType);
if (runtimeConverter == this)
{
ThrowHelper.ThrowNotSupportedException_DictionaryKeyTypeNotSupported(runtimeType, this);
}

runtimeConverter.WriteAsPropertyNameCoreAsObject(writer, value, options, isWritingExtensionDataProperty);
}
}

/// <summary>
/// A placeholder ObjectConverter used for driving object root value
/// serialization only and does not root JsonNode/JsonDocument.
/// </summary>
internal sealed class ObjectConverterSlim : JsonConverter<object?>
{
private protected override ConverterStrategy GetDefaultConverterStrategy() => ConverterStrategy.Object;

public ObjectConverterSlim()
{
CanBePolymorphic = true;
}

public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
Debug.Fail("Converter should only be used to drive root-level object serialization.");
return null;
}

public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerOptions options)
{
Debug.Fail("Converter should only be used to drive root-level object serialization.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali
{
// Special case object converters since they don't
// require the expensive ReadStack.Push()/Pop() operations.
Debug.Assert(this is ObjectConverter or ObjectConverterSlim);
Debug.Assert(this is ObjectConverter);
success = OnTryRead(ref reader, typeToConvert, options, ref state, out value);
Debug.Assert(success);
isPopulatedValue = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,22 +183,7 @@ internal JsonTypeInfo ObjectTypeInfo
get
{
Debug.Assert(IsReadOnly);
return _objectTypeInfo ??= GetObjectTypeInfo(this);

static JsonTypeInfo GetObjectTypeInfo(JsonSerializerOptions options)
{
JsonTypeInfo? typeInfo = options.GetTypeInfoInternal(JsonTypeInfo.ObjectType, ensureNotNull: null);
if (typeInfo is null)
{
// If the user-supplied resolver does not provide a JsonTypeInfo<object>,
// use a placeholder value to drive root-level boxed value serialization.
var converter = new ObjectConverterSlim();
typeInfo = new JsonTypeInfo<object>(converter, options);
typeInfo.EnsureConfigured();
}

return typeInfo;
}
return _objectTypeInfo ??= GetTypeInfoInternal(JsonTypeInfo.ObjectType);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Text.Encodings.Web;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Converters;
using System.Text.Json.Serialization.Metadata;
using System.Threading;

Expand Down Expand Up @@ -756,7 +757,13 @@ internal void ConfigureForJsonSerializer()

private JsonTypeInfo? GetTypeInfoNoCaching(Type type)
{
JsonTypeInfo? info = (_effectiveJsonTypeInfoResolver ?? _typeInfoResolver)?.GetTypeInfo(type, this);
IJsonTypeInfoResolver? resolver = _effectiveJsonTypeInfoResolver ?? _typeInfoResolver;
if (resolver is null)
{
return null;
}

JsonTypeInfo? info = resolver.GetTypeInfo(type, this);

if (info != null)
{
Expand All @@ -770,6 +777,18 @@ internal void ConfigureForJsonSerializer()
ThrowHelper.ThrowInvalidOperationException_ResolverTypeInfoOptionsNotCompatible();
}
}
else
{
Debug.Assert(_effectiveJsonTypeInfoResolver is null, "an effective resolver always returns metadata");

if (type == JsonTypeInfo.ObjectType)
{
// If the resolver does not provide a JsonTypeInfo<object> instance, fill
// with the serialization-only converter to enable polymorphic serialization.
var converter = new SlimObjectConverter(resolver);
info = new JsonTypeInfo<object>(converter, this);
}
}

return info;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ public static partial class JsonMetadataServices
/// Returns a <see cref="JsonConverter{T}"/> instance that converts <see cref="object"/> values.
/// </summary>
/// <remarks>This API is for use by the output of the System.Text.Json source generator and should not be called directly.</remarks>
public static JsonConverter<object?> ObjectConverter => s_objectConverter ??= new ObjectConverter();
public static JsonConverter<object?> ObjectConverter => s_objectConverter ??= new DeserializingObjectConverter();
private static JsonConverter<object?>? s_objectConverter;

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1287,7 +1287,7 @@ private static JsonTypeInfoKind GetTypeInfoKind(Type type, JsonConverter convert
if (type == typeof(object) && converter.CanBePolymorphic)
{
// System.Object is polymorphic and will not respect Properties
Debug.Assert(converter is ObjectConverter or ObjectConverterSlim);
Debug.Assert(converter is ObjectConverter);
return JsonTypeInfoKind.None;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,18 @@ public static async Task SupportsBoxedRootLevelValues()
PersonJsonContext context = PersonJsonContext.Default;
object person = new Person("John", "Smith");
string expectedJson = """{"firstName":"John","lastName":"Smith"}""";
// Sanity check -- context does not specify object metadata
Assert.Null(context.GetTypeInfo(typeof(object)));
// Sanity check -- context resolver does not specify object metadata
Assert.Null(((IJsonTypeInfoResolver)context).GetTypeInfo(typeof(object), new()));

string json = JsonSerializer.Serialize(person, context.Options);
Assert.Equal(expectedJson, json);

json = JsonSerializer.Serialize(person, typeof(object), context);
Assert.Equal(expectedJson, json);

json = JsonSerializer.Serialize(person, context.GetTypeInfo(typeof(object)));
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
Assert.NotNull(context.GetTypeInfo(typeof(object)));

var stream = new Utf8MemoryStream();
await JsonSerializer.SerializeAsync(stream, person, context.Options);
Assert.Equal(expectedJson, stream.AsString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,5 +158,21 @@ private static IList<IJsonTypeInfoResolver> GetAndValidateCombinedResolvers(IJso

return list;
}

[Fact]
public static void NullResolver_ReturnsObjectMetadata()
{
var options = new JsonSerializerOptions();
var resolver = new NullResolver();
Assert.Null(resolver.GetTypeInfo(typeof(object), options));

options.TypeInfoResolver = resolver;
Assert.IsAssignableFrom<JsonTypeInfo<object>>(options.GetTypeInfo(typeof(object)));
}

public sealed class NullResolver : IJsonTypeInfoResolver
{
public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) => null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ public static void JsonEncodedTextStringsCustomAllowAll(string message, string e
[Fact]
public static void Options_GetConverterForObjectJsonElement_GivesCorrectConverter()
{
GenericObjectOrJsonElementConverterTestHelper<object>("ObjectConverter", new object(), "{}");
GenericObjectOrJsonElementConverterTestHelper<object>("DeserializingObjectConverter", new object(), "{}");
JsonElement element = JsonDocument.Parse("[3]").RootElement;
GenericObjectOrJsonElementConverterTestHelper<JsonElement>("JsonElementConverter", element, "[3]");
}
Expand Down