From 2495dea83fef872752120edeb842be492a0723d4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 18:53:09 +0000 Subject: [PATCH 1/5] Initial plan From 50b44aad12d8fa86a46f7f6d62896a7b1c344d2e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 19:07:23 +0000 Subject: [PATCH 2/5] fix: wrap extension parser calls in try-catch to ensure correct error pointers When extension parsers throw OpenApiException, the exceptions are now caught in LoadExtension methods across all OpenAPI versions (V2, V3, V3.1, V3.2). This ensures the error pointer correctly includes all path segments (e.g., #/definitions/demo/x-tag instead of #/definitions/x-tag). Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../Reader/V2/OpenApiV2Deserializer.cs | 11 +- .../Reader/V3/OpenApiV3Deserializer.cs | 23 ++-- .../Reader/V31/OpenApiV31Deserializer.cs | 17 ++- .../Reader/V32/OpenApiV32Deserializer.cs | 17 ++- .../TestCustomExtension.cs | 102 ++++++++++++++++++ 5 files changed, 156 insertions(+), 14 deletions(-) diff --git a/src/Microsoft.OpenApi/Reader/V2/OpenApiV2Deserializer.cs b/src/Microsoft.OpenApi/Reader/V2/OpenApiV2Deserializer.cs index c640b310c..2b075c1bc 100644 --- a/src/Microsoft.OpenApi/Reader/V2/OpenApiV2Deserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V2/OpenApiV2Deserializer.cs @@ -79,7 +79,16 @@ private static IOpenApiExtension LoadExtension(string name, ParseNode node) { if (node.Context.ExtensionParsers is not null && node.Context.ExtensionParsers.TryGetValue(name, out var parser)) { - return parser(node.CreateAny(), OpenApiSpecVersion.OpenApi2_0); + try + { + return parser(node.CreateAny(), OpenApiSpecVersion.OpenApi2_0); + } + catch (OpenApiException ex) + { + ex.Pointer = node.Context.GetLocation(); + node.Context.Diagnostic.Errors.Add(new(ex)); + return new JsonNodeExtension(node.CreateAny()); + } } else { diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiV3Deserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiV3Deserializer.cs index 0b74cedc5..1a03268d6 100644 --- a/src/Microsoft.OpenApi/Reader/V3/OpenApiV3Deserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiV3Deserializer.cs @@ -130,15 +130,24 @@ public static JsonNodeExtension LoadAny(ParseNode node, OpenApiDocument hostDocu private static IOpenApiExtension LoadExtension(string name, ParseNode node) { - if (node.Context.ExtensionParsers is not null && node.Context.ExtensionParsers.TryGetValue(name, out var parser) && parser( - node.CreateAny(), OpenApiSpecVersion.OpenApi3_0) is { } result) + if (node.Context.ExtensionParsers is not null && node.Context.ExtensionParsers.TryGetValue(name, out var parser)) { - return result; - } - else - { - return new JsonNodeExtension(node.CreateAny()); + try + { + var result = parser(node.CreateAny(), OpenApiSpecVersion.OpenApi3_0); + if (result is { }) + { + return result; + } + } + catch (OpenApiException ex) + { + ex.Pointer = node.Context.GetLocation(); + node.Context.Diagnostic.Errors.Add(new(ex)); + } } + + return new JsonNodeExtension(node.CreateAny()); } private static string? LoadString(ParseNode node) diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiV31Deserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiV31Deserializer.cs index 08f2ec048..3608ad5f7 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiV31Deserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiV31Deserializer.cs @@ -131,9 +131,20 @@ public static JsonNode LoadAny(ParseNode node, OpenApiDocument hostDocument) private static IOpenApiExtension LoadExtension(string name, ParseNode node) { - return node.Context.ExtensionParsers is not null && node.Context.ExtensionParsers.TryGetValue(name, out var parser) - ? parser(node.CreateAny(), OpenApiSpecVersion.OpenApi3_1) - : new JsonNodeExtension(node.CreateAny()); + if (node.Context.ExtensionParsers is not null && node.Context.ExtensionParsers.TryGetValue(name, out var parser)) + { + try + { + return parser(node.CreateAny(), OpenApiSpecVersion.OpenApi3_1); + } + catch (OpenApiException ex) + { + ex.Pointer = node.Context.GetLocation(); + node.Context.Diagnostic.Errors.Add(new(ex)); + } + } + + return new JsonNodeExtension(node.CreateAny()); } private static string? LoadString(ParseNode node) diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiV32Deserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiV32Deserializer.cs index 9b062931d..55f45b38e 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiV32Deserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiV32Deserializer.cs @@ -131,9 +131,20 @@ public static JsonNode LoadAny(ParseNode node, OpenApiDocument hostDocument) private static IOpenApiExtension LoadExtension(string name, ParseNode node) { - return node.Context.ExtensionParsers is not null && node.Context.ExtensionParsers.TryGetValue(name, out var parser) - ? parser(node.CreateAny(), OpenApiSpecVersion.OpenApi3_2) - : new JsonNodeExtension(node.CreateAny()); + if (node.Context.ExtensionParsers is not null && node.Context.ExtensionParsers.TryGetValue(name, out var parser)) + { + try + { + return parser(node.CreateAny(), OpenApiSpecVersion.OpenApi3_2); + } + catch (OpenApiException ex) + { + ex.Pointer = node.Context.GetLocation(); + node.Context.Diagnostic.Errors.Add(new(ex)); + } + } + + return new JsonNodeExtension(node.CreateAny()); } private static string? LoadString(ParseNode node) diff --git a/test/Microsoft.OpenApi.Readers.Tests/TestCustomExtension.cs b/test/Microsoft.OpenApi.Readers.Tests/TestCustomExtension.cs index 57f55e95e..1964ea61b 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/TestCustomExtension.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/TestCustomExtension.cs @@ -44,6 +44,108 @@ public void ParseCustomExtension() Assert.Equal("hey", fooExtension.Bar); Assert.Equal("hi!", fooExtension.Baz); } + + [Fact] + public void ExtensionParserThrowingOpenApiException_V2_ShouldHaveCorrectPointer() + { + var json = @"{ + ""swagger"": ""2.0"", + ""info"": { + ""title"": ""Demo"", + ""version"": ""1"" + }, + ""paths"": {}, + ""definitions"": { + ""demo"": { + ""x-tag"": null + } + } +}"; + var settings = new OpenApiReaderSettings + { + ExtensionParsers = + { + { "x-tag", (any, version) => throw new OpenApiException("Testing") } + } + }; + + var result = OpenApiDocument.Parse(json, "json", settings); + + Assert.NotNull(result.Diagnostic); + Assert.NotEmpty(result.Diagnostic.Errors); + var error = result.Diagnostic.Errors[0]; + Assert.Equal("Testing", error.Message); + Assert.Equal("#/definitions/demo/x-tag", error.Pointer); + } + + [Fact] + public void ExtensionParserThrowingOpenApiException_V3_ShouldHaveCorrectPointer() + { + var json = @"{ + ""openapi"": ""3.0.0"", + ""info"": { + ""title"": ""Demo"", + ""version"": ""1"" + }, + ""paths"": {}, + ""components"": { + ""schemas"": { + ""demo"": { + ""x-tag"": null + } + } + } +}"; + var settings = new OpenApiReaderSettings + { + ExtensionParsers = + { + { "x-tag", (any, version) => throw new OpenApiException("Testing") } + } + }; + + var result = OpenApiDocument.Parse(json, "json", settings); + + Assert.NotNull(result.Diagnostic); + Assert.NotEmpty(result.Diagnostic.Errors); + var error = result.Diagnostic.Errors[0]; + Assert.Equal("Testing", error.Message); + Assert.Equal("#/components/schemas/demo/x-tag", error.Pointer); + } + + [Fact] + public void ExtensionParserThrowingOpenApiException_V31_ShouldHaveCorrectPointer() + { + var json = @"{ + ""openapi"": ""3.1.0"", + ""info"": { + ""title"": ""Demo"", + ""version"": ""1"" + }, + ""components"": { + ""schemas"": { + ""demo"": { + ""x-tag"": null + } + } + } +}"; + var settings = new OpenApiReaderSettings + { + ExtensionParsers = + { + { "x-tag", (any, version) => throw new OpenApiException("Testing") } + } + }; + + var result = OpenApiDocument.Parse(json, "json", settings); + + Assert.NotNull(result.Diagnostic); + Assert.NotEmpty(result.Diagnostic.Errors); + var error = result.Diagnostic.Errors[0]; + Assert.Equal("Testing", error.Message); + Assert.Equal("#/components/schemas/demo/x-tag", error.Pointer); + } } internal class FooExtension : IOpenApiExtension, IOpenApiElement From 12f649959fe73d76c36a51f0126b41f14f4823fd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 19:10:01 +0000 Subject: [PATCH 3/5] refactor: make V2 LoadExtension consistent with other versions Moved the return statement for JsonNodeExtension outside the catch block to match the pattern used in V3, V31, and V32 deserializers. Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- src/Microsoft.OpenApi/Reader/V2/OpenApiV2Deserializer.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.OpenApi/Reader/V2/OpenApiV2Deserializer.cs b/src/Microsoft.OpenApi/Reader/V2/OpenApiV2Deserializer.cs index 2b075c1bc..80d079b5e 100644 --- a/src/Microsoft.OpenApi/Reader/V2/OpenApiV2Deserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V2/OpenApiV2Deserializer.cs @@ -87,13 +87,10 @@ private static IOpenApiExtension LoadExtension(string name, ParseNode node) { ex.Pointer = node.Context.GetLocation(); node.Context.Diagnostic.Errors.Add(new(ex)); - return new JsonNodeExtension(node.CreateAny()); } } - else - { - return new JsonNodeExtension(node.CreateAny()); - } + + return new JsonNodeExtension(node.CreateAny()); } private static string? LoadString(ParseNode node) From e85698a4172a874c33eba6c902d60f975e51e887 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 19:20:54 +0000 Subject: [PATCH 4/5] style: use raw string literals for JSON test data Changed from verbatim string literals (@"...") to raw string literals ("""...""") for consistency with existing test code. Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../TestCustomExtension.cs | 68 ++++++++++--------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/test/Microsoft.OpenApi.Readers.Tests/TestCustomExtension.cs b/test/Microsoft.OpenApi.Readers.Tests/TestCustomExtension.cs index 1964ea61b..b12a56d31 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/TestCustomExtension.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/TestCustomExtension.cs @@ -48,19 +48,21 @@ public void ParseCustomExtension() [Fact] public void ExtensionParserThrowingOpenApiException_V2_ShouldHaveCorrectPointer() { - var json = @"{ - ""swagger"": ""2.0"", - ""info"": { - ""title"": ""Demo"", - ""version"": ""1"" + var json = """ +{ + "swagger": "2.0", + "info": { + "title": "Demo", + "version": "1" }, - ""paths"": {}, - ""definitions"": { - ""demo"": { - ""x-tag"": null + "paths": {}, + "definitions": { + "demo": { + "x-tag": null } } -}"; +} +"""; var settings = new OpenApiReaderSettings { ExtensionParsers = @@ -81,21 +83,23 @@ public void ExtensionParserThrowingOpenApiException_V2_ShouldHaveCorrectPointer( [Fact] public void ExtensionParserThrowingOpenApiException_V3_ShouldHaveCorrectPointer() { - var json = @"{ - ""openapi"": ""3.0.0"", - ""info"": { - ""title"": ""Demo"", - ""version"": ""1"" + var json = """ +{ + "openapi": "3.0.0", + "info": { + "title": "Demo", + "version": "1" }, - ""paths"": {}, - ""components"": { - ""schemas"": { - ""demo"": { - ""x-tag"": null + "paths": {}, + "components": { + "schemas": { + "demo": { + "x-tag": null } } } -}"; +} +"""; var settings = new OpenApiReaderSettings { ExtensionParsers = @@ -116,20 +120,22 @@ public void ExtensionParserThrowingOpenApiException_V3_ShouldHaveCorrectPointer( [Fact] public void ExtensionParserThrowingOpenApiException_V31_ShouldHaveCorrectPointer() { - var json = @"{ - ""openapi"": ""3.1.0"", - ""info"": { - ""title"": ""Demo"", - ""version"": ""1"" + var json = """ +{ + "openapi": "3.1.0", + "info": { + "title": "Demo", + "version": "1" }, - ""components"": { - ""schemas"": { - ""demo"": { - ""x-tag"": null + "components": { + "schemas": { + "demo": { + "x-tag": null } } } -}"; +} +"""; var settings = new OpenApiReaderSettings { ExtensionParsers = From 8021a752fb5516ccf749bd3628f860bf3f0848c0 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 22 Dec 2025 14:42:22 -0500 Subject: [PATCH 5/5] chore: refactors test definition for better coverage Signed-off-by: Vincent Biret --- .../TestCustomExtension.cs | 47 +++---------------- 1 file changed, 7 insertions(+), 40 deletions(-) diff --git a/test/Microsoft.OpenApi.Readers.Tests/TestCustomExtension.cs b/test/Microsoft.OpenApi.Readers.Tests/TestCustomExtension.cs index b12a56d31..9f4404c05 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/TestCustomExtension.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/TestCustomExtension.cs @@ -80,12 +80,15 @@ public void ExtensionParserThrowingOpenApiException_V2_ShouldHaveCorrectPointer( Assert.Equal("#/definitions/demo/x-tag", error.Pointer); } - [Fact] - public void ExtensionParserThrowingOpenApiException_V3_ShouldHaveCorrectPointer() + [Theory] + [InlineData("3.0.4")] + [InlineData("3.1.1")] + [InlineData("3.2.0")] + public void ExtensionParserThrowingOpenApiException_V3_ShouldHaveCorrectPointer(string version) { - var json = """ + var json = $$""" { - "openapi": "3.0.0", + "openapi": "{{version}}", "info": { "title": "Demo", "version": "1" @@ -116,42 +119,6 @@ public void ExtensionParserThrowingOpenApiException_V3_ShouldHaveCorrectPointer( Assert.Equal("Testing", error.Message); Assert.Equal("#/components/schemas/demo/x-tag", error.Pointer); } - - [Fact] - public void ExtensionParserThrowingOpenApiException_V31_ShouldHaveCorrectPointer() - { - var json = """ -{ - "openapi": "3.1.0", - "info": { - "title": "Demo", - "version": "1" - }, - "components": { - "schemas": { - "demo": { - "x-tag": null - } - } - } -} -"""; - var settings = new OpenApiReaderSettings - { - ExtensionParsers = - { - { "x-tag", (any, version) => throw new OpenApiException("Testing") } - } - }; - - var result = OpenApiDocument.Parse(json, "json", settings); - - Assert.NotNull(result.Diagnostic); - Assert.NotEmpty(result.Diagnostic.Errors); - var error = result.Diagnostic.Errors[0]; - Assert.Equal("Testing", error.Message); - Assert.Equal("#/components/schemas/demo/x-tag", error.Pointer); - } } internal class FooExtension : IOpenApiExtension, IOpenApiElement