From 28d614f3a8a4e98cfe202ab0f1ff8d75241bbdf8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:16:58 +0000 Subject: [PATCH 1/9] fix(library): do not emit unevaluatedProperties for non-object schemas When `Type` is explicitly set and does not include `Object`, skip emitting `unevaluatedProperties` in all serialization paths (v3.1+ native property, v2.0/v3.0 extension, and direct v2.0 serialization). Closes: unevaluatedProperties emitted for array/string/number/integer/boolean/null schemas Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- src/Microsoft.OpenApi/Models/OpenApiSchema.cs | 84 ++++++++------ .../Models/OpenApiSchemaTests.cs | 105 ++++++++++++++++++ 2 files changed, 154 insertions(+), 35 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index 85f899bf5..fd802f13a 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs @@ -542,19 +542,24 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version // For versions < 3.1, write unevaluatedProperties as an extension if (version < OpenApiSpecVersion.OpenApi3_1) { - // Write UnevaluatedPropertiesSchema as extension if present - if (UnevaluatedPropertiesSchema is not null) + // Only emit unevaluatedProperties when the type could include objects. + // Skip when type is explicitly set to a non-object type (array, string, number, integer, boolean, null). + if (!Type.HasValue || (Type.Value & JsonSchemaType.Object) != 0) { - writer.WriteOptionalObject( - OpenApiConstants.UnevaluatedPropertiesExtension, - UnevaluatedPropertiesSchema, - callback); - } - // Write boolean false as extension if explicitly set to false - else if (!UnevaluatedProperties) - { - writer.WritePropertyName(OpenApiConstants.UnevaluatedPropertiesExtension); - writer.WriteValue(false); + // Write UnevaluatedPropertiesSchema as extension if present + if (UnevaluatedPropertiesSchema is not null) + { + writer.WriteOptionalObject( + OpenApiConstants.UnevaluatedPropertiesExtension, + UnevaluatedPropertiesSchema, + callback); + } + // Write boolean false as extension if explicitly set to false + else if (!UnevaluatedProperties) + { + writer.WritePropertyName(OpenApiConstants.UnevaluatedPropertiesExtension); + writer.WriteValue(false); + } } // Write patternProperties as an extension @@ -594,19 +599,23 @@ internal void WriteJsonSchemaKeywords(IOpenApiWriter writer) writer.WriteProperty(OpenApiConstants.DynamicRef, DynamicRef); writer.WriteProperty(OpenApiConstants.DynamicAnchor, DynamicAnchor); - // UnevaluatedProperties: similar to AdditionalProperties, serialize as schema if present, else as boolean - if (UnevaluatedPropertiesSchema is not null) - { - writer.WriteOptionalObject( - OpenApiConstants.UnevaluatedProperties, - UnevaluatedPropertiesSchema, - (w, s) => s.SerializeAsV31(w)); - } - else if (!UnevaluatedProperties) + // UnevaluatedProperties: similar to AdditionalProperties, serialize as schema if present, else as boolean. + // Only emit when the type could include objects. + // Skip when type is explicitly set to a non-object type (array, string, number, integer, boolean, null). + if (!Type.HasValue || (Type.Value & JsonSchemaType.Object) != 0) { - writer.WriteProperty(OpenApiConstants.UnevaluatedProperties, UnevaluatedProperties); + if (UnevaluatedPropertiesSchema is not null) + { + writer.WriteOptionalObject( + OpenApiConstants.UnevaluatedProperties, + UnevaluatedPropertiesSchema, + (w, s) => s.SerializeAsV31(w)); + } + else if (!UnevaluatedProperties) + { + writer.WriteProperty(OpenApiConstants.UnevaluatedProperties, UnevaluatedProperties); + } } - // true is the default, no need to write it out writer.WriteOptionalCollection(OpenApiConstants.Examples, Examples, (nodeWriter, s) => nodeWriter.WriteAny(s)); writer.WriteOptionalMap(OpenApiConstants.PatternProperties, PatternProperties, (w, s) => s.SerializeAsV31(w)); writer.WriteOptionalMap(OpenApiConstants.DependentRequired, DependentRequired, (w, s) => w.WriteValue(s)); @@ -827,19 +836,24 @@ private void SerializeAsV2( // x-nullable extension SerializeNullable(writer, OpenApiSpecVersion.OpenApi2_0); - // Write UnevaluatedPropertiesSchema as extension if present - if (UnevaluatedPropertiesSchema is not null) + // Write UnevaluatedPropertiesSchema as extension if present. + // Only emit when the type could include objects. + // Skip when type is explicitly set to a non-object type (array, string, number, integer, boolean, null). + if (!Type.HasValue || (Type.Value & JsonSchemaType.Object) != 0) { - writer.WriteOptionalObject( - OpenApiConstants.UnevaluatedPropertiesExtension, - UnevaluatedPropertiesSchema, - (w, s) => s.SerializeAsV2(w)); - } - // Write boolean false as extension if explicitly set to false - else if (!UnevaluatedProperties) - { - writer.WritePropertyName(OpenApiConstants.UnevaluatedPropertiesExtension); - writer.WriteValue(false); + if (UnevaluatedPropertiesSchema is not null) + { + writer.WriteOptionalObject( + OpenApiConstants.UnevaluatedPropertiesExtension, + UnevaluatedPropertiesSchema, + (w, s) => s.SerializeAsV2(w)); + } + // Write boolean false as extension if explicitly set to false + else if (!UnevaluatedProperties) + { + writer.WritePropertyName(OpenApiConstants.UnevaluatedPropertiesExtension); + writer.WriteValue(false); + } } // Write patternProperties as an extension diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs index 1e924cad3..c6a198918 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs @@ -1308,6 +1308,111 @@ public async Task SerializeUnevaluatedPropertiesTrueNotEmittedInEarlierVersions( Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual))); } + [Theory] + [InlineData(JsonSchemaType.Array, "array")] + [InlineData(JsonSchemaType.String, "string")] + [InlineData(JsonSchemaType.Number, "number")] + [InlineData(JsonSchemaType.Integer, "integer")] + [InlineData(JsonSchemaType.Boolean, "boolean")] + [InlineData(JsonSchemaType.Null, "null")] + public async Task SerializeUnevaluatedPropertiesFalseNotEmittedForNonObjectType(JsonSchemaType nonObjectType, string typeName) + { + var expected = $@"{{ ""type"": ""{typeName}"" }}"; + // Given - unevaluatedProperties should not be emitted when type is explicitly set to a non-object type + var schema = new OpenApiSchema + { + Type = nonObjectType, + UnevaluatedProperties = false + }; + + // When + var actual = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_1); + + // Then + Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual))); + } + + [Theory] + [InlineData(JsonSchemaType.Array, "array")] + [InlineData(JsonSchemaType.String, "string")] + [InlineData(JsonSchemaType.Number, "number")] + [InlineData(JsonSchemaType.Integer, "integer")] + [InlineData(JsonSchemaType.Boolean, "boolean")] + [InlineData(JsonSchemaType.Null, "null")] + public async Task SerializeUnevaluatedPropertiesSchemaNotEmittedForNonObjectType(JsonSchemaType nonObjectType, string typeName) + { + var expected = $@"{{ ""type"": ""{typeName}"" }}"; + // Given - unevaluatedProperties schema should not be emitted when type is explicitly set to a non-object type + var schema = new OpenApiSchema + { + Type = nonObjectType, + UnevaluatedPropertiesSchema = new OpenApiSchema { Type = JsonSchemaType.String } + }; + + // When + var actual = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_1); + + // Then + Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual))); + } + + [Theory] + [InlineData(OpenApiSpecVersion.OpenApi2_0, JsonSchemaType.Array)] + [InlineData(OpenApiSpecVersion.OpenApi2_0, JsonSchemaType.String)] + [InlineData(OpenApiSpecVersion.OpenApi3_0, JsonSchemaType.Array)] + [InlineData(OpenApiSpecVersion.OpenApi3_0, JsonSchemaType.String)] + public async Task SerializeUnevaluatedPropertiesNotEmittedAsExtensionForNonObjectType(OpenApiSpecVersion version, JsonSchemaType nonObjectType) + { + // Given - unevaluatedProperties should not be emitted as extension when type is a non-object type + var schema = new OpenApiSchema + { + Type = nonObjectType, + UnevaluatedProperties = false + }; + + // When + var actual = await schema.SerializeAsJsonAsync(version); + + // Then - should not contain unevaluatedProperties extension + var parsed = JsonNode.Parse(actual)!.AsObject(); + Assert.False(parsed.ContainsKey(OpenApiConstants.UnevaluatedPropertiesExtension)); + } + + [Fact] + public async Task SerializeUnevaluatedPropertiesFalseStillEmittedForObjectType() + { + var expected = @"{ ""type"": ""object"", ""unevaluatedProperties"": false }"; + // Given - unevaluatedProperties should still be emitted for object type + var schema = new OpenApiSchema + { + Type = JsonSchemaType.Object, + UnevaluatedProperties = false + }; + + // When + var actual = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_1); + + // Then + Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual))); + } + + [Fact] + public async Task SerializeUnevaluatedPropertiesFalseStillEmittedWhenTypeNotSet() + { + var expected = @"{ ""unevaluatedProperties"": false }"; + // Given - unevaluatedProperties should still be emitted when type is not explicitly set + var schema = new OpenApiSchema + { + UnevaluatedProperties = false + }; + + // When + var actual = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_1); + + // Then + Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual))); + } + // PatternProperties tests [Theory] [InlineData(OpenApiSpecVersion.OpenApi3_1)] From 60c9adfaafdd7a6d534728324f2e67d4302085c8 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 13 Mar 2026 09:41:47 -0400 Subject: [PATCH 2/9] chore: reverts sdk version change --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 459fe3e6e..d72c3f669 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "8.0.418" + "version": "8.0.419" } } \ No newline at end of file From 6fed45deb2e17a31dcc1b6b37e2710ecce8faaba Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 19 Mar 2026 11:28:27 -0400 Subject: [PATCH 3/9] Merge pull request #2783 from microsoft/fix/key-location-encoding fix/key location encoding --- .../Services/OpenApiVisitorBase.cs | 14 +++- .../Services/OpenApiWalker.cs | 15 +--- .../Validations/Rules/OpenApiDocumentRules.cs | 43 +++++++--- .../Services/OpenApiVisitorBaseTests.cs | 80 +++++++++++++++++++ .../OpenApiRecommendedRulesTests.cs | 2 +- 5 files changed, 129 insertions(+), 25 deletions(-) create mode 100644 test/Microsoft.OpenApi.Tests/Services/OpenApiVisitorBaseTests.cs diff --git a/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs b/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs index 7aa411b57..4c3419765 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Net.Http; using System.Text.Json.Nodes; @@ -27,7 +28,16 @@ public abstract class OpenApiVisitorBase /// Identifier for context public virtual void Enter(string segment) { - this._path.Push(segment); + if (string.IsNullOrEmpty(segment)) + { + this._path.Push(string.Empty); + return; + } +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP1_0_OR_GREATER + this._path.Push(segment.Replace("~", "~0", StringComparison.Ordinal).Replace("/", "~1", StringComparison.OrdinalIgnoreCase)); +#else + this._path.Push(segment.Replace("~", "~0").Replace("/", "~1")); +#endif } /// @@ -41,7 +51,7 @@ public virtual void Exit() /// /// Pointer to source of validation error in document /// - public string PathString { get => "#/" + String.Join("/", _path.Reverse()); } + public string PathString { get => "#/" + string.Join("/", _path.Reverse()); } /// /// Visits diff --git a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs index 8b5868216..315146102 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs @@ -1276,15 +1276,6 @@ internal void Walk(IOpenApiElement element) } } - private static string ReplaceSlashes(string value) - { -#if NET8_0_OR_GREATER - return value.Replace("/", "~1", StringComparison.Ordinal); -#else - return value.Replace("/", "~1"); -#endif - } - /// /// Adds a segment to the context path to enable pointing to the current location in the document /// @@ -1294,7 +1285,7 @@ private static string ReplaceSlashes(string value) /// An action that walks objects within the context. private void WalkItem(string context, T state, Action walk) { - _visitor.Enter(ReplaceSlashes(context)); + _visitor.Enter(context); walk(this, state); _visitor.Exit(); } @@ -1309,7 +1300,7 @@ private void WalkItem(string context, T state, Action walk) /// An action that walks objects within the context. private void WalkItem(string context, T state, Action walk, bool isComponent) { - _visitor.Enter(ReplaceSlashes(context)); + _visitor.Enter(context); walk(this, state, isComponent); _visitor.Exit(); } @@ -1330,7 +1321,7 @@ private void WalkDictionary( { if (state != null && state.Count > 0) { - _visitor.Enter(ReplaceSlashes(context)); + _visitor.Enter(context); foreach (var item in state) { diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs index a1f166ee3..38767035f 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. using System; +using System.Collections.Generic; +using System.Linq; namespace Microsoft.OpenApi { @@ -74,28 +76,49 @@ private void ValidateSchemaReference(OpenApiSchemaReference reference) { if (reference.RecursiveTarget is null) { + var segments = GetSegments().ToArray(); + EnterSegments(segments); // The reference was not followed to a valid schema somewhere in the document - context.Enter(GetSegment()); context.CreateWarning(ruleName, string.Format(SRResource.Validation_SchemaReferenceDoesNotExist, reference.Reference.ReferenceV3)); - context.Exit(); + ExitSegments(segments.Length); } } catch (InvalidOperationException ex) { - context.Enter(GetSegment()); + var segments = GetSegments().ToArray(); + EnterSegments(segments); context.CreateWarning(ruleName, ex.Message); - context.Exit(); + ExitSegments(segments.Length); } - string GetSegment() + void ExitSegments(int length) { - // Trim off the leading "#/" as the context is already at the root of the document - return -#if NET8_0_OR_GREATER - $"{PathString[2..]}/$ref"; + for (var i = 0; i < length; i++) + { + context.Exit(); + } + } + + void EnterSegments(string[] segments) + { + foreach (var segment in segments) + { + context.Enter(segment); + } + } + + IEnumerable GetSegments() + { + foreach (var segment in this.PathString.Substring(2).Split('/')) + { +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP1_0_OR_GREATER + yield return segment.Replace("~1", "/", StringComparison.OrdinalIgnoreCase).Replace("~0", "~", StringComparison.OrdinalIgnoreCase); #else - PathString.Substring(2) + "/$ref"; + yield return segment.Replace("~1", "/").Replace("~0", "~"); #endif + } + yield return "$ref"; + // Trim off the leading "#/" as the context is already at the root of the document } } } diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiVisitorBaseTests.cs b/test/Microsoft.OpenApi.Tests/Services/OpenApiVisitorBaseTests.cs new file mode 100644 index 000000000..ce92819f3 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiVisitorBaseTests.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.OpenApi.Tests.Services; + +public class OpenApiVisitorBaseTests +{ + [Fact] + public void EncodesReservedCharacters() + { + // Given + var openApiDocument = new OpenApiDocument + { + Info = new() + { + Title = "foo", + Version = "1.2.2" + }, + Paths = new() + { + }, + Components = new() + { + Schemas = new Dictionary() + { + ["Pet~"] = new OpenApiSchema() + { + Type = JsonSchemaType.Object + }, + ["Pet/"] = new OpenApiSchema() + { + Type = JsonSchemaType.Object + }, + } + } + }; + var visitor = new LocatorVisitor(); + + // When + visitor.Visit(openApiDocument); + + // Then + Assert.Equivalent( + new List + { + "#/components/schemas/Pet~0", + "#/components/schemas/Pet~1" + }, visitor.Locations); + } + + private class LocatorVisitor : OpenApiVisitorBase + { + public List Locations { get; } = new List(); + + public override void Visit(IOpenApiSchema openApiSchema) + { + Locations.Add(this.PathString); + } + public override void Visit(OpenApiComponents components) + { + Enter("schemas"); + if (components.Schemas != null) + { + foreach (var schemaKvp in components.Schemas) + { + Enter(schemaKvp.Key); + this.Visit(schemaKvp.Value); + Exit(); + } + } + Exit(); + } + public override void Visit(OpenApiDocument doc) + { + Enter("components"); + Visit(doc.Components); + Exit(); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiRecommendedRulesTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiRecommendedRulesTests.cs index fc0b0df31..b4d796f39 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/OpenApiRecommendedRulesTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiRecommendedRulesTests.cs @@ -200,7 +200,7 @@ public static void GetOperationWithRequestBodyIsInvalid() Assert.NotNull(warnings); var warning = Assert.Single(warnings); Assert.Equal("GET operations should not have a request body.", warning.Message); - Assert.Equal("#/paths//people/get/requestBody", warning.Pointer); + Assert.Equal("#/paths/~1people/get/requestBody", warning.Pointer); } [Fact] From 2efd2b2e48be072f092115dface6cea3e0ba7182 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 20 Mar 2026 11:50:42 -0400 Subject: [PATCH 4/9] Merge pull request #2790 from microsoft/fix/path-parameter-with-slash-validation fix: a bug where path parameter validation would fail if they contained forbidden JSON pointer characters --- .../Services/OpenApiVisitorBase.cs | 13 +++- .../Rules/OpenApiParameterRules.cs | 2 +- .../OpenApiHeaderValidationTests.cs | 2 +- .../OpenApiParameterValidationTests.cs | 69 ++++++++++++++++++- .../OpenApiSchemaValidationTests.cs | 6 +- 5 files changed, 83 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs b/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs index 4c3419765..c00a54888 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs @@ -33,10 +33,19 @@ public virtual void Enter(string segment) this._path.Push(string.Empty); return; } + this._path.Push(EncodeJsonPointerSegment(segment)); + } + + internal static string EncodeJsonPointerSegment(string? segment) + { + if (string.IsNullOrEmpty(segment)) + { + return string.Empty; + } #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP1_0_OR_GREATER - this._path.Push(segment.Replace("~", "~0", StringComparison.Ordinal).Replace("/", "~1", StringComparison.OrdinalIgnoreCase)); + return segment.Replace("~", "~0", StringComparison.Ordinal).Replace("/", "~1", StringComparison.OrdinalIgnoreCase); #else - this._path.Push(segment.Replace("~", "~0").Replace("/", "~1")); + return segment!.Replace("~", "~0").Replace("/", "~1"); #endif } diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiParameterRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiParameterRules.cs index a84ea3255..c00e1dcce 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiParameterRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiParameterRules.cs @@ -61,7 +61,7 @@ public static class OpenApiParameterRules (context, parameter) => { if (parameter.In == ParameterLocation.Path && - !(context.PathString.Contains("{" + parameter.Name + "}") || context.PathString.Contains("#/components"))) + !(context.PathString.Contains("{" + OpenApiVisitorBase.EncodeJsonPointerSegment(parameter.Name) + "}") || context.PathString.Contains("#/components"))) { context.Enter("in"); context.CreateError( diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiHeaderValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiHeaderValidationTests.cs index a7a0cba0a..98e59928c 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/OpenApiHeaderValidationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiHeaderValidationTests.cs @@ -86,7 +86,7 @@ public void ValidateExamplesShouldNotHaveDataTypeMismatchForSimpleSchema() // Act var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); var walker = new OpenApiWalker(validator); - walker.Walk(header); + walker.Walk((IOpenApiHeader)header); warnings = validator.Warnings; var result = !warnings.Any(); diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiParameterValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiParameterValidationTests.cs index 207db74b6..1d2882033 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/OpenApiParameterValidationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiParameterValidationTests.cs @@ -75,7 +75,7 @@ public void ValidateExampleShouldNotHaveDataTypeMismatchForSimpleSchema() var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); validator.Enter("{parameter1}"); var walker = new OpenApiWalker(validator); - walker.Walk(parameter); + walker.Walk((IOpenApiParameter)parameter); warnings = validator.Warnings; var result = !warnings.Any(); @@ -203,7 +203,7 @@ public void PathParameterInThePathShouldBeOk() validator.Enter("1"); var walker = new OpenApiWalker(validator); - walker.Walk(parameter); + walker.Walk((IOpenApiParameter)parameter); errors = validator.Errors; var result = errors.Any(); @@ -211,5 +211,70 @@ public void PathParameterInThePathShouldBeOk() // Assert Assert.False(result); } + + [Fact] + public void PathParameterInThePathShouldBeOkWithSlashInParameterName() + { + // Arrange + IEnumerable errors; + + var parameter = new OpenApiParameter + { + Name = "parameter/1", + In = ParameterLocation.Path, + Required = true, + Schema = new OpenApiSchema() + { + Type = JsonSchemaType.String, + } + }; + + // Act + var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); + validator.Enter("paths"); + validator.Enter("/{parameter/1}"); + validator.Enter("get"); + validator.Enter("parameters"); + validator.Enter("1"); + + var walker = new OpenApiWalker(validator); + walker.Walk((IOpenApiParameter)parameter); + + errors = validator.Errors; + var result = errors.Any(); + + // Assert + Assert.False(result); + } + + [Fact] + public void PathParameterValidationShouldNotThrowWithEmptyParameterName() + { + // Arrange + var parameter = new OpenApiParameter + { + Name = string.Empty, + In = ParameterLocation.Path, + Required = true, + Schema = new OpenApiSchema() + { + Type = JsonSchemaType.String, + } + }; + + // Act + var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); + validator.Enter("paths"); + validator.Enter("/{}"); + validator.Enter("get"); + validator.Enter("parameters"); + validator.Enter("1"); + + var walker = new OpenApiWalker(validator); + var exception = Record.Exception(() => walker.Walk((IOpenApiParameter)parameter)); + + // Assert + Assert.Null(exception); + } } } diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiSchemaValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiSchemaValidationTests.cs index fdf36e0b4..0cb229534 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/OpenApiSchemaValidationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiSchemaValidationTests.cs @@ -26,7 +26,7 @@ public void ValidateDefaultShouldNotHaveDataTypeMismatchForSimpleSchema() // Act var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); var walker = new OpenApiWalker(validator); - walker.Walk(schema); + walker.Walk((IOpenApiSchema)schema); warnings = validator.Warnings; var result = !warnings.Any(); @@ -50,7 +50,7 @@ public void ValidateExampleAndDefaultShouldNotHaveDataTypeMismatchForSimpleSchem // Act var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); var walker = new OpenApiWalker(validator); - walker.Walk(schema); + walker.Walk((IOpenApiSchema)schema); warnings = validator.Warnings; bool result = !warnings.Any(); @@ -92,7 +92,7 @@ public void ValidateEnumShouldNotHaveDataTypeMismatchForSimpleSchema() // Act var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); var walker = new OpenApiWalker(validator); - walker.Walk(schema); + walker.Walk((IOpenApiSchema)schema); warnings = validator.Warnings; var result = !warnings.Any(); From 7640a8aacea475505b64c9dc60f73f905909f5e7 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 20 Mar 2026 11:52:15 -0400 Subject: [PATCH 5/9] Merge pull request #2782 from microsoft/copilot/fix-openapi-schema-reference-description fix(library): enforce spec-compliant $ref serialization; add Extensions support for schema references in v3.1/v3.2 --- .../Models/JsonSchemaReference.cs | 20 +- .../References/OpenApiSchemaReference.cs | 14 +- src/Microsoft.OpenApi/PublicAPI.Unshipped.txt | 4 + ...orks_produceTerseOutput=False.verified.txt | 3 + ...Works_produceTerseOutput=True.verified.txt | 1 + ...orks_produceTerseOutput=False.verified.txt | 1 + ...Works_produceTerseOutput=True.verified.txt | 2 +- ...orks_produceTerseOutput=False.verified.txt | 12 ++ ...Works_produceTerseOutput=True.verified.txt | 1 + .../References/OpenApiSchemaReferenceTests.cs | 193 +++++++++++++++++- 10 files changed, 244 insertions(+), 7 deletions(-) create mode 100644 test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV2JsonWorks_produceTerseOutput=False.verified.txt create mode 100644 test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV2JsonWorks_produceTerseOutput=True.verified.txt create mode 100644 test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV32JsonWorks_produceTerseOutput=False.verified.txt create mode 100644 test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV32JsonWorks_produceTerseOutput=True.verified.txt diff --git a/src/Microsoft.OpenApi/Models/JsonSchemaReference.cs b/src/Microsoft.OpenApi/Models/JsonSchemaReference.cs index bd758ec8e..2e62ada5d 100644 --- a/src/Microsoft.OpenApi/Models/JsonSchemaReference.cs +++ b/src/Microsoft.OpenApi/Models/JsonSchemaReference.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; @@ -52,6 +52,12 @@ public class JsonSchemaReference : OpenApiReferenceWithDescription /// public IList? Examples { get; set; } + /// + /// Extension data for this schema reference. Only allowed in OpenAPI 3.1 and later. + /// Extensions are NOT written when serializing for OpenAPI 2.0 or 3.0. + /// + public IDictionary? Extensions { get; set; } + /// /// Parameterless constructor /// @@ -69,6 +75,7 @@ public JsonSchemaReference(JsonSchemaReference reference) : base(reference) ReadOnly = reference.ReadOnly; WriteOnly = reference.WriteOnly; Examples = reference.Examples; + Extensions = reference.Extensions != null ? new Dictionary(reference.Extensions) : null; } /// @@ -97,6 +104,7 @@ protected override void SerializeAdditionalV31Properties(IOpenApiWriter writer) { writer.WriteOptionalCollection(OpenApiConstants.Examples, Examples, (w, e) => w.WriteAny(e)); } + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_1); } /// @@ -137,5 +145,15 @@ protected override void SetAdditional31MetadataFromMapNode(JsonObject jsonObject { Examples = examplesArray.OfType().ToList(); } + + // Extensions (properties starting with "x-") + foreach (var property in jsonObject + .Where(static p => p.Key.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase) + && p.Value is not null)) + { + var extensionValue = property.Value!; + Extensions ??= new Dictionary(StringComparer.OrdinalIgnoreCase); + Extensions[property.Key] = new JsonNodeExtension(extensionValue.DeepClone()); + } } } diff --git a/src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs b/src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs index 55be30a51..67eb79645 100644 --- a/src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs +++ b/src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs @@ -10,7 +10,7 @@ namespace Microsoft.OpenApi /// /// Schema reference object /// - public class OpenApiSchemaReference : BaseOpenApiReferenceHolder, IOpenApiSchema, IOpenApiSchemaWithUnevaluatedProperties + public class OpenApiSchemaReference : BaseOpenApiReferenceHolder, IOpenApiSchema, IOpenApiSchemaWithUnevaluatedProperties, IOpenApiExtensible { /// @@ -158,7 +158,11 @@ public bool Deprecated /// public OpenApiXml? Xml { get => Target?.Xml; } /// - public IDictionary? Extensions { get => Target?.Extensions; } + public IDictionary? Extensions + { + get => Reference.Extensions ?? Target?.Extensions; + set => Reference.Extensions = value; + } /// public IDictionary? UnrecognizedKeywords { get => Target?.UnrecognizedKeywords; } @@ -172,6 +176,12 @@ public override void SerializeAsV31(IOpenApiWriter writer) SerializeAsWithoutLoops(writer, (w, element) => (element is IOpenApiSchema s ? CopyReferenceAsTargetElementWithOverrides(s) : element).SerializeAsV31(w)); } + /// + public override void SerializeAsV32(IOpenApiWriter writer) + { + SerializeAsWithoutLoops(writer, (w, element) => (element is IOpenApiSchema s ? CopyReferenceAsTargetElementWithOverrides(s) : element).SerializeAsV32(w)); + } + /// public override void SerializeAsV3(IOpenApiWriter writer) { diff --git a/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt b/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt index 7dc5c5811..7b7b54aca 100644 --- a/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt +++ b/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt @@ -1 +1,5 @@ #nullable enable +Microsoft.OpenApi.JsonSchemaReference.Extensions.get -> System.Collections.Generic.IDictionary? +Microsoft.OpenApi.JsonSchemaReference.Extensions.set -> void +Microsoft.OpenApi.OpenApiSchemaReference.Extensions.set -> void +override Microsoft.OpenApi.OpenApiSchemaReference.SerializeAsV32(Microsoft.OpenApi.IOpenApiWriter! writer) -> void diff --git a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV2JsonWorks_produceTerseOutput=False.verified.txt b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV2JsonWorks_produceTerseOutput=False.verified.txt new file mode 100644 index 000000000..ddb324ad6 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV2JsonWorks_produceTerseOutput=False.verified.txt @@ -0,0 +1,3 @@ +{ + "$ref": "#/definitions/Pet" +} diff --git a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV2JsonWorks_produceTerseOutput=True.verified.txt b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV2JsonWorks_produceTerseOutput=True.verified.txt new file mode 100644 index 000000000..b36112c01 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV2JsonWorks_produceTerseOutput=True.verified.txt @@ -0,0 +1 @@ +{"$ref":"#/definitions/Pet"} diff --git a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV31JsonWorks_produceTerseOutput=False.verified.txt b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV31JsonWorks_produceTerseOutput=False.verified.txt index 3d7372e1b..87dbbe91f 100644 --- a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV31JsonWorks_produceTerseOutput=False.verified.txt +++ b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV31JsonWorks_produceTerseOutput=False.verified.txt @@ -7,5 +7,6 @@ "examples": [ "reference example" ], + "x-custom": "custom value", "$ref": "#/components/schemas/Pet" } \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV31JsonWorks_produceTerseOutput=True.verified.txt b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV31JsonWorks_produceTerseOutput=True.verified.txt index 2a7cc8e44..c7f7f3cd0 100644 --- a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV31JsonWorks_produceTerseOutput=True.verified.txt +++ b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV31JsonWorks_produceTerseOutput=True.verified.txt @@ -1 +1 @@ -{"description":"Reference Description","default":"reference default","title":"Reference Title","deprecated":true,"readOnly":true,"examples":["reference example"],"$ref":"#/components/schemas/Pet"} \ No newline at end of file +{"description":"Reference Description","default":"reference default","title":"Reference Title","deprecated":true,"readOnly":true,"examples":["reference example"],"x-custom":"custom value","$ref":"#/components/schemas/Pet"} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV32JsonWorks_produceTerseOutput=False.verified.txt b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV32JsonWorks_produceTerseOutput=False.verified.txt new file mode 100644 index 000000000..87dbbe91f --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV32JsonWorks_produceTerseOutput=False.verified.txt @@ -0,0 +1,12 @@ +{ + "description": "Reference Description", + "default": "reference default", + "title": "Reference Title", + "deprecated": true, + "readOnly": true, + "examples": [ + "reference example" + ], + "x-custom": "custom value", + "$ref": "#/components/schemas/Pet" +} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV32JsonWorks_produceTerseOutput=True.verified.txt b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV32JsonWorks_produceTerseOutput=True.verified.txt new file mode 100644 index 000000000..c7f7f3cd0 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV32JsonWorks_produceTerseOutput=True.verified.txt @@ -0,0 +1 @@ +{"description":"Reference Description","default":"reference default","title":"Reference Title","deprecated":true,"readOnly":true,"examples":["reference example"],"x-custom":"custom value","$ref":"#/components/schemas/Pet"} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.cs b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.cs index 57ccae0cb..488488518 100644 --- a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.cs @@ -133,7 +133,11 @@ public async Task SerializeSchemaReferenceAsV31JsonWorks(bool produceTerseOutput WriteOnly = false, Deprecated = true, Default = JsonValue.Create("reference default"), - Examples = new List { JsonValue.Create("reference example") } + Examples = new List { JsonValue.Create("reference example") }, + Extensions = new Dictionary + { + ["x-custom"] = new JsonNodeExtension(JsonValue.Create("custom value")) + } }; var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); @@ -150,7 +154,7 @@ public async Task SerializeSchemaReferenceAsV31JsonWorks(bool produceTerseOutput [Theory] [InlineData(true)] [InlineData(false)] - public async Task SerializeSchemaReferenceAsV3JsonWorks(bool produceTerseOutput) + public async Task SerializeSchemaReferenceAsV32JsonWorks(bool produceTerseOutput) { // Arrange var reference = new OpenApiSchemaReference("Pet", null) @@ -161,7 +165,43 @@ public async Task SerializeSchemaReferenceAsV3JsonWorks(bool produceTerseOutput) WriteOnly = false, Deprecated = true, Default = JsonValue.Create("reference default"), - Examples = new List { JsonValue.Create("reference example") } + Examples = new List { JsonValue.Create("reference example") }, + Extensions = new Dictionary + { + ["x-custom"] = new JsonNodeExtension(JsonValue.Create("custom value")) + } + }; + + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputStringWriter, new OpenApiJsonWriterSettings { Terse = produceTerseOutput }); + + // Act + reference.SerializeAsV32(writer); + await writer.FlushAsync(); + + // Assert + await Verifier.Verify(outputStringWriter).UseParameters(produceTerseOutput); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task SerializeSchemaReferenceAsV3JsonWorks(bool produceTerseOutput) + { + // Arrange - Extensions should NOT appear in v3.0 output + var reference = new OpenApiSchemaReference("Pet", null) + { + Title = "Reference Title", + Description = "Reference Description", + ReadOnly = true, + WriteOnly = false, + Deprecated = true, + Default = JsonValue.Create("reference default"), + Examples = new List { JsonValue.Create("reference example") }, + Extensions = new Dictionary + { + ["x-custom"] = new JsonNodeExtension(JsonValue.Create("custom value")) + } }; var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); @@ -175,6 +215,38 @@ public async Task SerializeSchemaReferenceAsV3JsonWorks(bool produceTerseOutput) await Verifier.Verify(outputStringWriter).UseParameters(produceTerseOutput); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task SerializeSchemaReferenceAsV2JsonWorks(bool produceTerseOutput) + { + // Arrange - Extensions should NOT appear in v2 output + var reference = new OpenApiSchemaReference("Pet", null) + { + Title = "Reference Title", + Description = "Reference Description", + ReadOnly = true, + WriteOnly = false, + Deprecated = true, + Default = JsonValue.Create("reference default"), + Examples = new List { JsonValue.Create("reference example") }, + Extensions = new Dictionary + { + ["x-custom"] = new JsonNodeExtension(JsonValue.Create("custom value")) + } + }; + + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputStringWriter, new OpenApiJsonWriterSettings { Terse = produceTerseOutput }); + + // Act + reference.SerializeAsV2(writer); + await writer.FlushAsync(); + + // Assert + await Verifier.Verify(outputStringWriter).UseParameters(produceTerseOutput); + } + [Fact] public void ParseSchemaReferenceWithAnnotationsWorks() { @@ -256,5 +328,120 @@ public void ParseSchemaReferenceWithAnnotationsWorks() Assert.Equal("Original Pet Title", targetSchema.Title); Assert.Equal("Original Pet Description", targetSchema.Description); } + + [Fact] + public void ParseSchemaReferenceWithExtensionsWorks() + { + // Arrange + var jsonContent = @"{ + ""openapi"": ""3.1.0"", + ""info"": { + ""title"": ""Test API"", + ""version"": ""1.0.0"" + }, + ""paths"": { + ""/test"": { + ""get"": { + ""responses"": { + ""200"": { + ""description"": ""OK"", + ""content"": { + ""application/json"": { + ""schema"": { + ""$ref"": ""#/components/schemas/Pet"", + ""description"": ""A pet object"", + ""x-custom-extension"": ""custom value"", + ""x-another-extension"": 42 + } + } + } + } + } + } + } + }, + ""components"": { + ""schemas"": { + ""Pet"": { + ""type"": ""object"", + ""properties"": { + ""name"": { + ""type"": ""string"" + } + } + } + } + } +}"; + + // Act + var readResult = OpenApiDocument.Parse(jsonContent, "json"); + var document = readResult.Document; + + // Assert + Assert.NotNull(document); + Assert.Empty(readResult.Diagnostic.Errors); + + var schema = document.Paths["/test"].Operations[HttpMethod.Get] + .Responses["200"].Content["application/json"].Schema; + + Assert.IsType(schema); + var schemaRef = (OpenApiSchemaReference)schema; + + // Test that reference-level extensions are parsed + Assert.NotNull(schemaRef.Extensions); + Assert.Contains("x-custom-extension", schemaRef.Extensions.Keys); + Assert.Contains("x-another-extension", schemaRef.Extensions.Keys); + } + + [Fact] + public async Task SchemaReferenceExtensionsNotWrittenInV30() + { + // Arrange + var reference = new OpenApiSchemaReference("Pet", null) + { + Description = "Local description", + Extensions = new Dictionary + { + ["x-custom"] = new JsonNodeExtension(JsonValue.Create("custom value")) + } + }; + + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputStringWriter, new OpenApiJsonWriterSettings { Terse = true }); + + // Act + reference.SerializeAsV3(writer); + await writer.FlushAsync(); + var output = outputStringWriter.ToString(); + + // Assert: In v3.0, ONLY $ref should appear - no description, no extensions + Assert.Equal(@"{""$ref"":""#/components/schemas/Pet""}", output); + } + + [Fact] + public async Task SchemaReferenceExtensionsNotWrittenInV2() + { + // Arrange + var reference = new OpenApiSchemaReference("Pet", null) + { + Description = "Local description", + Extensions = new Dictionary + { + ["x-custom"] = new JsonNodeExtension(JsonValue.Create("custom value")) + } + }; + + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputStringWriter, new OpenApiJsonWriterSettings { Terse = true }); + + // Act + reference.SerializeAsV2(writer); + await writer.FlushAsync(); + var output = outputStringWriter.ToString(); + + // Assert: In v2, ONLY $ref should appear - no description, no extensions + Assert.Equal(@"{""$ref"":""#/definitions/Pet""}", output); + } } } From 19862b83760a3c46de98f5881dc58549e17a97ed Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 20 Mar 2026 12:44:18 -0400 Subject: [PATCH 6/9] chore: removes 32 API surface after migration Co-authored-by: Vincent Biret --- .../Models/References/OpenApiSchemaReference.cs | 6 ------ src/Microsoft.OpenApi/PublicAPI.Unshipped.txt | 1 - 2 files changed, 7 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs b/src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs index 67eb79645..104f4791b 100644 --- a/src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs +++ b/src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs @@ -176,12 +176,6 @@ public override void SerializeAsV31(IOpenApiWriter writer) SerializeAsWithoutLoops(writer, (w, element) => (element is IOpenApiSchema s ? CopyReferenceAsTargetElementWithOverrides(s) : element).SerializeAsV31(w)); } - /// - public override void SerializeAsV32(IOpenApiWriter writer) - { - SerializeAsWithoutLoops(writer, (w, element) => (element is IOpenApiSchema s ? CopyReferenceAsTargetElementWithOverrides(s) : element).SerializeAsV32(w)); - } - /// public override void SerializeAsV3(IOpenApiWriter writer) { diff --git a/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt b/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt index 7b7b54aca..f0d9e15fb 100644 --- a/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt +++ b/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt @@ -2,4 +2,3 @@ Microsoft.OpenApi.JsonSchemaReference.Extensions.get -> System.Collections.Generic.IDictionary? Microsoft.OpenApi.JsonSchemaReference.Extensions.set -> void Microsoft.OpenApi.OpenApiSchemaReference.Extensions.set -> void -override Microsoft.OpenApi.OpenApiSchemaReference.SerializeAsV32(Microsoft.OpenApi.IOpenApiWriter! writer) -> void From c261ed708199853ff009539e809d8a2249f514b6 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 20 Mar 2026 12:49:09 -0400 Subject: [PATCH 7/9] tests: removes 32 extraneous tests Signed-off-by: Vincent Biret --- ...orks_produceTerseOutput=False.verified.txt | 12 ------- ...Works_produceTerseOutput=True.verified.txt | 1 - .../References/OpenApiSchemaReferenceTests.cs | 34 +------------------ 3 files changed, 1 insertion(+), 46 deletions(-) delete mode 100644 test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV32JsonWorks_produceTerseOutput=False.verified.txt delete mode 100644 test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV32JsonWorks_produceTerseOutput=True.verified.txt diff --git a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV32JsonWorks_produceTerseOutput=False.verified.txt b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV32JsonWorks_produceTerseOutput=False.verified.txt deleted file mode 100644 index 87dbbe91f..000000000 --- a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV32JsonWorks_produceTerseOutput=False.verified.txt +++ /dev/null @@ -1,12 +0,0 @@ -{ - "description": "Reference Description", - "default": "reference default", - "title": "Reference Title", - "deprecated": true, - "readOnly": true, - "examples": [ - "reference example" - ], - "x-custom": "custom value", - "$ref": "#/components/schemas/Pet" -} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV32JsonWorks_produceTerseOutput=True.verified.txt b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV32JsonWorks_produceTerseOutput=True.verified.txt deleted file mode 100644 index c7f7f3cd0..000000000 --- a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.SerializeSchemaReferenceAsV32JsonWorks_produceTerseOutput=True.verified.txt +++ /dev/null @@ -1 +0,0 @@ -{"description":"Reference Description","default":"reference default","title":"Reference Title","deprecated":true,"readOnly":true,"examples":["reference example"],"x-custom":"custom value","$ref":"#/components/schemas/Pet"} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.cs b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.cs index 488488518..5b46950e1 100644 --- a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System.Collections.Generic; @@ -151,38 +151,6 @@ public async Task SerializeSchemaReferenceAsV31JsonWorks(bool produceTerseOutput await Verifier.Verify(outputStringWriter).UseParameters(produceTerseOutput); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task SerializeSchemaReferenceAsV32JsonWorks(bool produceTerseOutput) - { - // Arrange - var reference = new OpenApiSchemaReference("Pet", null) - { - Title = "Reference Title", - Description = "Reference Description", - ReadOnly = true, - WriteOnly = false, - Deprecated = true, - Default = JsonValue.Create("reference default"), - Examples = new List { JsonValue.Create("reference example") }, - Extensions = new Dictionary - { - ["x-custom"] = new JsonNodeExtension(JsonValue.Create("custom value")) - } - }; - - var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); - var writer = new OpenApiJsonWriter(outputStringWriter, new OpenApiJsonWriterSettings { Terse = produceTerseOutput }); - - // Act - reference.SerializeAsV32(writer); - await writer.FlushAsync(); - - // Assert - await Verifier.Verify(outputStringWriter).UseParameters(produceTerseOutput); - } - [Theory] [InlineData(true)] [InlineData(false)] From fe13cd39ca9b7896f0065de0ae186d8223f8bb49 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 20 Mar 2026 18:13:48 +0000 Subject: [PATCH 8/9] chore: promote shipped APIs --- src/Microsoft.OpenApi/PublicAPI.Shipped.txt | 3 +++ src/Microsoft.OpenApi/PublicAPI.Unshipped.txt | 3 --- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.OpenApi/PublicAPI.Shipped.txt b/src/Microsoft.OpenApi/PublicAPI.Shipped.txt index 0007b11f6..2ad2d72f9 100644 --- a/src/Microsoft.OpenApi/PublicAPI.Shipped.txt +++ b/src/Microsoft.OpenApi/PublicAPI.Shipped.txt @@ -1897,3 +1897,6 @@ Microsoft.OpenApi.OpenApiSchema.UnevaluatedPropertiesSchema.get -> Microsoft.Ope Microsoft.OpenApi.OpenApiSchema.UnevaluatedPropertiesSchema.set -> void Microsoft.OpenApi.OpenApiSchemaReference.UnevaluatedPropertiesSchema.get -> Microsoft.OpenApi.IOpenApiSchema? const Microsoft.OpenApi.OpenApiConstants.PatternPropertiesExtension = "x-jsonschema-patternProperties" -> string! +Microsoft.OpenApi.JsonSchemaReference.Extensions.get -> System.Collections.Generic.IDictionary? +Microsoft.OpenApi.JsonSchemaReference.Extensions.set -> void +Microsoft.OpenApi.OpenApiSchemaReference.Extensions.set -> void diff --git a/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt b/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt index f0d9e15fb..7dc5c5811 100644 --- a/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt +++ b/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt @@ -1,4 +1 @@ #nullable enable -Microsoft.OpenApi.JsonSchemaReference.Extensions.get -> System.Collections.Generic.IDictionary? -Microsoft.OpenApi.JsonSchemaReference.Extensions.set -> void -Microsoft.OpenApi.OpenApiSchemaReference.Extensions.set -> void From ea1791e2c970447abccdb95c9784755efebe23ef Mon Sep 17 00:00:00 2001 From: "release-please-token-provider[bot]" <225477224+release-please-token-provider[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 18:14:06 +0000 Subject: [PATCH 9/9] chore(support/v2): release 2.7.1 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 9 +++++++++ Directory.Build.props | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d1328ca9c..0a163d740 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.7.0" + ".": "2.7.1" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d1a78155..9cd00394e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [2.7.1](https://github.com/microsoft/OpenAPI.NET/compare/v2.7.0...v2.7.1) (2026-03-20) + + +### Bug Fixes + +* a bug where path parameter validation would fail if they contained forbidden JSON pointer characters ([2efd2b2](https://github.com/microsoft/OpenAPI.NET/commit/2efd2b2e48be072f092115dface6cea3e0ba7182)) +* **library:** do not emit unevaluatedProperties for non-object schemas ([28d614f](https://github.com/microsoft/OpenAPI.NET/commit/28d614f3a8a4e98cfe202ab0f1ff8d75241bbdf8)) +* **library:** enforce spec-compliant $ref serialization; add Extensions support for schema references in v3.1/v3.2 ([7640a8a](https://github.com/microsoft/OpenAPI.NET/commit/7640a8aacea475505b64c9dc60f73f905909f5e7)) + ## [2.7.0](https://github.com/microsoft/OpenAPI.NET/compare/v2.6.1...v2.7.0) (2026-03-05) diff --git a/Directory.Build.props b/Directory.Build.props index 127a16526..efd2c5430 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -12,7 +12,7 @@ https://github.com/Microsoft/OpenAPI.NET © Microsoft Corporation. All rights reserved. OpenAPI .NET - 2.7.0 + 2.7.1