Skip to content

Commit e0bceaa

Browse files
committed
fix: relative uri in json schema references would not parse appropriately or provide feedback to the user
Signed-off-by: Vincent Biret <[email protected]>
1 parent 388d6f7 commit e0bceaa

File tree

3 files changed

+106
-25
lines changed

3 files changed

+106
-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: 69 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,73 @@ public async Task ShouldResolveRelativeSubReference()
297298
Assert.Equal(JsonSchemaType.String, seq2Property.Items.Items.Type);
298299
}
299300
[Fact]
301+
public void ShouldFailToResolveRelativeSubReferenceFromTheObjectModel()
302+
{
303+
var document = new OpenApiDocument
304+
{
305+
Info = new OpenApiInfo { Title = "Test API", Version = "1.0.0" },
306+
};
307+
document.Components = new OpenApiComponents
308+
{
309+
Schemas = new Dictionary<string, IOpenApiSchema>
310+
{
311+
["Foo"] = new OpenApiSchema
312+
{
313+
Properties = new Dictionary<string, IOpenApiSchema>
314+
{
315+
["seq1"] = new OpenApiSchema { Type = JsonSchemaType.Array | JsonSchemaType.Null, Items = new OpenApiSchema { Type = JsonSchemaType.Array, Items = new OpenApiSchema { Type = JsonSchemaType.String } } },
316+
["seq2"] = new OpenApiSchema { Type = JsonSchemaType.Array | JsonSchemaType.Null, Items = new OpenApiSchemaReference("#/properties/seq1/items", document) }
317+
}
318+
}
319+
}
320+
};
321+
document.RegisterComponents();
322+
323+
var fooComponentSchema = document.Components.Schemas["Foo"];
324+
var seq1Property = fooComponentSchema.Properties["seq1"];
325+
Assert.NotNull(seq1Property);
326+
var seq2Property = fooComponentSchema.Properties["seq2"];
327+
Assert.NotNull(seq2Property);
328+
Assert.Throws<ArgumentException>(() => seq2Property.Items.Type);
329+
// it's impossible to resolve relative references from the object model only because we don't have a way to get to
330+
// the parent object to build the full path for the reference.
331+
332+
333+
// #/properties/seq1/items
334+
// #/components/schemas/Foo/properties/seq1/items
335+
}
336+
[Fact]
337+
public void ShouldResolveAbsoluteSubReferenceFromTheObjectModel()
338+
{
339+
var document = new OpenApiDocument
340+
{
341+
Info = new OpenApiInfo { Title = "Test API", Version = "1.0.0" },
342+
};
343+
document.Components = new OpenApiComponents
344+
{
345+
Schemas = new Dictionary<string, IOpenApiSchema>
346+
{
347+
["Foo"] = new OpenApiSchema
348+
{
349+
Properties = new Dictionary<string, IOpenApiSchema>
350+
{
351+
["seq1"] = new OpenApiSchema { Type = JsonSchemaType.Array | JsonSchemaType.Null, Items = new OpenApiSchema { Type = JsonSchemaType.Array, Items = new OpenApiSchema { Type = JsonSchemaType.String } } },
352+
["seq2"] = new OpenApiSchema { Type = JsonSchemaType.Array | JsonSchemaType.Null, Items = new OpenApiSchemaReference("#/components/schemas/Foo/properties/seq1/items", document) }
353+
}
354+
}
355+
}
356+
};
357+
document.RegisterComponents();
358+
359+
var fooComponentSchema = document.Components.Schemas["Foo"];
360+
var seq1Property = fooComponentSchema.Properties["seq1"];
361+
Assert.NotNull(seq1Property);
362+
var seq2Property = fooComponentSchema.Properties["seq2"];
363+
Assert.NotNull(seq2Property);
364+
Assert.Equal(JsonSchemaType.Array, seq2Property.Items.Type);
365+
Assert.Equal(JsonSchemaType.String, seq2Property.Items.Items.Type);
366+
}
367+
[Fact]
300368
public async Task ShouldResolveRecursiveRelativeSubReference()
301369
{
302370
// Arrange

0 commit comments

Comments
 (0)