-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Avoid name collision for covariant and hidden properties (#63443) #63823
Conversation
Tagging subscribers to this area: @dotnet/area-system-text-json Issue Detailsnull
|
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs
Show resolved
Hide resolved
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs
Outdated
Show resolved
Hide resolved
var derived = new NotIgnoredPropertyBase_IgnoredPropertyDerived { Id = "Test" }; | ||
|
||
string json = JsonSerializer.Serialize(derived, DefaultContext.NotIgnoredPropertyBase_IgnoredPropertyDerived); | ||
JsonTestHelper.AssertJsonEqual("{\"Id\":null}", json); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this should be expected, considering that for JsonIgnoreForHiddenProperties_NotIgnoredBase_NotIgnoredDerived
we're not showing both Id
s my gut tells me I should not be seeing any Id
s here either. We should be consistent for hidden non-ignored properties I think. cc: @layomia @eiriktsarpalis @steveharter
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure either, what are the semantics in the reflection-based for that scenario?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried it out without JsonSerializerContext with the following code:
using System.Text.Json;
using System.Text.Json.Serialization;
NotIgnoredBase b1 = new NotIgnoredBase();
NotIgnoredBase_NotIgnoredDerived b2 = new NotIgnoredBase_NotIgnoredDerived();
NotIgnoredBase b3 = new NotIgnoredBase_NotIgnoredDerived();
NotIgnoredBase_IgnoredDerived b4 = new NotIgnoredBase_IgnoredDerived();
NotIgnoredBase b5 = new NotIgnoredBase_IgnoredDerived();
b1.Property = "1"; // Property on base
b2.Property = "2";
b3.Property = "3"; // Property on base
b4.Property = "4";
b5.Property = "5"; // Property on base
Console.WriteLine(JsonSerializer.Serialize(b1));
Console.WriteLine(JsonSerializer.Serialize(b2));
Console.WriteLine(JsonSerializer.Serialize(b3));
Console.WriteLine(JsonSerializer.Serialize(b4));
Console.WriteLine(JsonSerializer.Serialize(b5));
Console.WriteLine();
Console.WriteLine(JsonSerializer.Serialize(b1, typeof(NotIgnoredBase)));
Console.WriteLine(JsonSerializer.Serialize(b2, typeof(NotIgnoredBase)));
Console.WriteLine(JsonSerializer.Serialize(b2, typeof(NotIgnoredBase_NotIgnoredDerived)));
Console.WriteLine(JsonSerializer.Serialize(b3, typeof(NotIgnoredBase)));
Console.WriteLine(JsonSerializer.Serialize(b4, typeof(NotIgnoredBase)));
Console.WriteLine(JsonSerializer.Serialize(b4, typeof(NotIgnoredBase_IgnoredDerived)));
Console.WriteLine(JsonSerializer.Serialize(b5, typeof(NotIgnoredBase)));
public class NotIgnoredBase
{
public string Property { get; set; } = nameof(NotIgnoredBase);
}
public class NotIgnoredBase_NotIgnoredDerived : NotIgnoredBase
{
public new string Property { get; set; } = nameof(NotIgnoredBase_NotIgnoredDerived);
}
public class NotIgnoredBase_IgnoredDerived : NotIgnoredBase
{
[JsonIgnore]
public new string Property { get; set; } = nameof(NotIgnoredBase_IgnoredDerived);
}
Results:
{"Property":"1"}
{"Property":"2"}
{"Property":"3"}
{"Property":"NotIgnoredBase"}
{"Property":"5"}
{"Property":"1"}
{"Property":"NotIgnoredBase"}
{"Property":"2"}
{"Property":"3"}
{"Property":"NotIgnoredBase"}
{"Property":"NotIgnoredBase"}
{"Property":"5"}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It turns out the reflection-based serializer also throws an exception if the properties are ignored both on the base and the derived class.
IgnoredBase_IgnoredDerived b4 = new IgnoredBase_IgnoredDerived();
Console.WriteLine(JsonSerializer.Serialize(b4));
public class IgnoredBase
{
[JsonIgnore]
public string Property { get; set; } = nameof(IgnoredBase);
}
public class IgnoredBase_IgnoredDerived : IgnoredBase
{
[JsonIgnore]
public new string Property { get; set; } = nameof(IgnoredBase_IgnoredDerived);
}
Exception:
System.ArgumentException: An item with the same key has already been added. Key: Property
at System.ThrowHelper.ThrowAddingDuplicateWithKeyArgumentException[T](T key)
at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
at System.Text.Json.Serialization.Metadata.JsonTypeInfo.CacheMember(JsonPropertyInfo jsonPropertyInfo, JsonPropertyDictionary`1 propertyCache, Dictionary`2& ignoredMembers) in /_/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs:line 409
at System.Text.Json.Serialization.Metadata.JsonTypeInfo.CacheMember(Type declaringType, Type memberType, MemberInfo memberInfo, Boolean isVirtual, Nullable`1 typeNumberHandling, Boolean& propertyOrderSpecified, Dictionary`2& ignoredMembers) in /_/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs:line 373
at System.Text.Json.Serialization.Metadata.JsonTypeInfo..ctor(Type type, JsonConverter converter, Type runtimeType, JsonSerializerOptions options) in /_/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs:line 224
at System.Text.Json.JsonSerializerOptions.<InitializeForReflectionSerializer>g__CreateJsonTypeInfo|112_0(Type type, JsonSerializerOptions options) in /_/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs:line 596
at System.Text.Json.JsonSerializerOptions.GetClassFromContextOrCreate(Type type) in /_/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs:line 625
at System.Text.Json.JsonSerializerOptions.GetOrAddClass(Type type) in /_/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs:line 605
at System.Text.Json.JsonSerializer.GetTypeInfo(JsonSerializerOptions options, Type runtimeType) in /_/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Helpers.cs:line 26
at System.Text.Json.JsonSerializer.Serialize(Object value, Type inputType, JsonSerializerOptions options) in /_/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.String.cs:line 63
at Program.<Main>$(String[] args) in C:\src\JsonTest\JsonTest\Program.cs:line 54
This error would be also fixed by the PR:
https://github.com/dotnet/runtime/pull/63823/files#diff-8d841bf86f2a9da83758276bd17f55d002ee5b55f58b72da658df351a7c22bc0L412-R412
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Full code to compare the source generator and reflection-based output:
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
{
NotIgnoredBase b1 = new NotIgnoredBase();
NotIgnoredBase_NotIgnoredDerived b2 = new NotIgnoredBase_NotIgnoredDerived();
NotIgnoredBase b3 = new NotIgnoredBase_NotIgnoredDerived();
NotIgnoredBase_IgnoredDerived b4 = new NotIgnoredBase_IgnoredDerived();
NotIgnoredBase b5 = new NotIgnoredBase_IgnoredDerived();
b1.Property = "1"; // Property on base
b2.Property = "2";
b3.Property = "3"; // Property on base
b4.Property = "4";
b5.Property = "5"; // Property on base
Console.WriteLine("NotIgnoredBase, Reflection-based:");
Console.WriteLine(JsonSerializer.Serialize(b1, typeof(NotIgnoredBase)));
Console.WriteLine(JsonSerializer.Serialize(b2, typeof(NotIgnoredBase)));
Console.WriteLine(JsonSerializer.Serialize(b2, typeof(NotIgnoredBase_NotIgnoredDerived)));
Console.WriteLine(JsonSerializer.Serialize(b3, typeof(NotIgnoredBase)));
Console.WriteLine(JsonSerializer.Serialize(b4, typeof(NotIgnoredBase)));
Console.WriteLine(JsonSerializer.Serialize(b4, typeof(NotIgnoredBase_IgnoredDerived)));
Console.WriteLine(JsonSerializer.Serialize(b5, typeof(NotIgnoredBase)));
Console.WriteLine();
Console.WriteLine("NotIgnoredBase, Source generator-based:");
Console.WriteLine(JsonSerializer.Serialize(b1, JsonContext.Default.NotIgnoredBase));
Console.WriteLine(JsonSerializer.Serialize(b2, JsonContext.Default.NotIgnoredBase));
Console.WriteLine(JsonSerializer.Serialize(b2, JsonContext.Default.NotIgnoredBase_NotIgnoredDerived));
Console.WriteLine(JsonSerializer.Serialize(b3, JsonContext.Default.NotIgnoredBase));
Console.WriteLine(JsonSerializer.Serialize(b4, JsonContext.Default.NotIgnoredBase));
Console.WriteLine(JsonSerializer.Serialize(b4, JsonContext.Default.NotIgnoredBase_IgnoredDerived));
Console.WriteLine(JsonSerializer.Serialize(b5, JsonContext.Default.NotIgnoredBase));
}
{
IgnoredBase b1 = new IgnoredBase();
IgnoredBase_NotIgnoredDerived b2 = new IgnoredBase_NotIgnoredDerived();
IgnoredBase b3 = new IgnoredBase_NotIgnoredDerived();
IgnoredBase_IgnoredDerived b4 = new IgnoredBase_IgnoredDerived();
IgnoredBase b5 = new IgnoredBase_IgnoredDerived();
b1.Property = "1"; // Property on base
b2.Property = "2";
b3.Property = "3"; // Property on base
b4.Property = "4";
b5.Property = "5"; // Property on base
Console.WriteLine();
Console.WriteLine("IgnoredBase, Reflection-based:");
Console.WriteLine(JsonSerializer.Serialize(b1, typeof(IgnoredBase)));
Console.WriteLine(JsonSerializer.Serialize(b2, typeof(IgnoredBase)));
Console.WriteLine(JsonSerializer.Serialize(b2, typeof(IgnoredBase_NotIgnoredDerived)));
Console.WriteLine(JsonSerializer.Serialize(b3, typeof(IgnoredBase)));
Console.WriteLine(JsonSerializer.Serialize(b4, typeof(IgnoredBase)));
Console.WriteLine("Serialization error"/*JsonSerializer.Serialize(b4, typeof(IgnoredBase_IgnoredDerived))*/);
Console.WriteLine(JsonSerializer.Serialize(b5, typeof(IgnoredBase)));
Console.WriteLine();
Console.WriteLine("IgnoredBase, Source generator-based:");
Console.WriteLine(JsonSerializer.Serialize(b1, JsonContext.Default.IgnoredBase));
Console.WriteLine(JsonSerializer.Serialize(b2, JsonContext.Default.IgnoredBase));
Console.WriteLine(JsonSerializer.Serialize(b2, JsonContext.Default.IgnoredBase_NotIgnoredDerived));
Console.WriteLine(JsonSerializer.Serialize(b3, JsonContext.Default.IgnoredBase));
Console.WriteLine(JsonSerializer.Serialize(b4, JsonContext.Default.IgnoredBase));
Console.WriteLine("Source generator error"/*JsonSerializer.Serialize(b4, JsonContext.Default.IgnoredBase_IgnoredDerived)*/);
Console.WriteLine(JsonSerializer.Serialize(b5, JsonContext.Default.IgnoredBase));
Console.WriteLine();
}
public class NotIgnoredBase
{
public string Property { get; set; } = nameof(NotIgnoredBase);
}
public class NotIgnoredBase_NotIgnoredDerived : NotIgnoredBase
{
public new string Property { get; set; } = nameof(NotIgnoredBase_NotIgnoredDerived);
}
public class NotIgnoredBase_IgnoredDerived : NotIgnoredBase
{
[JsonIgnore]
public new string Property { get; set; } = nameof(NotIgnoredBase_IgnoredDerived);
}
public class IgnoredBase
{
[JsonIgnore]
public string Property { get; set; } = nameof(IgnoredBase);
}
public class IgnoredBase_NotIgnoredDerived : IgnoredBase
{
public new string Property { get; set; } = nameof(IgnoredBase_NotIgnoredDerived);
}
public class IgnoredBase_IgnoredDerived : IgnoredBase
{
[JsonIgnore]
public new string Property { get; set; } = nameof(IgnoredBase_IgnoredDerived);
}
[JsonSerializable(typeof(NotIgnoredBase))]
[JsonSerializable(typeof(NotIgnoredBase_NotIgnoredDerived))]
[JsonSerializable(typeof(NotIgnoredBase_IgnoredDerived))]
[JsonSerializable(typeof(IgnoredBase))]
[JsonSerializable(typeof(IgnoredBase_NotIgnoredDerived))]
//[JsonSerializable(typeof(IgnoredBase_IgnoredDerived))]
public partial class JsonContext : JsonSerializerContext
{ }
Output:
NotIgnoredBase, Reflection-based:
{"Property":"1"}
{"Property":"NotIgnoredBase"}
{"Property":"2"}
{"Property":"3"}
{"Property":"NotIgnoredBase"}
{"Property":"NotIgnoredBase"}
{"Property":"5"}
NotIgnoredBase, Source generator-based:
{"Property":"1"}
{"Property":"NotIgnoredBase"}
{"Property":"2"}
{"Property":"3"}
{"Property":"NotIgnoredBase"}
{"Property":"NotIgnoredBase"}
{"Property":"5"}
IgnoredBase, Reflection-based:
{}
{}
{"Property":"2"}
{}
{}
Serialization error
{}
IgnoredBase, Source generator-based:
{}
{}
{"Property":"2"}
{}
{}
Source generator error
{}
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs
Outdated
Show resolved
Hide resolved
@@ -1115,7 +1115,7 @@ private Type GetCompatibleGenericBaseClass(Type type, Type baseType) | |||
if (propGenSpec.DefaultIgnoreCondition == JsonIgnoreCondition.Always) | |||
{ | |||
ignoredMembers ??= new Dictionary<string, PropertyGenerationSpec>(); | |||
ignoredMembers.Add(propGenSpec.ClrName, propGenSpec); | |||
ignoredMembers.TryAdd(propGenSpec.ClrName, propGenSpec); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should really we be ignoring the result of TryAdd
if it returns false? Is there a possibility that PropertyGenerationSpec
varies significantly between the base and derived properties and the order of traversal can substantially impact how source code is being generated? cc @layomia
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If for example we only want to take the spec for the base property into account, we might want to introduce so that only the base member is inserted into the dictionary (using Add
instead of TryAdd
).
@@ -882,5 +882,121 @@ public virtual void NullableStruct() | |||
Assert.Equal("Jane", person.Value.FirstName); | |||
Assert.Equal("Doe", person.Value.LastName); | |||
} | |||
|
|||
[Fact] | |||
public void JsonIgnoreForCovariantProperties() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't tell just by looking at the diff, but we should make sure that these tests are exercised both by the fast-path and metadata-based source generator.
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs
Outdated
Show resolved
Hide resolved
…ion/Metadata/JsonTypeInfo.cs Co-authored-by: Eirik Tsarpalis <eirik.tsarpalis@gmail.com>
Co-authored-by: Eirik Tsarpalis <eirik.tsarpalis@gmail.com>
else | ||
{ | ||
string json = JsonSerializer.Serialize(derived, DefaultContext.CovariantBaseNotIgnored_CovariantDerivedIgnored); | ||
JsonTestHelper.AssertJsonEqual("{\"Id\":\"CovariantBaseNotIgnored_CovariantDerivedIgnored\"}", json); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so this effectively means that [JsonIgnore]
on the derived overriden member is effectively useless? I'd expect either ignore ({}
) or error here
else | ||
{ | ||
string json = JsonSerializer.Serialize(derived, DefaultContext.CovariantBaseNotIgnored_CovariantDerivedGenericIgnoredString); | ||
JsonTestHelper.AssertJsonEqual("{\"Id\":\"CovariantBaseNotIgnored_CovariantDerivedGenericIgnored\"}", json); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same as with the one above
else | ||
{ | ||
string json = JsonSerializer.Serialize(derived, DefaultContext.CovariantBaseIgnored_CovariantDerivedNotIgnored); | ||
JsonTestHelper.AssertJsonEqual("{\"Id\":\"CovariantBaseIgnored_CovariantDerivedNotIgnored\"}", json); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here I would always expect this to not be present because base ignores it
@Kloizdena @eiriktsarpalis @layomia I think we should write down doc/spec on how JsonIgnore should work and based on the exploration and expectations I'd adjust the implementation here. Current design (at least first couple of cases) work opposite to what my expectations are. My personal opinion is that I'd rather invest time in specing this out and then eventually fix implementation according to that spec even if that means breaking changes. |
This pull request has been automatically marked |
This pull request will now be closed since it had been marked |
Fixes #63443