diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index 32832fc919c9e9..fd80bccebe97b8 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -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;"); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs index bfbcc0e9156e15..7ff0d80739af79 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs @@ -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)] diff --git a/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs index 190166ef19c3f8..b53f2d0d09d8de 100644 --- a/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs @@ -82,6 +82,30 @@ public void TreatNullObliviousAsNonNullable_True_MarksReferenceTypesAsNonNullabl Assert.Equal(expectedType, (string)schema["type"]!); } + [Fact] + public void GetOnlyProperties_DoNotUseSetterNullabilityInSchema() + { + 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))] @@ -262,6 +286,18 @@ public void LegacySchemaExporter_CanAccessReflectedMembers() record PocoWithProperty(int Value); + public sealed class PocoWithGetOnlyProperties + { + public IEnumerable 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; diff --git a/src/libraries/System.Text.Json/tests/Common/NullableAnnotationsTests.cs b/src/libraries/System.Text.Json/tests/Common/NullableAnnotationsTests.cs index f0e120986ae179..8e796080dd8cc5 100644 --- a/src/libraries/System.Text.Json/tests/Common/NullableAnnotationsTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/NullableAnnotationsTests.cs @@ -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 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 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; } @@ -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; + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/JsonSchemaExporterTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/JsonSchemaExporterTests.cs index 55d8ccb8ce1771..5291b4ab724c62 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/JsonSchemaExporterTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/JsonSchemaExporterTests.cs @@ -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 diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/NullableAnnotationsTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/NullableAnnotationsTests.cs index 663229c132e6ef..9208f4fa7768f9 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/NullableAnnotationsTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/NullableAnnotationsTests.cs @@ -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 { } } @@ -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 { } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/Baselines/ConstructorWithDefaultValues/net462/MyContext.Config.g.cs.txt b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/Baselines/ConstructorWithDefaultValues/net462/MyContext.Config.g.cs.txt index b93da1d8b5db63..31a080dab1a7f7 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/Baselines/ConstructorWithDefaultValues/net462/MyContext.Config.g.cs.txt +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/Baselines/ConstructorWithDefaultValues/net462/MyContext.Config.g.cs.txt @@ -69,6 +69,7 @@ namespace TestApp }; properties[0] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo(options, info0); + properties[0].IsSetNullable = false; var info1 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues { @@ -89,6 +90,7 @@ namespace TestApp }; properties[1] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo(options, info1); + properties[1].IsSetNullable = false; var info2 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues { @@ -109,6 +111,7 @@ namespace TestApp }; properties[2] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo(options, info2); + properties[2].IsSetNullable = false; return properties; } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/Baselines/ConstructorWithDefaultValues/netcoreapp/MyContext.Config.g.cs.txt b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/Baselines/ConstructorWithDefaultValues/netcoreapp/MyContext.Config.g.cs.txt index b93da1d8b5db63..31a080dab1a7f7 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/Baselines/ConstructorWithDefaultValues/netcoreapp/MyContext.Config.g.cs.txt +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/Baselines/ConstructorWithDefaultValues/netcoreapp/MyContext.Config.g.cs.txt @@ -69,6 +69,7 @@ namespace TestApp }; properties[0] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo(options, info0); + properties[0].IsSetNullable = false; var info1 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues { @@ -89,6 +90,7 @@ namespace TestApp }; properties[1] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo(options, info1); + properties[1].IsSetNullable = false; var info2 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues { @@ -109,6 +111,7 @@ namespace TestApp }; properties[2] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo(options, info2); + properties[2].IsSetNullable = false; return properties; } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/Baselines/ParameterizedConstructor/net462/MyContext.Point.g.cs.txt b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/Baselines/ParameterizedConstructor/net462/MyContext.Point.g.cs.txt index 2ccbc399144db2..858d84ce3e7e64 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/Baselines/ParameterizedConstructor/net462/MyContext.Point.g.cs.txt +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/Baselines/ParameterizedConstructor/net462/MyContext.Point.g.cs.txt @@ -69,6 +69,7 @@ namespace TestApp }; properties[0] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo(options, info0); + properties[0].IsSetNullable = false; var info1 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues { @@ -89,6 +90,7 @@ namespace TestApp }; properties[1] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo(options, info1); + properties[1].IsSetNullable = false; return properties; } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/Baselines/ParameterizedConstructor/netcoreapp/MyContext.Point.g.cs.txt b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/Baselines/ParameterizedConstructor/netcoreapp/MyContext.Point.g.cs.txt index 2ccbc399144db2..858d84ce3e7e64 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/Baselines/ParameterizedConstructor/netcoreapp/MyContext.Point.g.cs.txt +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/Baselines/ParameterizedConstructor/netcoreapp/MyContext.Point.g.cs.txt @@ -69,6 +69,7 @@ namespace TestApp }; properties[0] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo(options, info0); + properties[0].IsSetNullable = false; var info1 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues { @@ -89,6 +90,7 @@ namespace TestApp }; properties[1] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo(options, info1); + properties[1].IsSetNullable = false; return properties; } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/Baselines/UnsafeAccessors_InaccessibleConstructor/net462/MyContext.InaccessibleCtor.g.cs.txt b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/Baselines/UnsafeAccessors_InaccessibleConstructor/net462/MyContext.InaccessibleCtor.g.cs.txt index 8b96296b270f08..c44d748e8fba1d 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/Baselines/UnsafeAccessors_InaccessibleConstructor/net462/MyContext.InaccessibleCtor.g.cs.txt +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/Baselines/UnsafeAccessors_InaccessibleConstructor/net462/MyContext.InaccessibleCtor.g.cs.txt @@ -69,6 +69,7 @@ namespace TestApp }; properties[0] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo(options, info0); + properties[0].IsSetNullable = false; var info1 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues { @@ -89,6 +90,7 @@ namespace TestApp }; properties[1] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo(options, info1); + properties[1].IsSetNullable = false; return properties; } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/Baselines/UnsafeAccessors_InaccessibleConstructor/netcoreapp/MyContext.InaccessibleCtor.g.cs.txt b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/Baselines/UnsafeAccessors_InaccessibleConstructor/netcoreapp/MyContext.InaccessibleCtor.g.cs.txt index f65cf101aee548..570a5d4f1089fa 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/Baselines/UnsafeAccessors_InaccessibleConstructor/netcoreapp/MyContext.InaccessibleCtor.g.cs.txt +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/Baselines/UnsafeAccessors_InaccessibleConstructor/netcoreapp/MyContext.InaccessibleCtor.g.cs.txt @@ -69,6 +69,7 @@ namespace TestApp }; properties[0] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo(options, info0); + properties[0].IsSetNullable = false; var info1 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues { @@ -89,6 +90,7 @@ namespace TestApp }; properties[1] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo(options, info1); + properties[1].IsSetNullable = false; return properties; }