Skip to content
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 @@ -932,7 +932,7 @@ property.DefaultIgnoreCondition is JsonIgnoreCondition.Always &&
{
writer.WriteLine($"properties[{i}].IsGetNullable = false;");
}
if (property.IsSetterNonNullableAnnotation)
if (property.IsSetterNonNullableAnnotation || property.IsReadOnly)
{
writer.WriteLine($"properties[{i}].IsSetNullable = false;");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -952,19 +952,21 @@ private static void DeterminePropertyNullability(JsonPropertyInfo propertyInfo,
return;
}

NullabilityInfo nullabilityInfo;
if (propertyInfo.MemberType is MemberTypes.Property)
{
nullabilityInfo = nullabilityCtx.Create((PropertyInfo)memberInfo);
var reflectionPropertyInfo = (PropertyInfo)memberInfo;
NullabilityInfo nullabilityInfo = nullabilityCtx.Create(reflectionPropertyInfo);
propertyInfo.IsGetNullable = reflectionPropertyInfo.GetMethod is not null && nullabilityInfo.ReadState is not NullabilityState.NotNull;
propertyInfo.IsSetNullable = reflectionPropertyInfo.SetMethod is not null && nullabilityInfo.WriteState is not NullabilityState.NotNull;
}
else
{
Debug.Assert(propertyInfo.MemberType is MemberTypes.Field);
nullabilityInfo = nullabilityCtx.Create((FieldInfo)memberInfo);
var fieldInfo = (FieldInfo)memberInfo;
NullabilityInfo nullabilityInfo = nullabilityCtx.Create(fieldInfo);
propertyInfo.IsGetNullable = nullabilityInfo.ReadState is not NullabilityState.NotNull;
propertyInfo.IsSetNullable = !fieldInfo.IsInitOnly && nullabilityInfo.WriteState is not NullabilityState.NotNull;
}

propertyInfo.IsGetNullable = nullabilityInfo.ReadState is not NullabilityState.NotNull;
propertyInfo.IsSetNullable = nullabilityInfo.WriteState is not NullabilityState.NotNull;
}

[RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,30 @@ public void TreatNullObliviousAsNonNullable_True_MarksReferenceTypesAsNonNullabl
Assert.Equal(expectedType, (string)schema["type"]!);
}

[Fact]
public void GetOnlyProperties_DoNotUseSetterNullabilityInSchema()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot There existing tests that assert the full JSON output. This test should do the same. See JsonSchemaExporterTests.TestTypes.cs‎

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated in 11fcebc2: the test now validates the full schema output (using AssertValidJsonSchema) instead of checking individual nodes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

TODO: I need to make sure we have tests to exercise both the reflection path and the source generator path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in the latest commit. GetOnlyProperties_DoNotUseSetterNullabilityInSchema runs under both paths: the abstract base class test in JsonSchemaExporterTests.cs is exercised for the reflection path by System.Text.Json.Tests and for the source gen path by the context in tests/System.Text.Json.SourceGeneration.Tests/Serialization/JsonSchemaExporterTests.cs (line 131, [JsonSerializable(typeof(PocoWithGetOnlyProperties))] already registered there). The ClassWithReadonlyMembers nullability tests likewise run under both paths.

{
JsonNode schema = Serializer.DefaultOptions.GetJsonSchemaAsNode(typeof(PocoWithGetOnlyProperties));
const string ExpectedJsonSchema = """
{
"type": ["object", "null"],
"properties": {
"Values": {
"type": "array",
"items": { "type": ["string", "null"] }
},
"SingleValueGetOnly": { "type": "string" },
"NullableGetOnly": { "type": ["string", "null"] },
"SingleValueGetSet": { "type": "string" },
"NonNullableReadonlyField": { "type": "string" },
"NullableReadonlyField": { "type": ["string", "null"] }
}
}
""";

AssertValidJsonSchema(typeof(PocoWithGetOnlyProperties), ExpectedJsonSchema, schema);
}

