Skip to content

Commit 940945d

Browse files
authored
Merge pull request #2397 from microsoft/fix/relative-uri-parsing-json-schema-reference
fix: relative uri in json schema references would not parse appropriately or provide feedback to the user
2 parents 388d6f7 + 5ad2f37 commit 940945d

File tree

3 files changed

+132
-25
lines changed

3 files changed

+132
-25
lines changed

src/Microsoft.OpenApi/Models/OpenApiDocument.cs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -575,9 +575,9 @@ private static string ConvertByteArrayToString(byte[] hash)
575575

576576
string uriLocation;
577577
var id = reference.Id;
578-
if (!string.IsNullOrEmpty(id) && id!.Contains("/")) // this means its a URL reference
578+
if (!string.IsNullOrEmpty(id) && id!.Contains('/')) // this means its a URL reference
579579
{
580-
uriLocation = id;
580+
uriLocation = id!;
581581
}
582582
else
583583
{
@@ -609,12 +609,24 @@ private static string ConvertByteArrayToString(byte[] hash)
609609
: BaseUri + relativePath;
610610
}
611611

612-
if (reference.Type is ReferenceType.Schema && uriLocation.Contains('#'))
612+
var absoluteUri =
613+
#if NETSTANDARD2_1 || NETCOREAPP || NET5_0_OR_GREATER
614+
uriLocation.StartsWith('#')
615+
#else
616+
uriLocation.StartsWith("#", StringComparison.OrdinalIgnoreCase)
617+
#endif
618+
switch
619+
{
620+
true => new Uri(BaseUri, uriLocation).AbsoluteUri,
621+
false => new Uri(uriLocation).AbsoluteUri
622+
};
623+
624+
if (reference.Type is ReferenceType.Schema && absoluteUri.Contains('#'))
613625
{
614-
return Workspace?.ResolveJsonSchemaReference(new Uri(uriLocation).AbsoluteUri);
626+
return Workspace?.ResolveJsonSchemaReference(absoluteUri);
615627
}
616628

617-
return Workspace?.ResolveReference<IOpenApiReferenceable>(new Uri(uriLocation).AbsoluteUri);
629+
return Workspace?.ResolveReference<IOpenApiReferenceable>(absoluteUri);
618630
}
619631

620632
private static bool IsSubComponent(string reference)

src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -340,29 +340,30 @@ public bool Contains(string location)
340340
* #/components/schemas/human/allOf/0
341341
*/
342342

343-
if (string.IsNullOrEmpty(location)) return default;
343+
if (string.IsNullOrEmpty(location) || ToLocationUrl(location) is not Uri uri) return default;
344344

345-
var uri = ToLocationUrl(location);
346-
string[] pathSegments;
345+
#if NETSTANDARD2_1 || NETCOREAPP || NET5_0_OR_GREATER
346+
if (!location.Contains("#/components/schemas/", StringComparison.OrdinalIgnoreCase))
347+
#else
348+
if (!location.Contains("#/components/schemas/"))
349+
#endif
350+
throw new ArgumentException($"Invalid schema reference location: {location}. It should contain '#/components/schemas/'");
347351

348-
if (uri is not null)
349-
{
350-
pathSegments = uri.Fragment.Split(['/'], StringSplitOptions.RemoveEmptyEntries);
352+
var pathSegments = uri.Fragment.Split(['/'], StringSplitOptions.RemoveEmptyEntries);
351353

352-
// Build the base path for the root schema: "#/components/schemas/person"
353-
var fragment = OpenApiConstants.ComponentsSegment + ReferenceType.Schema.GetDisplayName() + ComponentSegmentSeparator + pathSegments[3];
354-
var uriBuilder = new UriBuilder(uri)
355-
{
356-
Fragment = fragment
357-
}; // to avoid escaping the # character in the resulting Uri
354+
// Build the base path for the root schema: "#/components/schemas/person"
355+
var fragment = OpenApiConstants.ComponentsSegment + ReferenceType.Schema.GetDisplayName() + ComponentSegmentSeparator + pathSegments[3];
356+
var uriBuilder = new UriBuilder(uri)
357+
{
358+
Fragment = fragment
359+
}; // to avoid escaping the # character in the resulting Uri
358360

359-
if (_IOpenApiReferenceableRegistry.TryGetValue(uriBuilder.Uri, out var schema) && schema is IOpenApiSchema targetSchema)
360-
{
361-
// traverse remaining segments after fetching the base schema
362-
var remainingSegments = pathSegments.Skip(4).ToArray();
363-
return ResolveSubSchema(targetSchema, remainingSegments);
364-
}
365-
}
361+
if (_IOpenApiReferenceableRegistry.TryGetValue(uriBuilder.Uri, out var schema) && schema is IOpenApiSchema targetSchema)
362+
{
363+
// traverse remaining segments after fetching the base schema
364+
var remainingSegments = pathSegments.Skip(4).ToArray();
365+
return ResolveSubSchema(targetSchema, remainingSegments);
366+
}
366367

367368
return default;
368369
}

