Skip to content

Validate OpenAPI schema references #2459

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Microsoft.OpenApi/Microsoft.OpenApi.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
<EmbeddedResource Update="Properties\SRResource.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>SRResource.Designer.cs</LastGenOutput>
<CustomToolNamespace>Microsoft.OpenApi</CustomToolNamespace>
</EmbeddedResource>
</ItemGroup>

Expand Down
52 changes: 19 additions & 33 deletions src/Microsoft.OpenApi/Properties/SRResource.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 9 additions & 3 deletions src/Microsoft.OpenApi/Properties/SRResource.resx
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,15 @@
<value>OpenAPI document must be added to an OpenApiWorkspace to be able to resolve external references.</value>
</data>
<data name="ParseServerUrlDefaultValueNotAvailable" xml:space="preserve">
<value>Invalid server variable '{0}'. A value was not provided and no default value was provided.</value>
<value>Invalid server variable '{0}'. A value was not provided and no default value was provided.</value>
</data>
<data name="ParseServerUrlValueNotValid" xml:space="preserve">
<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>
<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>
</data>
</root>
<data name="Validation_SchemaReferenceDoesNotExist" xml:space="preserve">
<value>The schema reference '{0}' does not point to an existing schema.</value>
</data>
<data name="ArgumentNull" xml:space="preserve">
<value>The argument '{0}' is null.</value>
</data>
</root>
120 changes: 119 additions & 1 deletion src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// Licensed under the MIT license.

using System;
using System.Text.Json.Nodes;
using Microsoft.OpenApi.Reader;

namespace Microsoft.OpenApi
{
Expand All @@ -23,9 +25,125 @@ 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();
});

/// <summary>
/// All references in the OpenAPI document must be valid.
/// </summary>
public static ValidationRule<OpenApiDocument> 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)
{
// 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)
{
// The reference was followed to a valid schema somewhere in the document
return;
}
}
catch (InvalidOperationException ex)
{
context.Enter(segment);
context.CreateWarning(ruleName, ex.Message);
context.Exit();

return;
}

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<string>() is { Length: > 0 } refId)
{
id = refId;
isValid = IsValidSchemaReference(id, document);
}

if (!isValid)
{
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);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,7 @@ namespace Microsoft.OpenApi
public static class OpenApiDocumentRules
{
public static Microsoft.OpenApi.ValidationRule<Microsoft.OpenApi.OpenApiDocument> OpenApiDocumentFieldIsMissing { get; }
public static Microsoft.OpenApi.ValidationRule<Microsoft.OpenApi.OpenApiDocument> OpenApiDocumentReferencesAreValid { get; }
}
public static class OpenApiElementExtensions
{
Expand Down
Loading