[Theory]
[InlineData(typeof(Type))]
[InlineData(typeof(MethodInfo))]
Expand Down Expand Up @@ -262,6 +286,18 @@ public void LegacySchemaExporter_CanAccessReflectedMembers()

record PocoWithProperty(int Value);

public sealed class PocoWithGetOnlyProperties
{
public IEnumerable<string> Values => [];
public string SingleValueGetOnly { get; } = "value";
public string? NullableGetOnly { get; }
public string SingleValueGetSet { get; set; } = "value";
[JsonInclude]
public readonly string NonNullableReadonlyField = "value";
[JsonInclude]
public readonly string? NullableReadonlyField;
}

[JsonSerializable(typeof(PocoWithProperty))]
partial class PocoWithPropertyContext : JsonSerializerContext;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,44 @@ public async Task WriteNotNullPropertiesWithNullIgnoreConditions_Succeeds()
Assert.Equal("{}", json);
}

[Theory]
[MemberData(nameof(GetReadonlyMembers))]
public void IsSetNullable_ReadonlyMember_IsFalse(Type type, string propertyName)
{
JsonTypeInfo typeInfo = Serializer.GetTypeInfo(type, s_optionsWithIgnoredNullability);
JsonPropertyInfo propertyInfo = typeInfo.Properties.Single(p => p.Name == propertyName);
Assert.False(propertyInfo.IsSetNullable);
}

[Theory]
[MemberData(nameof(GetReadonlyMembersWithExpectedGetNullability))]
public void IsGetNullable_ReadonlyMember_MatchesAnnotation(Type type, string propertyName, bool expectedIsGetNullable)
{
JsonTypeInfo typeInfo = Serializer.GetTypeInfo(type, s_optionsWithIgnoredNullability);
JsonPropertyInfo propertyInfo = typeInfo.Properties.Single(p => p.Name == propertyName);
Assert.Equal(expectedIsGetNullable, propertyInfo.IsGetNullable);
}

public static IEnumerable<object[]> GetReadonlyMembers()
{
yield return Wrap(typeof(ClassWithReadonlyMembers), nameof(ClassWithReadonlyMembers.NonNullableGetOnly));
yield return Wrap(typeof(ClassWithReadonlyMembers), nameof(ClassWithReadonlyMembers.NullableGetOnly));
yield return Wrap(typeof(ClassWithReadonlyMembers), nameof(ClassWithReadonlyMembers.NonNullableReadonlyField));
yield return Wrap(typeof(ClassWithReadonlyMembers), nameof(ClassWithReadonlyMembers.NullableReadonlyField));

static object[] Wrap(Type type, string propertyName) => [type, propertyName];
}

public static IEnumerable<object[]> GetReadonlyMembersWithExpectedGetNullability()
{
yield return Wrap(typeof(ClassWithReadonlyMembers), nameof(ClassWithReadonlyMembers.NonNullableGetOnly), false);
yield return Wrap(typeof(ClassWithReadonlyMembers), nameof(ClassWithReadonlyMembers.NullableGetOnly), true);
yield return Wrap(typeof(ClassWithReadonlyMembers), nameof(ClassWithReadonlyMembers.NonNullableReadonlyField), false);
yield return Wrap(typeof(ClassWithReadonlyMembers), nameof(ClassWithReadonlyMembers.NullableReadonlyField), true);

static object[] Wrap(Type type, string propertyName, bool isGetNullable) => [type, propertyName, isGetNullable];
}

