Skip to content

Commit b14d3a2

Browse files
Validate OpenAPI schema references
Add validation rule for OpenAPI document schema references. Resolves #2453.
1 parent 1994320 commit b14d3a2

File tree

6 files changed

+134
-40
lines changed

6 files changed

+134
-40
lines changed

src/Microsoft.OpenApi/Microsoft.OpenApi.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
<EmbeddedResource Update="Properties\SRResource.resx">
4242
<Generator>ResXFileCodeGenerator</Generator>
4343
<LastGenOutput>SRResource.Designer.cs</LastGenOutput>
44+
<CustomToolNamespace>Microsoft.OpenApi</CustomToolNamespace>
4445
</EmbeddedResource>
4546
</ItemGroup>
4647

src/Microsoft.OpenApi/Properties/SRResource.Designer.cs

Lines changed: 19 additions & 33 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Microsoft.OpenApi/Properties/SRResource.resx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,9 +226,15 @@
226226
<value>OpenAPI document must be added to an OpenApiWorkspace to be able to resolve external references.</value>
227227
</data>
228228
<data name="ParseServerUrlDefaultValueNotAvailable" xml:space="preserve">
229-
<value>Invalid server variable '{0}'. A value was not provided and no default value was provided.</value>
229+
<value>Invalid server variable '{0}'. A value was not provided and no default value was provided.</value>
230230
</data>
231231
<data name="ParseServerUrlValueNotValid" xml:space="preserve">
232-
<value>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</value>
232+
<value>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</value>
233233
</data>
234-
</root>
234+
<data name="Validation_SchemaReferenceDoesNotExist" xml:space="preserve">
235+
<value>The schema reference '{0}' does not point to an existing schema.</value>
236+
</data>
237+
<data name="ArgumentNull" xml:space="preserve">
238+
<value>The argument '{0}' is null.</value>
239+
</data>
240+
</root>

src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
33

4-
using System;
4+
using System.Text.Json.Nodes;
5+
using Microsoft.OpenApi.Reader;
56

67
namespace Microsoft.OpenApi
78
{
@@ -23,9 +24,108 @@ public static class OpenApiDocumentRules
2324
if (item.Info == null)
2425
{
2526
context.CreateError(nameof(OpenApiDocumentFieldIsMissing),
26-
String.Format(SRResource.Validation_FieldIsRequired, "info", "document"));
27+
string.Format(SRResource.Validation_FieldIsRequired, "info", "document"));
2728
}
2829
context.Exit();
2930
});
31+
32+
/// <summary>
33+
/// All references in the OpenAPI document must be valid.
34+
/// </summary>
35+
public static ValidationRule<OpenApiDocument> OpenApiDocumentReferencesAreValid =>
36+
new(nameof(OpenApiDocumentReferencesAreValid),
37+
static (context, item) =>
38+
{
39+
const string RuleName = nameof(OpenApiDocumentReferencesAreValid);
40+
41+
JsonNode document;
42+
43+
using (var textWriter = new System.IO.StringWriter())
44+
{
45+
var writer = new OpenApiJsonWriter(textWriter);
46+
47+
item.SerializeAsV31(writer);
48+
49+
var json = textWriter.ToString();
50+
51+
document = JsonNode.Parse(json)!;
52+
}
53+
54+
var visitor = new OpenApiSchemaReferenceVisitor(RuleName, context, document);
55+
var walker = new OpenApiWalker(visitor);
56+
57+
walker.Walk(item);
58+
});
59+
60+
private sealed class OpenApiSchemaReferenceVisitor(
61+
string ruleName,
62+
IValidationContext context,
63+
JsonNode document) : OpenApiVisitorBase
64+
{
65+
public override void Visit(IOpenApiReferenceHolder referenceHolder)
66+
{
67+
if (referenceHolder is OpenApiSchemaReference { Reference.IsLocal: true } reference)
68+
{
69+
ValidateSchemaReference(reference);
70+
}
71+
}
72+
73+
public override void Visit(IOpenApiSchema schema)
74+
{
75+
if (schema is OpenApiSchemaReference { Reference.IsLocal: true } reference)
76+
{
77+
ValidateSchemaReference(reference);
78+
}
79+
}
80+
81+
private void ValidateSchemaReference(OpenApiSchemaReference reference)
82+
{
83+
var id = reference.Reference.ReferenceV3;
84+
85+
if (id is { Length: > 0 } && !IsValidSchemaReference(id, document))
86+
{
87+
var isValid = false;
88+
89+
// Sometimes ReferenceV3 is not a JSON valid JSON pointer, but the $ref
90+
// associated with it still points to a valid location in the document.
91+
// In these cases, we need to find it manually to verify that fact before
92+
// generating a warning that the schema reference is indeed invalid.
93+
// TODO Why is this, and can it be avoided?
94+
var parent = Find(PathString, document);
95+
96+
if (parent?["$ref"] is { } @ref &&
97+
@ref.GetValueKind() is System.Text.Json.JsonValueKind.String &&
98+
@ref.GetValue<string>() is { Length: > 0 } refId)
99+
{
100+
id = refId;
101+
isValid = IsValidSchemaReference(id, document);
102+
}
103+
104+
if (!isValid)
105+
{
106+
// Trim off the leading "#/" as the context is already at the root of the document
107+
var segment =
108+
#if NET8_0_OR_GREATER
109+
PathString[2..];
110+
#else
111+
PathString.Substring(2);
112+
#endif
113+
114+
context.Enter(segment);
115+
context.CreateWarning(ruleName, string.Format(SRResource.Validation_SchemaReferenceDoesNotExist, id));
116+
context.Exit();
117+
}
118+
}
119+
120+
static bool IsValidSchemaReference(string id, JsonNode baseNode)
121+
=> Find(id, baseNode) is not null;
122+
123+
static JsonNode? Find(string id, JsonNode baseNode)
124+
{
125+
var pointer = new JsonPointer(id.Replace("#/", "/"));
126+
return pointer.Find(baseNode);
127+
}
128+
}
129+
}
30130
}
31131
}

test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,7 @@ namespace Microsoft.OpenApi
627627
public static class OpenApiDocumentRules
628628
{
629629
public static Microsoft.OpenApi.ValidationRule<Microsoft.OpenApi.OpenApiDocument> OpenApiDocumentFieldIsMissing { get; }
630+
public static Microsoft.OpenApi.ValidationRule<Microsoft.OpenApi.OpenApiDocument> OpenApiDocumentReferencesAreValid { get; }
630631
}
631632
public static class OpenApiElementExtensions
632633
{

test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ public void RuleSetConstructorsReturnsTheCorrectRules()
5353
Assert.Empty(ruleSet_4.Rules);
5454

5555
// Update the number if you add new default rule(s).
56-
Assert.Equal(19, ruleSet_1.Rules.Count);
57-
Assert.Equal(19, ruleSet_2.Rules.Count);
56+
Assert.Equal(20, ruleSet_1.Rules.Count);
57+
Assert.Equal(20, ruleSet_2.Rules.Count);
5858
Assert.Equal(3, ruleSet_3.Rules.Count);
5959
}
6060

0 commit comments

Comments
 (0)