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

Fix JsonIgnore behavior on init-only properties in source generation mode #112924

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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 @@ -1514,6 +1514,11 @@ private void ProcessMember(
continue;
}

if (property.DefaultIgnoreCondition == JsonIgnoreCondition.Always && !property.IsRequired)
{
continue;
}

if ((property.IsRequired && !constructorSetsRequiredMembers) || property.IsInitOnlySetter)
{
if (!(memberInitializerNames ??= new()).Add(property.MemberName))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,29 @@ public virtual async Task NonPublicInitOnlySetter_With_JsonInclude(Type type)
Assert.Equal(@"{""MyInt"":1}", await Serializer.SerializeWrapper(obj));
}

[Theory]
[InlineData(typeof(Class_WithIgnoredInitOnlyProperty_WithDefaultValue))]
[InlineData(typeof(Record_WithIgnoredPropertyInCtor_WithDefaultValue))]
public async Task InitOnlySetter_With_JsonIgnoreAlways_AndWith_DefaultValue(Type type)
{
object obj = await Serializer.DeserializeWrapper(@"{""MyInt"":1}", type);
Assert.Equal(42, (int)type.GetProperty("MyInt").GetValue(obj));
}

[Fact]
public async Task JsonIgnoreOnInitOnlyProperty()
{
// Regression test for https://github.com/dotnet/runtime/issues/101877

RecordWithIgnoredNestedInitOnlyProperty.InnerRecord inner = new(42, "Baz");
RecordWithIgnoredNestedInitOnlyProperty value = new RecordWithIgnoredNestedInitOnlyProperty(inner);
string json = await Serializer.SerializeWrapper(value);
Assert.Equal("""{"foo":42}""", json);

RecordWithIgnoredNestedInitOnlyProperty? deserializedValue = await Serializer.DeserializeWrapper<RecordWithIgnoredNestedInitOnlyProperty>(json);
Assert.Equal(value, deserializedValue);
}

[Fact]
public async Task NullableStructWithInitOnlyProperty()
{
Expand Down Expand Up @@ -132,5 +155,28 @@ public class Class_PropertyWith_ProtectedInitOnlySetter_WithAttribute
[JsonInclude]
public int MyInt { get; protected init; }
}

public class Class_WithIgnoredInitOnlyProperty_WithDefaultValue
Copy link
Member

@eiriktsarpalis eiriktsarpalis Feb 26, 2025

Choose a reason for hiding this comment

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

Why is having a default value important in this context? Shouldn't we also be testing for the non-default case?

{
[JsonIgnore(Condition = JsonIgnoreCondition.Always)]
public int MyInt { get; init; } = 42;
}

public record Record_WithIgnoredPropertyInCtor_WithDefaultValue(
[property: JsonIgnore] int MyInt = 42);

public record RecordWithIgnoredNestedInitOnlyProperty(
[property: JsonIgnore] RecordWithIgnoredNestedInitOnlyProperty.InnerRecord Other)
{
[JsonConstructor]
public RecordWithIgnoredNestedInitOnlyProperty(int foo)
: this(new InnerRecord(foo, "Baz"))
{
}

[JsonPropertyName("foo")] public int Foo => Other.Foo;

public record InnerRecord(int Foo, string Bar);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ public override async Task ClassWithIgnoredAndPrivateMembers_DoesNotIncludeIgnor
[JsonSerializable(typeof(ClassWithStructProperty_IgnoreConditionWhenWritingDefault))]
[JsonSerializable(typeof(ClassWithMissingObjectProperty))]
[JsonSerializable(typeof(ClassWithInitOnlyProperty))]
[JsonSerializable(typeof(Class_WithIgnoredInitOnlyProperty_WithDefaultValue))]
[JsonSerializable(typeof(Record_WithIgnoredPropertyInCtor_WithDefaultValue))]
[JsonSerializable(typeof(RecordWithIgnoredNestedInitOnlyProperty))]
[JsonSerializable(typeof(StructWithInitOnlyProperty))]
[JsonSerializable(typeof(StructWithInitOnlyProperty?))]
[JsonSerializable(typeof(ClassWithCustomNamedInitOnlyProperty))]
Expand Down Expand Up @@ -473,6 +476,9 @@ partial class DefaultContextWithGlobalIgnoreSetting : JsonSerializerContext;
[JsonSerializable(typeof(ClassWithStructProperty_IgnoreConditionWhenWritingDefault))]
[JsonSerializable(typeof(ClassWithMissingObjectProperty))]
[JsonSerializable(typeof(ClassWithInitOnlyProperty))]
[JsonSerializable(typeof(Class_WithIgnoredInitOnlyProperty_WithDefaultValue))]
[JsonSerializable(typeof(Record_WithIgnoredPropertyInCtor_WithDefaultValue))]
[JsonSerializable(typeof(RecordWithIgnoredNestedInitOnlyProperty))]
[JsonSerializable(typeof(StructWithInitOnlyProperty))]
[JsonSerializable(typeof(StructWithInitOnlyProperty?))]
[JsonSerializable(typeof(ClassWithCustomNamedInitOnlyProperty))]
Expand Down