Skip to content

Commit d299936

Browse files
author
Mirroring
committed
Merge commit '792b6a9773d7718fc12ca6a9641908c91de26d44'
2 parents 1ca4d43 + 792b6a9 commit d299936

File tree

7 files changed

+716
-8
lines changed

7 files changed

+716
-8
lines changed

src/OpenApi/sample/EndpointRouteBuilderExtensions.cs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,92 @@ public static IEndpointConventionBuilder MapSwaggerUi(this IEndpointRouteBuilder
4343
</html>
4444
""", "text/html")).ExcludeFromDescription();
4545
}
46+
47+
public static IEndpointRouteBuilder MapTypesWithRef(this IEndpointRouteBuilder endpoints)
48+
{
49+
endpoints.MapPost("/category", (Category category) =>
50+
{
51+
return Results.Ok(category);
52+
});
53+
endpoints.MapPost("/container", (ContainerType container) =>
54+
{
55+
return Results.Ok(container);
56+
});
57+
endpoints.MapPost("/root", (Root root) =>
58+
{
59+
return Results.Ok(root);
60+
});
61+
endpoints.MapPost("/location", (LocationContainer location) =>
62+
{
63+
return Results.Ok(location);
64+
});
65+
endpoints.MapPost("/parent", (ParentObject parent) =>
66+
{
67+
return Results.Ok(parent);
68+
});
69+
endpoints.MapPost("/child", (ChildObject child) =>
70+
{
71+
return Results.Ok(child);
72+
});
73+
return endpoints;
74+
}
75+
76+
public sealed class Category
77+
{
78+
public required string Name { get; set; }
79+
80+
public required Category Parent { get; set; }
81+
82+
public IEnumerable<Tag> Tags { get; set; } = [];
83+
}
84+
85+
public sealed class Tag
86+
{
87+
public required string Name { get; set; }
88+
}
89+
90+
public sealed class ContainerType
91+
{
92+
public List<List<string>> Seq1 { get; set; } = [];
93+
public List<List<string>> Seq2 { get; set; } = [];
94+
}
95+
96+
public sealed class Root
97+
{
98+
public Item Item1 { get; set; } = null!;
99+
public Item Item2 { get; set; } = null!;
100+
}
101+
102+
public sealed class Item
103+
{
104+
public string[] Name { get; set; } = null!;
105+
public int value { get; set; }
106+
}
107+
108+
public sealed class LocationContainer
109+
{
110+
public required LocationDto Location { get; set; }
111+
}
112+
113+
public sealed class LocationDto
114+
{
115+
public required AddressDto Address { get; set; }
116+
}
117+
118+
public sealed class AddressDto
119+
{
120+
public required LocationDto RelatedLocation { get; set; }
121+
}
122+
123+
public sealed class ParentObject
124+
{
125+
public int Id { get; set; }
126+
public List<ChildObject> Children { get; set; } = [];
127+
}
128+
129+
public sealed class ChildObject
130+
{
131+
public int Id { get; set; }
132+
public required ParentObject Parent { get; set; }
133+
}
46134
}

src/OpenApi/sample/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
schemas.MapPost("/shape", (Shape shape) => { });
114114
schemas.MapPost("/weatherforecastbase", (WeatherForecastBase forecast) => { });
115115
schemas.MapPost("/person", (Person person) => { });
116+
schemas.MapTypesWithRef();
116117

117118
app.MapControllers();
118119

src/OpenApi/src/Comparers/OpenApiSchemaComparer.cs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,30 @@ public bool Equals(OpenApiSchema? x, OpenApiSchema? y)
2424
return true;
2525
}
2626

27-
// If a local reference is present, we can't compare the schema directly
28-
// and should instead use the schema ID as a type-check to assert if the schemas are
29-
// equivalent.
27+
// If both have references, compare the final segments to handle
28+
// equivalent types in different contexts, like the same schema
29+
// in a dictionary value or list like "#/components/schemas/#/additionalProperties/properties/location/properties/address"
30+
if (x.Reference != null && y.Reference != null)
31+
{
32+
if (x.Reference.Id.StartsWith("#", StringComparison.OrdinalIgnoreCase) &&
33+
y.Reference.Id.StartsWith("#", StringComparison.OrdinalIgnoreCase) &&
34+
x.Reference.ReferenceV3 is string xFullReferencePath &&
35+
y.Reference.ReferenceV3 is string yFullReferencePath)
36+
{
37+
// Compare the last segments of the reference paths
38+
// to handle equivalent types in different contexts,
39+
// like the same schema in a dictionary value or list
40+
var xLastIndexOf = xFullReferencePath.LastIndexOf('/');
41+
var yLastIndexOf = yFullReferencePath.LastIndexOf('/');
42+
43+
if (xLastIndexOf != -1 && yLastIndexOf != -1)
44+
{
45+
return xFullReferencePath.AsSpan(xLastIndexOf).Equals(yFullReferencePath.AsSpan(yLastIndexOf), StringComparison.OrdinalIgnoreCase);
46+
}
47+
}
48+
}
49+
50+
// If only one has a reference, compare using schema IDs
3051
if ((x.Reference != null && y.Reference == null)
3152
|| (x.Reference == null && y.Reference != null))
3253
{

src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs

Lines changed: 110 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ internal sealed class OpenApiSchemaService(
102102
// "nested": "#/properties/nested" becomes "nested": "#/components/schemas/NestedType"
103103
if (jsonPropertyInfo.PropertyType == jsonPropertyInfo.DeclaringType)
104104
{
105-
return new JsonObject { [OpenApiSchemaKeywords.RefKeyword] = createSchemaReferenceId(context.TypeInfo) };
105+
schema[OpenApiSchemaKeywords.RefKeyword] = createSchemaReferenceId(context.TypeInfo);
106106
}
107107
schema.ApplyNullabilityContextInfo(jsonPropertyInfo);
108108
}
@@ -213,13 +213,118 @@ private async Task InnerApplySchemaTransformersAsync(OpenApiSchema schema,
213213
}
214214
}
215215

216-
if (schema is { AdditionalPropertiesAllowed: true, AdditionalProperties: not null } && jsonTypeInfo.ElementType is not null)
217-
{
216+
if (schema is { AdditionalPropertiesAllowed: true, AdditionalProperties: not null } && jsonTypeInfo.ElementType is not null)
217+
{
218218
var elementTypeInfo = _jsonSerializerOptions.GetTypeInfo(jsonTypeInfo.ElementType);
219219
await InnerApplySchemaTransformersAsync(schema.AdditionalProperties, elementTypeInfo, null, context, transformer, cancellationToken);
220220
}
221-
}
221+
}
222222

223223
private JsonNode CreateSchema(OpenApiSchemaKey key)
224-
=> JsonSchemaExporter.GetJsonSchemaAsNode(_jsonSerializerOptions, key.Type, _configuration);
224+
{
225+
var sourceSchema = JsonSchemaExporter.GetJsonSchemaAsNode(_jsonSerializerOptions, key.Type, _configuration);
226+
227+
// Resolve any relative references in the schema
228+
ResolveRelativeReferences(sourceSchema, sourceSchema);
229+
230+
return sourceSchema;
231+
}
232+
233+
// Helper method to recursively resolve relative references in a schema
234+
private static void ResolveRelativeReferences(JsonNode node, JsonNode rootNode)
235+
{
236+
if (node is JsonObject jsonObj)
237+
{
238+
// Check if this node has a $ref property with a relative reference and no schemaId to
239+
// resolve to
240+
if (jsonObj.TryGetPropertyValue(OpenApiSchemaKeywords.RefKeyword, out var refNode) &&
241+
refNode is JsonValue refValue &&
242+
refValue.TryGetValue<string>(out var refPath) &&
243+
refPath.StartsWith("#/", StringComparison.OrdinalIgnoreCase) &&
244+
!jsonObj.TryGetPropertyValue(OpenApiConstants.SchemaId, out var schemaId) &&
245+
schemaId is null)
246+
{
247+
// Found a relative reference, resolve it
248+
var resolvedNode = ResolveJsonPointer(rootNode, refPath);
249+
if (resolvedNode != null)
250+
{
251+
// Copy all properties from the resolved node
252+
if (resolvedNode is JsonObject resolvedObj)
253+
{
254+
foreach (var property in resolvedObj)
255+
{
256+
// Clone the property value to avoid modifying the original
257+
var clonedValue = property.Value != null
258+
? JsonNode.Parse(property.Value.ToJsonString())
259+
: null;
260+
261+
jsonObj[property.Key] = clonedValue;
262+
}
263+
}
264+
}
265+
}
266+
else
267+
{
268+
// Recursively process all properties
269+
foreach (var property in jsonObj)
270+
{
271+
if (property.Value is JsonNode propNode)
272+
{
273+
ResolveRelativeReferences(propNode, rootNode);
274+
}
275+
}
276+
}
277+
}
278+
else if (node is JsonArray jsonArray)
279+
{
280+
// Process each item in the array
281+
for (var i = 0; i < jsonArray.Count; i++)
282+
{
283+
if (jsonArray[i] is JsonNode arrayItem)
284+
{
285+
ResolveRelativeReferences(arrayItem, rootNode);
286+
}
287+
}
288+
}
289+
}
290+
291+
// Helper method to resolve a JSON pointer path and return the referenced node
292+
private static JsonNode? ResolveJsonPointer(JsonNode root, string pointer)
293+
{
294+
if (string.IsNullOrEmpty(pointer) || !pointer.StartsWith("#/", StringComparison.OrdinalIgnoreCase))
295+
{
296+
return null; // Invalid pointer
297+
}
298+
299+
// Remove the leading "#/" and split the path into segments
300+
var jsonPointer = pointer.AsSpan(2);
301+
var segments = jsonPointer.Split('/');
302+
var currentNode = root;
303+
304+
foreach (var segment in segments)
305+
{
306+
if (currentNode is JsonObject jsonObj)
307+
{
308+
if (!jsonObj.TryGetPropertyValue(jsonPointer[segment].ToString(), out var nextNode))
309+
{
310+
return null; // Path segment not found
311+
}
312+
currentNode = nextNode;
313+
}
314+
else if (currentNode is JsonArray jsonArray && int.TryParse(jsonPointer[segment], out var index))
315+
{
316+
if (index < 0 || index >= jsonArray.Count)
317+
{
318+
return null; // Index out of range
319+
}
320+
currentNode = jsonArray[index];
321+
}
322+
else
323+
{
324+
return null; // Cannot navigate further
325+
}
326+
}
327+
328+
return currentNode;
329+
}
225330
}

src/OpenApi/src/Transformers/Implementations/OpenApiSchemaReferenceTransformer.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,13 @@ public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerC
112112
return new OpenApiSchema { Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = schemaId?.ToString() } };
113113
}
114114

115+
// Handle relative schemas that don't point to the parent document but to another property in the same type.
116+
// In this case, remove the reference and rely on the properties that have been resolved and copied by the OpenApiSchemaService.
117+
if (schema.Reference is { Type: ReferenceType.Schema, Id: var id } && id.StartsWith("#/", StringComparison.Ordinal))
118+
{
119+
schema.Reference = null;
120+
}
121+
115122
if (schema.AllOf is not null)
116123
{
117124
for (var i = 0; i < schema.AllOf.Count; i++)

0 commit comments

Comments
 (0)