Skip to content

Commit

Permalink
Ensure JsonSerializerOptions always returns a JsonTypeInfo for `objec…
Browse files Browse the repository at this point in the history
…t`. (#87093)

* Ensure JsonSerializerOptions always returns a JsonTypeInfo for `object`.

* Address feedback.
  • Loading branch information
eiriktsarpalis authored Jun 5, 2023
1 parent b27814c commit fe98008
Show file tree
Hide file tree
Showing 9 changed files with 127 additions and 93 deletions.
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 DefaultObjectConverter : ObjectConverter
{
public DefaultObjectConverter()
{
// 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 DefaultObjectConverter();
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)));
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>("DefaultObjectConverter", new object(), "{}");
JsonElement element = JsonDocument.Parse("[3]").RootElement;
GenericObjectOrJsonElementConverterTestHelper<JsonElement>("JsonElementConverter", element, "[3]");
}
Expand Down

0 comments on commit fe98008

Please sign in to comment.