From f23bb31343498f5c71e001f77402f33da5f0fa72 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:37:58 +0000 Subject: [PATCH 01/15] Initial plan From c18be4cacca19d80b9cf612fca662ebeb2480b20 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:18:31 +0000 Subject: [PATCH 02/15] Fix STJ schema nullability for get-only properties Co-authored-by: Youssef1313 <31348972+Youssef1313@users.noreply.github.com> --- .../Text/Json/Schema/JsonSchemaExporter.cs | 4 +++- .../tests/Common/JsonSchemaExporterTests.cs | 18 ++++++++++++++++++ .../Serialization/JsonSchemaExporterTests.cs | 1 + 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporter.cs index 00c39aa3ef2821..a77625c734549d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporter.cs @@ -447,7 +447,9 @@ bool IsNullableSchema(JsonSchemaExporterOptions options) if (propertyInfo is not null) { - return propertyInfo.IsGetNullable || propertyInfo.IsSetNullable; + return propertyInfo.IsGetNullable || + propertyInfo is { HasSetter: true, IsSetNullable: true } || + propertyInfo is { AssociatedParameter: not null, IsSetNullable: true }; } if (typeInfo.IsNullable) diff --git a/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs index 190166ef19c3f8..f26d4861c41011 100644 --- a/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs @@ -82,6 +82,17 @@ public void TreatNullObliviousAsNonNullable_True_MarksReferenceTypesAsNonNullabl Assert.Equal(expectedType, (string)schema["type"]!); } + [Fact] + public void GetOnlyProperties_DoNotUseSetterNullabilityInSchema() + { + JsonNode schema = Serializer.DefaultOptions.GetJsonSchemaAsNode(typeof(PocoWithGetOnlyProperties)); + JsonNode properties = schema["properties"]!; + + Assert.Equal("array", (string)properties["Values"]!["type"]!); + Assert.Equal("string", (string)properties["SingleValueGetOnly"]!["type"]!); + Assert.Equal("string", (string)properties["SingleValueGetSet"]!["type"]!); + } + [Theory] [InlineData(typeof(Type))] [InlineData(typeof(MethodInfo))] @@ -262,6 +273,13 @@ public void LegacySchemaExporter_CanAccessReflectedMembers() record PocoWithProperty(int Value); + public sealed class PocoWithGetOnlyProperties + { + public IEnumerable Values => []; + public string SingleValueGetOnly { get; } = "value"; + public string SingleValueGetSet { get; set; } = "value"; + } + [JsonSerializable(typeof(PocoWithProperty))] partial class PocoWithPropertyContext : JsonSerializerContext; 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 From 1053f1833e04e77f7aa1e700647b357b67d5f32b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:28:46 +0000 Subject: [PATCH 03/15] Refine schema nullability check for constructor-bound properties Co-authored-by: Youssef1313 <31348972+Youssef1313@users.noreply.github.com> --- .../src/System/Text/Json/Schema/JsonSchemaExporter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporter.cs index a77625c734549d..161a54ec0b3d29 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporter.cs @@ -448,8 +448,8 @@ bool IsNullableSchema(JsonSchemaExporterOptions options) if (propertyInfo is not null) { return propertyInfo.IsGetNullable || - propertyInfo is { HasSetter: true, IsSetNullable: true } || - propertyInfo is { AssociatedParameter: not null, IsSetNullable: true }; + (propertyInfo.IsSetNullable && + (propertyInfo.HasSetter || propertyInfo.AssociatedParameter?.IsNullable is true)); } if (typeInfo.IsNullable) From 6d138ac78b1cc5cb5a05f80529ec5bcd0088015f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:31:31 +0000 Subject: [PATCH 04/15] Finalize progress: STJ get-only nullability fix complete Co-authored-by: Youssef1313 <31348972+Youssef1313@users.noreply.github.com> --- .../System.Text.Json/tests/Common/JsonSchemaExporterTests.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs index f26d4861c41011..a0bd88f84c6eee 100644 --- a/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs @@ -87,8 +87,11 @@ public void GetOnlyProperties_DoNotUseSetterNullabilityInSchema() { JsonNode schema = Serializer.DefaultOptions.GetJsonSchemaAsNode(typeof(PocoWithGetOnlyProperties)); JsonNode properties = schema["properties"]!; + JsonNode valuesType = properties["Values"]!["type"]!; - Assert.Equal("array", (string)properties["Values"]!["type"]!); + Assert.IsNotType(valuesType); + Assert.Equal("array", (string)valuesType); + Assert.NotNull(properties["Values"]!["items"]); Assert.Equal("string", (string)properties["SingleValueGetOnly"]!["type"]!); Assert.Equal("string", (string)properties["SingleValueGetSet"]!["type"]!); } From 11fcebc27be3cc6384318544b195d8044bcefa62 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 21:17:15 +0000 Subject: [PATCH 05/15] Use full schema assertion in get-only properties test Co-authored-by: Youssef1313 <31348972+Youssef1313@users.noreply.github.com> --- .../tests/Common/JsonSchemaExporterTests.cs | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs index a0bd88f84c6eee..81add6d60bb534 100644 --- a/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs @@ -86,14 +86,21 @@ public void TreatNullObliviousAsNonNullable_True_MarksReferenceTypesAsNonNullabl public void GetOnlyProperties_DoNotUseSetterNullabilityInSchema() { JsonNode schema = Serializer.DefaultOptions.GetJsonSchemaAsNode(typeof(PocoWithGetOnlyProperties)); - JsonNode properties = schema["properties"]!; - JsonNode valuesType = properties["Values"]!["type"]!; - - Assert.IsNotType(valuesType); - Assert.Equal("array", (string)valuesType); - Assert.NotNull(properties["Values"]!["items"]); - Assert.Equal("string", (string)properties["SingleValueGetOnly"]!["type"]!); - Assert.Equal("string", (string)properties["SingleValueGetSet"]!["type"]!); + const string ExpectedJsonSchema = """ + { + "type": ["object", "null"], + "properties": { + "Values": { + "type": "array", + "items": { "type": ["string", "null"] } + }, + "SingleValueGetOnly": { "type": "string" }, + "SingleValueGetSet": { "type": "string" } + } + } + """; + + AssertValidJsonSchema(typeof(PocoWithGetOnlyProperties), ExpectedJsonSchema, schema); } [Theory] From 734fde85debcebf292dd7e903c115eacf2bcb934 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 22:33:52 +0000 Subject: [PATCH 06/15] Treat IsSetNullable as false when setter is absent Co-authored-by: Youssef1313 <31348972+Youssef1313@users.noreply.github.com> --- .../Metadata/DefaultJsonTypeInfoResolver.Helpers.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 6264eca84c06c7..0ecc89ab854818 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 @@ -895,7 +895,9 @@ private static void DeterminePropertyNullability(JsonPropertyInfo propertyInfo, } propertyInfo.IsGetNullable = nullabilityInfo.ReadState is not NullabilityState.NotNull; - propertyInfo.IsSetNullable = nullabilityInfo.WriteState is not NullabilityState.NotNull; + propertyInfo.IsSetNullable = + nullabilityInfo.WriteState is not NullabilityState.NotNull && + (memberInfo is not PropertyInfo property || property.SetMethod is not null); } [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] From de51772f4228602037663359bfa8f56a75b3d39b Mon Sep 17 00:00:00 2001 From: Youssef Fahmy Date: Fri, 31 Jul 2026 06:01:25 +0200 Subject: [PATCH 07/15] Simplify --- .../Metadata/DefaultJsonTypeInfoResolver.Helpers.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 0ecc89ab854818..398d7b22684af0 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 @@ -894,10 +894,8 @@ private static void DeterminePropertyNullability(JsonPropertyInfo propertyInfo, nullabilityInfo = nullabilityCtx.Create((FieldInfo)memberInfo); } - propertyInfo.IsGetNullable = nullabilityInfo.ReadState is not NullabilityState.NotNull; - propertyInfo.IsSetNullable = - nullabilityInfo.WriteState is not NullabilityState.NotNull && - (memberInfo is not PropertyInfo property || property.SetMethod is not null); + propertyInfo.IsGetNullable = nullabilityInfo.ReadState is NullabilityState.Nullable; + propertyInfo.IsSetNullable = nullabilityInfo.WriteState is NullabilityState.Nullable; } [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] From 1e8d64fb26acf4a4064e467cf0ccd6cd6f185b69 Mon Sep 17 00:00:00 2001 From: Youssef Fahmy Date: Fri, 31 Jul 2026 06:02:09 +0200 Subject: [PATCH 08/15] Update JsonSchemaExporter.cs --- .../src/System/Text/Json/Schema/JsonSchemaExporter.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporter.cs index 161a54ec0b3d29..00c39aa3ef2821 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporter.cs @@ -447,9 +447,7 @@ bool IsNullableSchema(JsonSchemaExporterOptions options) if (propertyInfo is not null) { - return propertyInfo.IsGetNullable || - (propertyInfo.IsSetNullable && - (propertyInfo.HasSetter || propertyInfo.AssociatedParameter?.IsNullable is true)); + return propertyInfo.IsGetNullable || propertyInfo.IsSetNullable; } if (typeInfo.IsNullable) From b1235a07e33d17458afb5a381c6e8c079e40c0b3 Mon Sep 17 00:00:00 2001 From: Youssef Fahmy Date: Fri, 31 Jul 2026 06:14:32 +0200 Subject: [PATCH 09/15] Update DefaultJsonTypeInfoResolver.Helpers.cs --- .../DefaultJsonTypeInfoResolver.Helpers.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) 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 398d7b22684af0..0d61c7eeb9693d 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 @@ -883,19 +883,22 @@ private static void DeterminePropertyNullability(JsonPropertyInfo propertyInfo, return; } - NullabilityInfo nullabilityInfo; + bool isGetNullable; + bool isSetNullable; 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); + NullabilityInfo nullabilityInfo = nullabilityCtx.Create((FieldInfo)memberInfo); + propertyInfo.IsGetNullable = nullabilityInfo.ReadState is not NullabilityState.NotNull; + propertyInfo.IsSetNullable = nullabilityInfo.WriteState is not NullabilityState.NotNull; } - - propertyInfo.IsGetNullable = nullabilityInfo.ReadState is NullabilityState.Nullable; - propertyInfo.IsSetNullable = nullabilityInfo.WriteState is NullabilityState.Nullable; } [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] From 01771f6e36547e0bd34d2fc4da385f898fc5706a Mon Sep 17 00:00:00 2001 From: Youssef Fahmy Date: Fri, 31 Jul 2026 06:19:33 +0200 Subject: [PATCH 10/15] Update DefaultJsonTypeInfoResolver.Helpers.cs --- .../Metadata/DefaultJsonTypeInfoResolver.Helpers.cs | 2 -- 1 file changed, 2 deletions(-) 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 0d61c7eeb9693d..c679f835baccfe 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 @@ -883,8 +883,6 @@ private static void DeterminePropertyNullability(JsonPropertyInfo propertyInfo, return; } - bool isGetNullable; - bool isSetNullable; if (propertyInfo.MemberType is MemberTypes.Property) { var reflectionPropertyInfo = (PropertyInfo)memberInfo; From f30973d2af884294ecbc2a10100bec679281d3e8 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Fri, 31 Jul 2026 13:04:42 +0200 Subject: [PATCH 11/15] Fix source gen --- .../System.Text.Json/gen/JsonSourceGenerator.Emitter.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index a6d9bbf360961a..ed815fc3223531 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -920,7 +920,8 @@ property.DefaultIgnoreCondition is JsonIgnoreCondition.Always && { writer.WriteLine($"properties[{i}].IsGetNullable = false;"); } - if (property.IsSetterNonNullableAnnotation) +q + if (property.IsSetterNonNullableAnnotation || property.IsReadOnly) { writer.WriteLine($"properties[{i}].IsSetNullable = false;"); } From 9eefdc86e9aa350dec055c8984ff4fd83f1e23d6 Mon Sep 17 00:00:00 2001 From: Youssef Fahmy Date: Fri, 31 Jul 2026 13:11:47 +0200 Subject: [PATCH 12/15] Fix Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../System.Text.Json/gen/JsonSourceGenerator.Emitter.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index ed815fc3223531..f07209ec741f18 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -920,7 +920,6 @@ property.DefaultIgnoreCondition is JsonIgnoreCondition.Always && { writer.WriteLine($"properties[{i}].IsGetNullable = false;"); } -q if (property.IsSetterNonNullableAnnotation || property.IsReadOnly) { writer.WriteLine($"properties[{i}].IsSetNullable = false;"); From 1c8b9bc92c95111be9624e70ff23e70b70f35ec4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Jul 2026 11:47:46 +0000 Subject: [PATCH 13/15] Add tests for readonly members: schema exporter, JsonPropertyInfo.IsSetNullable, and source gen coverage Co-authored-by: Youssef1313 <31348972+Youssef1313@users.noreply.github.com> --- .../DefaultJsonTypeInfoResolver.Helpers.cs | 5 +- .../tests/Common/JsonSchemaExporterTests.cs | 10 +++- .../tests/Common/NullableAnnotationsTests.cs | 48 +++++++++++++++++++ .../Serialization/NullableAnnotationsTests.cs | 2 + 4 files changed, 62 insertions(+), 3 deletions(-) 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 c679f835baccfe..c854a27c97603c 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 @@ -893,9 +893,10 @@ private static void DeterminePropertyNullability(JsonPropertyInfo propertyInfo, else { Debug.Assert(propertyInfo.MemberType is MemberTypes.Field); - NullabilityInfo nullabilityInfo = nullabilityCtx.Create((FieldInfo)memberInfo); + var fieldInfo = (FieldInfo)memberInfo; + NullabilityInfo nullabilityInfo = nullabilityCtx.Create(fieldInfo); propertyInfo.IsGetNullable = nullabilityInfo.ReadState is not NullabilityState.NotNull; - propertyInfo.IsSetNullable = nullabilityInfo.WriteState is not NullabilityState.NotNull; + propertyInfo.IsSetNullable = !fieldInfo.IsInitOnly && nullabilityInfo.WriteState is not NullabilityState.NotNull; } } diff --git a/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs index 81add6d60bb534..b53f2d0d09d8de 100644 --- a/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs @@ -95,7 +95,10 @@ public void GetOnlyProperties_DoNotUseSetterNullabilityInSchema() "items": { "type": ["string", "null"] } }, "SingleValueGetOnly": { "type": "string" }, - "SingleValueGetSet": { "type": "string" } + "NullableGetOnly": { "type": ["string", "null"] }, + "SingleValueGetSet": { "type": "string" }, + "NonNullableReadonlyField": { "type": "string" }, + "NullableReadonlyField": { "type": ["string", "null"] } } } """; @@ -287,7 +290,12 @@ 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))] 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/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 { } From 50d74854e06119a4d22f86df017c7d8ba3a2ac49 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:21:08 +0000 Subject: [PATCH 14/15] Fix unexpected nullability in get-only properties for JSON schema export - In DefaultJsonTypeInfoResolver.Helpers.cs (reflection path): guard IsSetNullable with SetMethod is not null for properties, and !IsInitOnly for fields. This ensures get-only/readonly members report IsSetNullable=false. - In JsonSourceGenerator.Emitter.cs (source gen path): add (property.PropertyType.CanBeNull && property.IsReadOnly) condition to emit IsSetNullable = false for get-only reference-type properties. - Update source gen baselines for ConstructorWithDefaultValues and UnsafeAccessors_InaccessibleConstructor to reflect new IsSetNullable = false emission for string get-only properties. - Add NullableAnnotationsTests for IsSetNullable/IsGetNullable on readonly members. - Add JsonSchemaExporterTests for get-only property schema correctness. - Add source gen context registrations for new test types. Co-authored-by: Youssef1313 <31348972+Youssef1313@users.noreply.github.com> --- .../System.Text.Json/gen/JsonSourceGenerator.Emitter.cs | 2 +- .../netcoreapp/MyContext.Config.g.cs.txt | 1 + .../netcoreapp/MyContext.InaccessibleCtor.g.cs.txt | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index f07209ec741f18..63c67bb3e6c8e1 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -920,7 +920,7 @@ property.DefaultIgnoreCondition is JsonIgnoreCondition.Always && { writer.WriteLine($"properties[{i}].IsGetNullable = false;"); } - if (property.IsSetterNonNullableAnnotation || property.IsReadOnly) + if (property.IsSetterNonNullableAnnotation || (property.PropertyType.CanBeNull && property.IsReadOnly)) { writer.WriteLine($"properties[{i}].IsSetNullable = false;"); } 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..86e9b11b324d2f 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 { 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..0c2010f04af4b2 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 { From 45d7d78bbc81b5e206b9c19533dcf28e48f3dd3e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Jul 2026 15:14:50 +0000 Subject: [PATCH 15/15] Remove CanBeNull guard: emit IsSetNullable=false for all readonly properties in source gen Co-authored-by: Youssef1313 <31348972+Youssef1313@users.noreply.github.com> --- .../System.Text.Json/gen/JsonSourceGenerator.Emitter.cs | 2 +- .../net462/MyContext.Config.g.cs.txt | 3 +++ .../netcoreapp/MyContext.Config.g.cs.txt | 2 ++ .../ParameterizedConstructor/net462/MyContext.Point.g.cs.txt | 2 ++ .../netcoreapp/MyContext.Point.g.cs.txt | 2 ++ .../net462/MyContext.InaccessibleCtor.g.cs.txt | 2 ++ .../netcoreapp/MyContext.InaccessibleCtor.g.cs.txt | 1 + 7 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index 63c67bb3e6c8e1..f07209ec741f18 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -920,7 +920,7 @@ property.DefaultIgnoreCondition is JsonIgnoreCondition.Always && { writer.WriteLine($"properties[{i}].IsGetNullable = false;"); } - if (property.IsSetterNonNullableAnnotation || (property.PropertyType.CanBeNull && property.IsReadOnly)) + if (property.IsSetterNonNullableAnnotation || property.IsReadOnly) { writer.WriteLine($"properties[{i}].IsSetNullable = false;"); } 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 86e9b11b324d2f..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 @@ -90,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 { @@ -110,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 0c2010f04af4b2..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 @@ -90,6 +90,7 @@ namespace TestApp }; properties[1] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo(options, info1); + properties[1].IsSetNullable = false; return properties; }