From 3c313d5fb09329ba2a77d9c724e5ffd3c434c784 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Tue, 14 Jul 2026 08:38:10 +0200 Subject: [PATCH 1/4] Produce deprecated property in JsonSchema for obsolete types --- .../src/System/Text/Json/Schema/JsonSchema.cs | 10 ++++++++++ .../Text/Json/Schema/JsonSchemaExporter.cs | 6 ++++++ .../tests/Common/JsonSchemaExporterTests.cs | 17 +++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchema.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchema.cs index 8ffc12bd077926..4ece65f346b173 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchema.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchema.cs @@ -25,6 +25,7 @@ internal sealed class JsonSchema internal const string DefaultPropertyName = "default"; internal const string MinLengthPropertyName = "minLength"; internal const string MaxLengthPropertyName = "maxLength"; + internal const string DeprecatedPropertyName = "deprecated"; public static JsonSchema CreateFalseSchema() => new(false); public static JsonSchema CreateTrueSchema() => new(true); @@ -95,6 +96,9 @@ public JsonSchema() { } public int? MaxLength { get => _maxLength; set { VerifyMutable(); _maxLength = value; } } private int? _maxLength; + public bool Deprecated { get => _deprecated; set { VerifyMutable(); _deprecated = value; } } + private bool _deprecated; + public JsonSchemaExporterContext? ExporterContext { get; set; } public int KeywordCount @@ -124,6 +128,7 @@ public int KeywordCount Count(HasDefaultValue); Count(MinLength != null); Count(MaxLength != null); + Count(Deprecated); return count; @@ -255,6 +260,11 @@ public JsonNode ToJsonNode(JsonSchemaExporterOptions options) objSchema.Add(MaxLengthPropertyName, (JsonNode)maxLength); } + if (Deprecated) + { + objSchema.Add(DeprecatedPropertyName, (JsonNode)true); + } + return CompleteSchema(objSchema); JsonNode CompleteSchema(JsonNode schema) 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 1ac5814543c963..0b66e82943dde0 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 @@ -423,6 +423,12 @@ private static JsonSchema MapJsonSchemaCore( JsonSchema CompleteSchema(ref GenerationState state, JsonSchema schema) { + if (typeInfo.Type.IsDefined(typeof(ObsoleteAttribute), inherit: true)) + { + JsonSchema.EnsureMutable(ref schema); + schema.Deprecated = true; + } + if (schema.Ref is null) { if (IsNullableSchema(state.ExporterOptions)) diff --git a/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs index 190166ef19c3f8..72ad32cee1114e 100644 --- a/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs @@ -260,6 +260,23 @@ public void LegacySchemaExporter_CanAccessReflectedMembers() Assert.Equal("Value", memberNameProperty.GetValue(propertyInfo)); } + [Fact] + public void JsonSchemaExporter_ObsoleteType_GeneratesDeprecatedProperty() + { +#pragma warning disable CS0612 // Type or member is obsolete + JsonNode schema = Serializer.DefaultOptions.GetJsonSchemaAsNode(typeof(MyObsoleteType)); +#pragma warning restore CS0612 // Type or member is obsolete + JsonValue value = Assert.IsType(schema["deprecated"], exactMatch: false); + Assert.Equal(JsonValueKind.True, value.GetValueKind()); + + } + + [Obsolete] + internal sealed class MyObsoleteType + { + public string? MyString { get; set; } + } + record PocoWithProperty(int Value); [JsonSerializable(typeof(PocoWithProperty))] From a1e1d3888438cf2be0e698008ce98ff322bd037f Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Tue, 14 Jul 2026 10:51:04 +0200 Subject: [PATCH 2/4] Address review comments --- .../src/System/Text/Json/Schema/JsonSchema.cs | 10 +++++----- .../src/System/Text/Json/Schema/JsonSchemaExporter.cs | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchema.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchema.cs index 4ece65f346b173..45ee76419a480d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchema.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchema.cs @@ -96,8 +96,8 @@ public JsonSchema() { } public int? MaxLength { get => _maxLength; set { VerifyMutable(); _maxLength = value; } } private int? _maxLength; - public bool Deprecated { get => _deprecated; set { VerifyMutable(); _deprecated = value; } } - private bool _deprecated; + public bool? Deprecated { get => _deprecated; set { VerifyMutable(); _deprecated = value; } } + private bool? _deprecated; public JsonSchemaExporterContext? ExporterContext { get; set; } @@ -128,7 +128,7 @@ public int KeywordCount Count(HasDefaultValue); Count(MinLength != null); Count(MaxLength != null); - Count(Deprecated); + Count(Deprecated != null); return count; @@ -260,9 +260,9 @@ public JsonNode ToJsonNode(JsonSchemaExporterOptions options) objSchema.Add(MaxLengthPropertyName, (JsonNode)maxLength); } - if (Deprecated) + if (Deprecated is { } deprecated) { - objSchema.Add(DeprecatedPropertyName, (JsonNode)true); + objSchema.Add(DeprecatedPropertyName, (JsonNode)deprecated); } return CompleteSchema(objSchema); 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 0b66e82943dde0..9d7c648ff61050 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 @@ -423,7 +423,8 @@ private static JsonSchema MapJsonSchemaCore( JsonSchema CompleteSchema(ref GenerationState state, JsonSchema schema) { - if (typeInfo.Type.IsDefined(typeof(ObsoleteAttribute), inherit: true)) + if (typeInfo.Type.IsDefined(typeof(ObsoleteAttribute), inherit: true) || + propertyInfo?.AttributeProvider?.IsDefined(typeof(ObsoleteAttribute), inherit: true) == true) { JsonSchema.EnsureMutable(ref schema); schema.Deprecated = true; From a6681658b28983423ec1c5ac22f146d0f4a27894 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Tue, 14 Jul 2026 15:17:01 +0200 Subject: [PATCH 3/4] Handle properties, add tests --- .../Text/Json/Schema/JsonSchemaExporter.cs | 27 +++++++++++- .../tests/Common/JsonSchemaExporterTests.cs | 41 +++++++++++++++++-- .../Serialization/JsonSchemaExporterTests.cs | 3 ++ 3 files changed, 66 insertions(+), 5 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 9d7c648ff61050..04c01ffda9c50e 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 @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Reflection; using System.Runtime.InteropServices; using System.Text.Json.Nodes; using System.Text.Json.Serialization; @@ -423,8 +424,8 @@ private static JsonSchema MapJsonSchemaCore( JsonSchema CompleteSchema(ref GenerationState state, JsonSchema schema) { - if (typeInfo.Type.IsDefined(typeof(ObsoleteAttribute), inherit: true) || - propertyInfo?.AttributeProvider?.IsDefined(typeof(ObsoleteAttribute), inherit: true) == true) + if (HasObsoleteAttribute(typeInfo.Type) || + HasObsoleteAttribute(propertyInfo?.AttributeProvider)) { JsonSchema.EnsureMutable(ref schema); schema.Deprecated = true; @@ -478,6 +479,28 @@ private static void ValidateOptions(JsonSerializerOptions options) options.MakeReadOnly(); } + private static bool HasObsoleteAttribute(ICustomAttributeProvider? attributeProvider) + { + if (attributeProvider is null) + { + return false; + } + + // Identify ObsoleteAttribute using its full type name rather than typeof(ObsoleteAttribute). + // On downlevel targets System.Text.Json compiles in an internal ObsoleteAttribute polyfill + // that would otherwise shadow the framework type, causing the typeof comparison to never match + // the ObsoleteAttribute applied by user code. + foreach (object attribute in attributeProvider.GetCustomAttributes(inherit: true)) + { + if (attribute.GetType().FullName == "System.ObsoleteAttribute") + { + return true; + } + } + + return false; + } + private static bool IsPolymorphicTypeThatSpecifiesItselfAsDerivedType(JsonTypeInfo typeInfo) { Debug.Assert(typeInfo.PolymorphismOptions is not null); diff --git a/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs index 72ad32cee1114e..0656bd3cb7a2d6 100644 --- a/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs @@ -266,15 +266,50 @@ public void JsonSchemaExporter_ObsoleteType_GeneratesDeprecatedProperty() #pragma warning disable CS0612 // Type or member is obsolete JsonNode schema = Serializer.DefaultOptions.GetJsonSchemaAsNode(typeof(MyObsoleteType)); #pragma warning restore CS0612 // Type or member is obsolete - JsonValue value = Assert.IsType(schema["deprecated"], exactMatch: false); - Assert.Equal(JsonValueKind.True, value.GetValueKind()); + AssertDeprecated(schema, expectedDeprecated: true); + + JsonObject properties = Assert.IsType(schema["properties"], exactMatch: false); + + // Non-obsolete property does not report deprecated. + AssertDeprecated(properties["MyString"], expectedDeprecated: false); + + // Property annotated with [Obsolete] reports deprecated. + AssertDeprecated(properties["MyObsoleteString"], expectedDeprecated: true); + + // Property whose type is annotated with [Obsolete] reports deprecated. + AssertDeprecated(properties["MyObsoleteInnerType"], expectedDeprecated: true); + + static void AssertDeprecated(JsonNode? schemaNode, bool expectedDeprecated) + { + JsonObject schemaObject = Assert.IsType(schemaNode, exactMatch: false); + + if (expectedDeprecated) + { + JsonValue value = Assert.IsType(schemaObject["deprecated"], exactMatch: false); + Assert.Equal(JsonValueKind.True, value.GetValueKind()); + } + else + { + Assert.False(schemaObject.ContainsKey("deprecated")); + } + } } [Obsolete] - internal sealed class MyObsoleteType + public sealed class MyObsoleteType { public string? MyString { get; set; } + + [Obsolete] + public string? MyObsoleteString { get; set; } + + public MyInnerObsoleteType? MyObsoleteInnerType { get; set; } + + [Obsolete] + public sealed class MyInnerObsoleteType + { + } } record PocoWithProperty(int Value); 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 f4cf65cb00a8b9..67eea71653bd49 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 @@ -117,6 +117,9 @@ public sealed partial class JsonSchemaExporterTests_SourceGen() [JsonSerializable(typeof(ClassWithJsonPointerEscapablePropertyNames))] [JsonSerializable(typeof(ClassWithOptionalObjectParameter))] [JsonSerializable(typeof(ClassWithPropertiesUsingCustomConverters))] +#pragma warning disable CS0612 // Type or member is obsolete + [JsonSerializable(typeof(MyObsoleteType))] +#pragma warning restore CS0612 // Type or member is obsolete // Collection types [JsonSerializable(typeof(int[]))] [JsonSerializable(typeof(List))] From f356da16ad6401ecded23b1dc3b1dd2625827154 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Wed, 15 Jul 2026 10:23:49 +0200 Subject: [PATCH 4/4] Assert complete schema --- .../JsonSchemaExporterTests.TestTypes.cs | 32 ++++++++++++ .../tests/Common/JsonSchemaExporterTests.cs | 52 ------------------- 2 files changed, 32 insertions(+), 52 deletions(-) diff --git a/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.TestTypes.cs b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.TestTypes.cs index 065f18e501d5ec..6bb37ddbb827fa 100644 --- a/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.TestTypes.cs +++ b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.TestTypes.cs @@ -1193,6 +1193,22 @@ of the type which points to the first occurrence. */ } """); +#pragma warning disable CS0612 // Type or member is obsolete + yield return new TestData( + Value: new() { MyString = "str", MyObsoleteString = "str", MyObsoleteInnerType = new() }, + ExpectedJsonSchema: """ + { + "type": ["object","null"], + "properties": { + "MyString": { "type": ["string","null"] }, + "MyObsoleteString": { "type": ["string","null"], "deprecated": true }, + "MyObsoleteInnerType": { "type": ["object","null"], "deprecated": true } + }, + "deprecated": true + } + """); +#pragma warning restore CS0612 // Type or member is obsolete + // Collection types yield return new TestData([1, 2, 3], ExpectedJsonSchema: """{"type":["array","null"],"items":{"type":"integer"}}"""); yield return new TestData>([false, true, false], ExpectedJsonSchema: """{"type":["array","null"],"items":{"type":"boolean"}}"""); @@ -1638,6 +1654,22 @@ public readonly struct StructDictionary(IEnumerable ((IEnumerable)_dictionary).GetEnumerator(); } + [Obsolete] + public sealed class MyObsoleteType + { + public string? MyString { get; set; } + + [Obsolete] + public string? MyObsoleteString { get; set; } + + public MyInnerObsoleteType? MyObsoleteInnerType { get; set; } + + [Obsolete] + public sealed class MyInnerObsoleteType + { + } + } + public record TestData( T? Value, string ExpectedJsonSchema, diff --git a/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs index 0656bd3cb7a2d6..190166ef19c3f8 100644 --- a/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs @@ -260,58 +260,6 @@ public void LegacySchemaExporter_CanAccessReflectedMembers() Assert.Equal("Value", memberNameProperty.GetValue(propertyInfo)); } - [Fact] - public void JsonSchemaExporter_ObsoleteType_GeneratesDeprecatedProperty() - { -#pragma warning disable CS0612 // Type or member is obsolete - JsonNode schema = Serializer.DefaultOptions.GetJsonSchemaAsNode(typeof(MyObsoleteType)); -#pragma warning restore CS0612 // Type or member is obsolete - - AssertDeprecated(schema, expectedDeprecated: true); - - JsonObject properties = Assert.IsType(schema["properties"], exactMatch: false); - - // Non-obsolete property does not report deprecated. - AssertDeprecated(properties["MyString"], expectedDeprecated: false); - - // Property annotated with [Obsolete] reports deprecated. - AssertDeprecated(properties["MyObsoleteString"], expectedDeprecated: true); - - // Property whose type is annotated with [Obsolete] reports deprecated. - AssertDeprecated(properties["MyObsoleteInnerType"], expectedDeprecated: true); - - static void AssertDeprecated(JsonNode? schemaNode, bool expectedDeprecated) - { - JsonObject schemaObject = Assert.IsType(schemaNode, exactMatch: false); - - if (expectedDeprecated) - { - JsonValue value = Assert.IsType(schemaObject["deprecated"], exactMatch: false); - Assert.Equal(JsonValueKind.True, value.GetValueKind()); - } - else - { - Assert.False(schemaObject.ContainsKey("deprecated")); - } - } - } - - [Obsolete] - public sealed class MyObsoleteType - { - public string? MyString { get; set; } - - [Obsolete] - public string? MyObsoleteString { get; set; } - - public MyInnerObsoleteType? MyObsoleteInnerType { get; set; } - - [Obsolete] - public sealed class MyInnerObsoleteType - { - } - } - record PocoWithProperty(int Value); [JsonSerializable(typeof(PocoWithProperty))]