test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Generic;
23
using System.IO;
34
using System.Net.Http;
45
using System.Text.Json.Nodes;
@@ -297,6 +298,99 @@ public async Task ShouldResolveRelativeSubReference()
297298
Assert.Equal(JsonSchemaType.String, seq2Property.Items.Items.Type);
298299
}
299300
[Fact]
301+
public async Task ShouldResolveRelativeSubReferenceUsingParsingContext()
302+
{
303+
// Arrange
304+
var filePath = Path.Combine(SampleFolderPath, "relativeSubschemaReference.json");
305+
using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
306+
var jsonNode = await JsonNode.ParseAsync(fs);
307+
var schemaJsonNode = jsonNode["components"]?["schemas"]?["Foo"];
308+
Assert.NotNull(schemaJsonNode);
309+
var diagnostic = new OpenApiDiagnostic();
310+
var parsingContext = new ParsingContext(diagnostic);
311+
parsingContext.StartObject("components");
312+
parsingContext.StartObject("schemas");
313+
parsingContext.StartObject("Foo");
314+
var document = new OpenApiDocument();
315+
316+
// Act
317+
var fooComponentSchema = parsingContext.ParseFragment<OpenApiSchema>(schemaJsonNode, OpenApiSpecVersion.OpenApi3_1, document);
318+
document.AddComponent("Foo", fooComponentSchema);
319+
var seq1Property = fooComponentSchema.Properties["seq1"];
320+
Assert.NotNull(seq1Property);
321+
var seq2Property = fooComponentSchema.Properties["seq2"];
322+
Assert.NotNull(seq2Property);
323+
Assert.Equal(JsonSchemaType.Array, seq2Property.Items.Type);
324+
Assert.Equal(JsonSchemaType.String, seq2Property.Items.Items.Type);
325+
}
326+
[Fact]
327+
public void ShouldFailToResolveRelativeSubReferenceFromTheObjectModel()
328+
{
329+
var document = new OpenApiDocument
330+
{
331+
Info = new OpenApiInfo { Title = "Test API", Version = "1.0.0" },
332+
};
333+
document.Components = new OpenApiComponents
334+
{
335+
Schemas = new Dictionary<string, IOpenApiSchema>
336+
{
337+
["Foo"] = new OpenApiSchema
338+
{
339+
Properties = new Dictionary<string, IOpenApiSchema>
340+
{
341+
["seq1"] = new OpenApiSchema { Type = JsonSchemaType.Array | JsonSchemaType.Null, Items = new OpenApiSchema { Type = JsonSchemaType.Array, Items = new OpenApiSchema { Type = JsonSchemaType.String } } },
342+
["seq2"] = new OpenApiSchema { Type = JsonSchemaType.Array | JsonSchemaType.Null, Items = new OpenApiSchemaReference("#/properties/seq1/items", document) }
343+
}
344+
}
345+
}
346+
};
347+
document.RegisterComponents();
348+
349+
var fooComponentSchema = document.Components.Schemas["Foo"];
350+
var seq1Property = fooComponentSchema.Properties["seq1"];
351+
Assert.NotNull(seq1Property);
352+
var seq2Property = fooComponentSchema.Properties["seq2"];
353+
Assert.NotNull(seq2Property);
354+
Assert.Throws<ArgumentException>(() => seq2Property.Items.Type);
355+
// it's impossible to resolve relative references from the object model only because we don't have a way to get to
356+
// the parent object to build the full path for the reference.
357+
358+
359+
// #/properties/seq1/items
360+
// #/components/schemas/Foo/properties/seq1/items
361+
}
362+
[Fact]
363+
public void ShouldResolveAbsoluteSubReferenceFromTheObjectModel()
364+
{
365+
var document = new OpenApiDocument
366+
{
367+
Info = new OpenApiInfo { Title = "Test API", Version = "1.0.0" },
368+
};
369+
document.Components = new OpenApiComponents
370+
{
371+
Schemas = new Dictionary<string, IOpenApiSchema>
372+
{
373+
["Foo"] = new OpenApiSchema
374+
{
375+
Properties = new Dictionary<string, IOpenApiSchema>
376+
{
377+
["seq1"] = new OpenApiSchema { Type = JsonSchemaType.Array | JsonSchemaType.Null, Items = new OpenApiSchema { Type = JsonSchemaType.Array, Items = new OpenApiSchema { Type = JsonSchemaType.String } } },
378+
["seq2"] = new OpenApiSchema { Type = JsonSchemaType.Array | JsonSchemaType.Null, Items = new OpenApiSchemaReference("#/components/schemas/Foo/properties/seq1/items", document) }
379+
}
380+
}
381+
}
382+
};
383+
document.RegisterComponents();
384+
385+
var fooComponentSchema = document.Components.Schemas["Foo"];
386+
var seq1Property = fooComponentSchema.Properties["seq1"];
387+
Assert.NotNull(seq1Property);
388+
var seq2Property = fooComponentSchema.Properties["seq2"];
389+
Assert.NotNull(seq2Property);
390+
Assert.Equal(JsonSchemaType.Array, seq2Property.Items.Type);
391+
Assert.Equal(JsonSchemaType.String, seq2Property.Items.Items.Type);
392+
}
393+
[Fact]
300394
public async Task ShouldResolveRecursiveRelativeSubReference()
301395
{
302396
// Arrange

0 commit comments

Comments
 (0)