public class NotNullablePropertyClass
{
public string Property { get; set; }
Expand Down Expand Up @@ -793,5 +831,15 @@ public class ClassWithNonNullableRequiredProperty
{
public required string Property { get; set; }
}

public class ClassWithReadonlyMembers
{
public string NonNullableGetOnly { get; }
public string? NullableGetOnly { get; }
[JsonInclude]
public readonly string NonNullableReadonlyField = "value";
[JsonInclude]
public readonly string? NullableReadonlyField;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ public sealed partial class JsonSchemaExporterTests_SourceGen()
[JsonSerializable(typeof(ClassWithPropertyNameRequiringFragmentEncoding))]
[JsonSerializable(typeof(ClassWithOptionalObjectParameter))]
[JsonSerializable(typeof(ClassWithPropertiesUsingCustomConverters))]
[JsonSerializable(typeof(JsonSchemaExporterTests.PocoWithGetOnlyProperties))]
#pragma warning disable CS0612 // Type or member is obsolete
[JsonSerializable(typeof(MyObsoleteType))]
#pragma warning restore CS0612 // Type or member is obsolete
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ protected NullableAnnotationsTests_Metadata(JsonSerializerWrapper serializer)
[JsonSerializable(typeof(ClassWithNonNullableInitProperty))]
[JsonSerializable(typeof(ClassWithNullableRequiredProperty))]
[JsonSerializable(typeof(ClassWithNonNullableRequiredProperty))]
[JsonSerializable(typeof(ClassWithReadonlyMembers))]
internal sealed partial class NullableAnnotationsTestsContext_Metadata
: JsonSerializerContext { }
}
Expand Down Expand Up @@ -136,6 +137,7 @@ protected NullableAnnotationsTests_Default(JsonSerializerWrapper serializer)
[JsonSerializable(typeof(ClassWithNonNullableInitProperty))]
[JsonSerializable(typeof(ClassWithNullableRequiredProperty))]
[JsonSerializable(typeof(ClassWithNonNullableRequiredProperty))]
[JsonSerializable(typeof(ClassWithReadonlyMembers))]
internal sealed partial class NullableAnnotationsTestsContext_Default
: JsonSerializerContext
{ }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ namespace TestApp
};

properties[0] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<string>(options, info0);
properties[0].IsSetNullable = false;

var info1 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<int>
{
Expand All @@ -89,6 +90,7 @@ namespace TestApp
};

properties[1] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<int>(options, info1);
properties[1].IsSetNullable = false;

var info2 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<bool>
{
Expand All @@ -109,6 +111,7 @@ namespace TestApp
};

properties[2] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<bool>(options, info2);
properties[2].IsSetNullable = false;

return properties;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ namespace TestApp
};

properties[0] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<string>(options, info0);
properties[0].IsSetNullable = false;

var info1 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<int>
{
Expand All @@ -89,6 +90,7 @@ namespace TestApp
};

properties[1] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<int>(options, info1);
properties[1].IsSetNullable = false;

var info2 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<bool>
{
Expand All @@ -109,6 +111,7 @@ namespace TestApp
};

properties[2] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<bool>(options, info2);
properties[2].IsSetNullable = false;

return properties;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ namespace TestApp
};

properties[0] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<int>(options, info0);
properties[0].IsSetNullable = false;

var info1 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<int>
{
Expand All @@ -89,6 +90,7 @@ namespace TestApp
};

properties[1] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<int>(options, info1);
properties[1].IsSetNullable = false;

return properties;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ namespace TestApp
};

properties[0] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<int>(options, info0);
properties[0].IsSetNullable = false;

var info1 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<int>
{
Expand All @@ -89,6 +90,7 @@ namespace TestApp
};

properties[1] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<int>(options, info1);
properties[1].IsSetNullable = false;

return properties;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ namespace TestApp
};

properties[0] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<string>(options, info0);
properties[0].IsSetNullable = false;

var info1 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<int>
{
Expand All @@ -89,6 +90,7 @@ namespace TestApp
};

properties[1] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<int>(options, info1);
properties[1].IsSetNullable = false;

return properties;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ namespace TestApp
};

properties[0] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<string>(options, info0);
properties[0].IsSetNullable = false;

var info1 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<int>
{
Expand All @@ -89,6 +90,7 @@ namespace TestApp
};

properties[1] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<int>(options, info1);
properties[1].IsSetNullable = false;

return properties;
}
Expand Down
Loading