From 3caf35d7184e51a211275f2ac65443bbf44dac78 Mon Sep 17 00:00:00 2001 From: martincostello Date: Fri, 8 Aug 2025 14:17:58 +0100 Subject: [PATCH 1/8] Validate OpenAPI schema references Add validation rule for OpenAPI document schema references. Resolves #2453. --- .../Properties/SRResource.Designer.cs | 9 ++ .../Properties/SRResource.resx | 3 + .../Validations/Rules/OpenApiDocumentRules.cs | 104 +++++++++++++++++- .../PublicApi/PublicApi.approved.txt | 1 + .../Validations/ValidationRuleSetTests.cs | 4 +- 5 files changed, 117 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs b/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs index 3de246750..bd42672a5 100644 --- a/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs +++ b/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs @@ -384,6 +384,15 @@ internal static string Validation_RuleAddTwice { } } + /// + /// Looks up a localized string similar to The schema reference '{0}' does not point to an existing schema.. + /// + internal static string Validation_SchemaReferenceDoesNotExist { + get { + return ResourceManager.GetString("Validation_SchemaReferenceDoesNotExist", resourceCulture); + } + } + /// /// Looks up a localized string similar to Schema {0} must contain property specified in the discriminator {1} in the required field list.. /// diff --git a/src/Microsoft.OpenApi/Properties/SRResource.resx b/src/Microsoft.OpenApi/Properties/SRResource.resx index 6cd1fc2ba..b758ff89b 100644 --- a/src/Microsoft.OpenApi/Properties/SRResource.resx +++ b/src/Microsoft.OpenApi/Properties/SRResource.resx @@ -231,6 +231,9 @@ Value '{0}' is not a valid value for variable '{1}'. If an enum is provided, it should not be empty and the value provided should exist in the enum + + The schema reference '{0}' does not point to an existing schema. + The argument '{0}' is null. diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs index a0bff927e..3cfb3a291 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs @@ -1,7 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -using System; +using System.Text.Json.Nodes; +using Microsoft.OpenApi.Reader; namespace Microsoft.OpenApi { @@ -23,9 +24,108 @@ public static class OpenApiDocumentRules if (item.Info == null) { context.CreateError(nameof(OpenApiDocumentFieldIsMissing), - String.Format(SRResource.Validation_FieldIsRequired, "info", "document")); + string.Format(SRResource.Validation_FieldIsRequired, "info", "document")); } context.Exit(); }); + + /// + /// All references in the OpenAPI document must be valid. + /// + public static ValidationRule OpenApiDocumentReferencesAreValid => + new(nameof(OpenApiDocumentReferencesAreValid), + static (context, item) => + { + const string RuleName = nameof(OpenApiDocumentReferencesAreValid); + + JsonNode document; + + using (var textWriter = new System.IO.StringWriter()) + { + var writer = new OpenApiJsonWriter(textWriter); + + item.SerializeAsV31(writer); + + var json = textWriter.ToString(); + + document = JsonNode.Parse(json)!; + } + + var visitor = new OpenApiSchemaReferenceVisitor(RuleName, context, document); + var walker = new OpenApiWalker(visitor); + + walker.Walk(item); + }); + + private sealed class OpenApiSchemaReferenceVisitor( + string ruleName, + IValidationContext context, + JsonNode document) : OpenApiVisitorBase + { + public override void Visit(IOpenApiReferenceHolder referenceHolder) + { + if (referenceHolder is OpenApiSchemaReference { Reference.IsLocal: true } reference) + { + ValidateSchemaReference(reference); + } + } + + public override void Visit(IOpenApiSchema schema) + { + if (schema is OpenApiSchemaReference { Reference.IsLocal: true } reference) + { + ValidateSchemaReference(reference); + } + } + + private void ValidateSchemaReference(OpenApiSchemaReference reference) + { + var id = reference.Reference.ReferenceV3; + + if (id is { Length: > 0 } && !IsValidSchemaReference(id, document)) + { + var isValid = false; + + // Sometimes ReferenceV3 is not a JSON valid JSON pointer, but the $ref + // associated with it still points to a valid location in the document. + // In these cases, we need to find it manually to verify that fact before + // generating a warning that the schema reference is indeed invalid. + // TODO Why is this, and can it be avoided? + var parent = Find(PathString, document); + + if (parent?["$ref"] is { } @ref && + @ref.GetValueKind() is System.Text.Json.JsonValueKind.String && + @ref.GetValue() is { Length: > 0 } refId) + { + id = refId; + isValid = IsValidSchemaReference(id, document); + } + + if (!isValid) + { + // Trim off the leading "#/" as the context is already at the root of the document + var segment = +#if NET8_0_OR_GREATER + PathString[2..]; +#else + PathString.Substring(2); +#endif + + context.Enter(segment); + context.CreateWarning(ruleName, string.Format(SRResource.Validation_SchemaReferenceDoesNotExist, id)); + context.Exit(); + } + } + + static bool IsValidSchemaReference(string id, JsonNode baseNode) + => Find(id, baseNode) is not null; + + static JsonNode? Find(string id, JsonNode baseNode) + { + var pointer = new JsonPointer(id.Replace("#/", "/")); + return pointer.Find(baseNode); + } + } + } } } diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt index 891d0d4f4..f8d83d291 100644 --- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt +++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt @@ -627,6 +627,7 @@ namespace Microsoft.OpenApi public static class OpenApiDocumentRules { public static Microsoft.OpenApi.ValidationRule OpenApiDocumentFieldIsMissing { get; } + public static Microsoft.OpenApi.ValidationRule OpenApiDocumentReferencesAreValid { get; } } public static class OpenApiElementExtensions { diff --git a/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs b/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs index 45a2dfcad..e1414232e 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs @@ -53,8 +53,8 @@ public void RuleSetConstructorsReturnsTheCorrectRules() Assert.Empty(ruleSet_4.Rules); // Update the number if you add new default rule(s). - Assert.Equal(19, ruleSet_1.Rules.Count); - Assert.Equal(19, ruleSet_2.Rules.Count); + Assert.Equal(20, ruleSet_1.Rules.Count); + Assert.Equal(20, ruleSet_2.Rules.Count); Assert.Equal(3, ruleSet_3.Rules.Count); } From ffbe7c992bcfe94543046da407c27ed44a7257a4 Mon Sep 17 00:00:00 2001 From: martincostello Date: Fri, 8 Aug 2025 16:56:24 +0100 Subject: [PATCH 2/8] Add unit tests Add two basic unit tests and a fast path for when the components are registered. --- .../Validations/Rules/OpenApiDocumentRules.cs | 6 + .../OpenApiDocumentValidationTests.cs | 134 ++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 test/Microsoft.OpenApi.Tests/Validations/OpenApiDocumentValidationTests.cs diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs index 3cfb3a291..868fd3874 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs @@ -80,6 +80,12 @@ public override void Visit(IOpenApiSchema schema) private void ValidateSchemaReference(OpenApiSchemaReference reference) { + if (reference.RecursiveTarget is not null) + { + // The reference was followed to a valid schema somewhere in the document + return; + } + var id = reference.Reference.ReferenceV3; if (id is { Length: > 0 } && !IsValidSchemaReference(id, document)) diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiDocumentValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiDocumentValidationTests.cs new file mode 100644 index 000000000..c6865eeec --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiDocumentValidationTests.cs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using Xunit; + +namespace Microsoft.OpenApi.Validations.Tests; + +public static class OpenApiDocumentValidationTests +{ + [Fact] + public static void ValidateSchemaReferencesAreValid() + { + // Arrange + var document = new OpenApiDocument + { + Components = new OpenApiComponents(), + Info = new OpenApiInfo + { + Title = "People Document", + Version = "1.0.0" + }, + Paths = [], + Workspace = new() + }; + + document.AddComponent("Person", new OpenApiSchema + { + Type = JsonSchemaType.Object, + Properties = new Dictionary() + { + ["name"] = new OpenApiSchema { Type = JsonSchemaType.String }, + ["email"] = new OpenApiSchema { Type = JsonSchemaType.String, Format = "email" } + } + }); + + document.Paths.Add("/people", new OpenApiPathItem + { + Operations = new Dictionary() + { + [HttpMethod.Get] = new OpenApiOperation + { + Responses = new() + { + ["200"] = new OpenApiResponse + { + Description = "OK", + Content = new Dictionary() + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchemaReference("Person", document), + } + } + } + } + } + } + }); + + // Act + var errors = document.Validate(ValidationRuleSet.GetDefaultRuleSet()); + var result = !errors.Any(); + + // Assert + Assert.True(result); + Assert.NotNull(errors); + Assert.Empty(errors); + } + + [Fact] + public static void ValidateSchemaReferencesAreInvalid() + { + // Arrange + var document = new OpenApiDocument + { + Components = new OpenApiComponents(), + Info = new OpenApiInfo + { + Title = "Pets Document", + Version = "1.0.0" + }, + Paths = [], + Workspace = new() + }; + + document.AddComponent("Person", new OpenApiSchema + { + Type = JsonSchemaType.Object, + Properties = new Dictionary() + { + ["name"] = new OpenApiSchema { Type = JsonSchemaType.String }, + ["email"] = new OpenApiSchema { Type = JsonSchemaType.String, Format = "email" } + } + }); + + document.Paths.Add("/pets", new OpenApiPathItem + { + Operations = new Dictionary() + { + [HttpMethod.Get] = new OpenApiOperation + { + Responses = new() + { + ["200"] = new OpenApiResponse + { + Description = "OK", + Content = new Dictionary() + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchemaReference("Pet", document), + } + } + } + } + } + } + }); + + // Act + var errors = document.Validate(ValidationRuleSet.GetDefaultRuleSet()); + var result = !errors.Any(); + + // Assert + Assert.False(result); + Assert.NotNull(errors); + var error = Assert.Single(errors); + Assert.Equal("The schema reference '#/components/schemas/Pet' does not point to an existing schema.", error.Message); + Assert.Equal("#/paths/~1pets/get/responses/200/content/application~1json/schema", error.Pointer); + } +} From a83318c4d932bde15ebce819bb8efb0f7e435b75 Mon Sep 17 00:00:00 2001 From: martincostello Date: Fri, 8 Aug 2025 17:20:02 +0100 Subject: [PATCH 3/8] Improve reference validator - Improve handling of circular references. - Improve the path. --- .../Validations/Rules/OpenApiDocumentRules.cs | 30 +++++++++++++------ .../OpenApiDocumentValidationTests.cs | 2 +- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs index 868fd3874..7b6e9c75f 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System; using System.Text.Json.Nodes; using Microsoft.OpenApi.Reader; @@ -80,9 +81,28 @@ public override void Visit(IOpenApiSchema schema) private void ValidateSchemaReference(OpenApiSchemaReference reference) { - if (reference.RecursiveTarget is not null) + // Trim off the leading "#/" as the context is already at the root of the document + var segment = +#if NET8_0_OR_GREATER + $"{PathString[2..]}/$ref"; +#else + PathString.Substring(2) + "/$ref"; +#endif + + try + { + if (reference.RecursiveTarget is not null) + { + return; + } + } + catch (InvalidOperationException ex) { // The reference was followed to a valid schema somewhere in the document + context.Enter(segment); + context.CreateWarning(ruleName, ex.Message); + context.Exit(); + return; } @@ -109,14 +129,6 @@ private void ValidateSchemaReference(OpenApiSchemaReference reference) if (!isValid) { - // Trim off the leading "#/" as the context is already at the root of the document - var segment = -#if NET8_0_OR_GREATER - PathString[2..]; -#else - PathString.Substring(2); -#endif - context.Enter(segment); context.CreateWarning(ruleName, string.Format(SRResource.Validation_SchemaReferenceDoesNotExist, id)); context.Exit(); diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiDocumentValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiDocumentValidationTests.cs index c6865eeec..885093af6 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/OpenApiDocumentValidationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiDocumentValidationTests.cs @@ -129,6 +129,6 @@ public static void ValidateSchemaReferencesAreInvalid() Assert.NotNull(errors); var error = Assert.Single(errors); Assert.Equal("The schema reference '#/components/schemas/Pet' does not point to an existing schema.", error.Message); - Assert.Equal("#/paths/~1pets/get/responses/200/content/application~1json/schema", error.Pointer); + Assert.Equal("#/paths/~1pets/get/responses/200/content/application~1json/schema/$ref", error.Pointer); } } From 173e321bc7bc18d54824015268b43a2949456aee Mon Sep 17 00:00:00 2001 From: martincostello Date: Fri, 8 Aug 2025 17:35:11 +0100 Subject: [PATCH 4/8] Add circular reference test Add a test for circular schema references. --- .../OpenApiDocumentValidationTests.cs | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiDocumentValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiDocumentValidationTests.cs index 885093af6..467d5f3a7 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/OpenApiDocumentValidationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiDocumentValidationTests.cs @@ -131,4 +131,65 @@ public static void ValidateSchemaReferencesAreInvalid() Assert.Equal("The schema reference '#/components/schemas/Pet' does not point to an existing schema.", error.Message); Assert.Equal("#/paths/~1pets/get/responses/200/content/application~1json/schema/$ref", error.Pointer); } + + [Fact] + public static void ValidateCircularSchemaReferencesAreDetected() + { + // Arrange + var document = new OpenApiDocument + { + Components = new OpenApiComponents(), + Info = new OpenApiInfo + { + Title = "Infinite Document", + Version = "1.0.0" + }, + Paths = [], + Workspace = new() + }; + + document.AddComponent("Cycle", new OpenApiSchema + { + Type = JsonSchemaType.Object, + Properties = new Dictionary() + { + ["self"] = new OpenApiSchemaReference("#/components/schemas/Cycle/properties/self", document) + } + }); + + document.Paths.Add("/cycle", new OpenApiPathItem + { + Operations = new Dictionary() + { + [HttpMethod.Get] = new OpenApiOperation + { + Responses = new() + { + ["200"] = new OpenApiResponse + { + Description = "OK", + Content = new Dictionary() + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchemaReference("Cycle", document) + } + } + } + } + } + } + }); + + // Act + var errors = document.Validate(ValidationRuleSet.GetDefaultRuleSet()); + var result = !errors.Any(); + + // Assert + Assert.False(result); + Assert.NotNull(errors); + var error = Assert.Single(errors); + Assert.Equal("Circular reference detected while resolving schema: #/components/schemas/Cycle/properties/self", error.Message); + Assert.Equal("#/components/schemas/Cycle/properties/self/$ref", error.Pointer); + } } From 983457c59535746e8529283c66ff5b679d5c5e20 Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Fri, 8 Aug 2025 17:39:55 +0100 Subject: [PATCH 5/8] Fix comment Copy-pasted into the wrong place during a refactor. --- src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs index 7b6e9c75f..e50c27745 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs @@ -93,12 +93,12 @@ private void ValidateSchemaReference(OpenApiSchemaReference reference) { if (reference.RecursiveTarget is not null) { + // The reference was followed to a valid schema somewhere in the document return; } } catch (InvalidOperationException ex) { - // The reference was followed to a valid schema somewhere in the document context.Enter(segment); context.CreateWarning(ruleName, ex.Message); context.Exit(); From cd8d13bffa6450e50225a01892991ef8c4cd09af Mon Sep 17 00:00:00 2001 From: martincostello Date: Fri, 15 Aug 2025 17:48:33 +0100 Subject: [PATCH 6/8] Avoid allocating segment Avoid allocating the segment for the context if the reference is valid. --- .../Validations/Rules/OpenApiDocumentRules.cs | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs index e50c27745..59c9d2c6c 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs @@ -81,14 +81,6 @@ public override void Visit(IOpenApiSchema schema) private void ValidateSchemaReference(OpenApiSchemaReference reference) { - // Trim off the leading "#/" as the context is already at the root of the document - var segment = -#if NET8_0_OR_GREATER - $"{PathString[2..]}/$ref"; -#else - PathString.Substring(2) + "/$ref"; -#endif - try { if (reference.RecursiveTarget is not null) @@ -99,7 +91,7 @@ private void ValidateSchemaReference(OpenApiSchemaReference reference) } catch (InvalidOperationException ex) { - context.Enter(segment); + context.Enter(GetSegment()); context.CreateWarning(ruleName, ex.Message); context.Exit(); @@ -129,7 +121,7 @@ private void ValidateSchemaReference(OpenApiSchemaReference reference) if (!isValid) { - context.Enter(segment); + context.Enter(GetSegment()); context.CreateWarning(ruleName, string.Format(SRResource.Validation_SchemaReferenceDoesNotExist, id)); context.Exit(); } @@ -143,6 +135,17 @@ static bool IsValidSchemaReference(string id, JsonNode baseNode) var pointer = new JsonPointer(id.Replace("#/", "/")); return pointer.Find(baseNode); } + + string GetSegment() + { + // Trim off the leading "#/" as the context is already at the root of the document + return +#if NET8_0_OR_GREATER + $"{PathString[2..]}/$ref"; +#else + PathString.Substring(2) + "/$ref"; +#endif + } } } } From a22ac92bebab6d38fb05e39d92cc743208907f27 Mon Sep 17 00:00:00 2001 From: martincostello Date: Fri, 15 Aug 2025 17:49:39 +0100 Subject: [PATCH 7/8] Fix test Parse the document invariantly. --- src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs index 59c9d2c6c..c10567071 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs @@ -41,7 +41,7 @@ public static class OpenApiDocumentRules JsonNode document; - using (var textWriter = new System.IO.StringWriter()) + using (var textWriter = new System.IO.StringWriter(System.Globalization.CultureInfo.InvariantCulture)) { var writer = new OpenApiJsonWriter(textWriter); From 216d62b404f3043998ad232955d91b5fd9072e0b Mon Sep 17 00:00:00 2001 From: martincostello Date: Mon, 18 Aug 2025 15:40:00 +0100 Subject: [PATCH 8/8] Simplify OpenApiDocumentReferencesAreValid Remove reparsing of the document and just validate the in-memory `OpenApiDocument` instead. --- .../Validations/Rules/OpenApiDocumentRules.cs | 75 ++++--------------- 1 file changed, 13 insertions(+), 62 deletions(-) diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs index c10567071..d15b0a0a0 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs @@ -2,8 +2,6 @@ // Licensed under the MIT license. using System; -using System.Text.Json.Nodes; -using Microsoft.OpenApi.Reader; namespace Microsoft.OpenApi { @@ -39,20 +37,7 @@ public static class OpenApiDocumentRules { const string RuleName = nameof(OpenApiDocumentReferencesAreValid); - JsonNode document; - - using (var textWriter = new System.IO.StringWriter(System.Globalization.CultureInfo.InvariantCulture)) - { - var writer = new OpenApiJsonWriter(textWriter); - - item.SerializeAsV31(writer); - - var json = textWriter.ToString(); - - document = JsonNode.Parse(json)!; - } - - var visitor = new OpenApiSchemaReferenceVisitor(RuleName, context, document); + var visitor = new OpenApiSchemaReferenceVisitor(RuleName, context); var walker = new OpenApiWalker(visitor); walker.Walk(item); @@ -60,12 +45,11 @@ public static class OpenApiDocumentRules private sealed class OpenApiSchemaReferenceVisitor( string ruleName, - IValidationContext context, - JsonNode document) : OpenApiVisitorBase + IValidationContext context) : OpenApiVisitorBase { public override void Visit(IOpenApiReferenceHolder referenceHolder) { - if (referenceHolder is OpenApiSchemaReference { Reference.IsLocal: true } reference) + if (referenceHolder is OpenApiSchemaReference reference) { ValidateSchemaReference(reference); } @@ -73,7 +57,7 @@ public override void Visit(IOpenApiReferenceHolder referenceHolder) public override void Visit(IOpenApiSchema schema) { - if (schema is OpenApiSchemaReference { Reference.IsLocal: true } reference) + if (schema is OpenApiSchemaReference reference) { ValidateSchemaReference(reference); } @@ -81,59 +65,26 @@ public override void Visit(IOpenApiSchema schema) private void ValidateSchemaReference(OpenApiSchemaReference reference) { - try + if (!reference.Reference.IsLocal) { - if (reference.RecursiveTarget is not null) - { - // The reference was followed to a valid schema somewhere in the document - return; - } - } - catch (InvalidOperationException ex) - { - context.Enter(GetSegment()); - context.CreateWarning(ruleName, ex.Message); - context.Exit(); - return; } - var id = reference.Reference.ReferenceV3; - - if (id is { Length: > 0 } && !IsValidSchemaReference(id, document)) + try { - var isValid = false; - - // Sometimes ReferenceV3 is not a JSON valid JSON pointer, but the $ref - // associated with it still points to a valid location in the document. - // In these cases, we need to find it manually to verify that fact before - // generating a warning that the schema reference is indeed invalid. - // TODO Why is this, and can it be avoided? - var parent = Find(PathString, document); - - if (parent?["$ref"] is { } @ref && - @ref.GetValueKind() is System.Text.Json.JsonValueKind.String && - @ref.GetValue() is { Length: > 0 } refId) - { - id = refId; - isValid = IsValidSchemaReference(id, document); - } - - if (!isValid) + if (reference.RecursiveTarget is null) { + // The reference was not followed to a valid schema somewhere in the document context.Enter(GetSegment()); - context.CreateWarning(ruleName, string.Format(SRResource.Validation_SchemaReferenceDoesNotExist, id)); + context.CreateWarning(ruleName, string.Format(SRResource.Validation_SchemaReferenceDoesNotExist, reference.Reference.ReferenceV3)); context.Exit(); } } - - static bool IsValidSchemaReference(string id, JsonNode baseNode) - => Find(id, baseNode) is not null; - - static JsonNode? Find(string id, JsonNode baseNode) + catch (InvalidOperationException ex) { - var pointer = new JsonPointer(id.Replace("#/", "/")); - return pointer.Find(baseNode); + context.Enter(GetSegment()); + context.CreateWarning(ruleName, ex.Message); + context.Exit(); } string GetSegment()