Skip to content

Fix JsonSerializerOptions.GetConverter recreating built-in converters #65799

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

Closed
Show file tree
Hide file tree
Changes from all commits
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 @@ -159,7 +159,10 @@ internal JsonConverter GetConverterFromMember(Type? parentClassType, Type proper
[RequiresUnreferencedCode("Getting a converter for a type may require reflection which depends on unreferenced code.")]
public JsonConverter GetConverter(Type typeToConvert!!)
{
RootBuiltInConverters();
if (!IsInitializedForReflectionSerializer)
{
InitializeForReflectionSerializer();
}
Comment on lines +162 to +165
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think calling this method should root the reflection-based metadata gathering code paths, potentially masking future bugs with source-gen.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. What is the distinction between rooting the reflection-based metadata gathering paths and rooting the built-in converter factories that depend on said reflection-based metadata gathering paths? Under the current setup an options instance not configured for reflection will eagerly return converters that do require reflection.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on your remarks on this PR #51897 I'm guessing that this is only meant as an entrypoint for reflection-based serialization anyways.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Under the current setup an options instance not configured for reflection will eagerly return converters that do require reflection.

Hmm what do you mean? The source generator doesn't use this method GetConverter(Type), so we left the reflection to avoid breaking folks that used it pre .NET 6.0.

return GetConverterInternal(typeToConvert);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ private void EnsurePushCapacity()
public JsonConverter Initialize(Type type, JsonSerializerOptions options, bool supportContinuation)
{
JsonTypeInfo jsonTypeInfo = options.GetOrAddJsonTypeInfoForRootType(type);
Debug.Assert(options == jsonTypeInfo.Options);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion has been invalidated by #64646, however it wasn't being caught by our test suite until now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume it would still be true if the new comparer introduced in #64646 were used.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

== still checks for reference equality, the comparer is internal implementation detail when initializing the caches.

return Initialize(jsonTypeInfo, supportContinuation);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,8 @@ public class Point_2D : ITestClass
[JsonConstructor]
public Point_2D(int x, int y) => (X, Y) = (x, y);

public Point_2D() : this(0, 0) { }

public void Initialize() { }

public void Verify()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.IO;
using Microsoft.DotNet.RemoteExecutor;
using Xunit;

namespace System.Text.Json.Serialization.Tests
Expand Down Expand Up @@ -161,19 +163,29 @@ public static void VerifyObjectConverterWithPreservedReferences()
Assert.Equal(true, obj);
}

[Fact]
[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
public static void GetConverterRootsBuiltInConverters()
{
JsonSerializerOptions options = new();
RunTest<DateTime>();
RunTest<Point_2D>();

void RunTest<TConverterReturn>()
RemoteExecutor.Invoke(static () =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this feels a bit like a unit test rather than functional test because it tests an implementation detail. I'll suggest to move it to unit tests.

{
JsonConverter converter = options.GetConverter(typeof(TConverterReturn));
Assert.NotNull(converter);
Assert.True(converter is JsonConverter<TConverterReturn>);
}
JsonSerializerOptions options = new();
RunTest<DateTime>();
RunTest<Point_2D>();

void RunTest<T>() where T : new()
{
JsonConverter<T> converter = options.GetConverter(typeof(T)) as JsonConverter<T>;
Assert.NotNull(converter);

// Should be possible to call into converters that require
// reflection-based metadata
T value = new();
using var writer = new Utf8JsonWriter(new MemoryStream());
converter.Write(writer, value, options);
writer.Flush();
Assert.True(writer.BytesCommitted > 0);
}
}).Dispose();
}

[Fact]
Expand Down