-
Notifications
You must be signed in to change notification settings - Fork 10.3k
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
Fix self-referential schema handling in collection schemas #60410
base: release/9.0
Are you sure you want to change the base?
Conversation
@@ -112,6 +112,13 @@ public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerC | |||
return new OpenApiSchema { Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = schemaId?.ToString() } }; | |||
} | |||
|
|||
// Handle relative schemas that don't point to the parent document but to another property in the same type. | |||
// In this case, remove the reference and rely on the properties that have been resolved and copied by the OpenApiSchemaService. | |||
if (schema.Reference is { Type: ReferenceType.Schema, Id: var id } && id.StartsWith("#/", StringComparison.Ordinal)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We resolve the JSON pointer that this ref points to in the new code OpenApiSchemaService
. We retain the $ref
so that we can use it as a comparison shorthand in OpenApiSchemaComparer. Here, before we resolve all the schemas in the document, we remove the relative reference so that the schema can be inlined using the properties that were copied over.
{ | ||
// Check if this node has a $ref property with a relative reference and no schemaId to | ||
// resolve to | ||
if (jsonObj.TryGetPropertyValue(OpenApiSchemaKeywords.RefKeyword, out var refNode) && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not having a schema ID indicates that the relative reference is to something that isn't a complex type, for example a List<string>
. For things that have a schema ID, we create a ref to the schema in the OpenApiComponents
via the OpenApiSchemaReferenceTransformer
.
x.Reference.ReferenceV3 is string xFullReferencePath && | ||
y.Reference.ReferenceV3 is string yFullReferencePath) | ||
{ | ||
// Compare the last segments of the reference paths |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
STJ will use different relative references for the same type depending on its entrypoint. This comparison helps us evaluate relative references with different structure that point to the same underlying property. For example, the schemas for ParentObject
and List<ParentObject>
differ like this:
using System.Text.Json.Nodes;
using System.Text.Json;
using System.Text.Json.Schema;
using System.Collections.Generic;
using System;
var schema1 = JsonSchemaExporter.GetJsonSchemaAsNode(JsonSerializerOptions.Web, typeof(ParentObject));
var schema2 = JsonSchemaExporter.GetJsonSchemaAsNode(JsonSerializerOptions.Web, typeof(List<ParentObject>));
Console.WriteLine(schema1.ToJsonString());
Console.WriteLine(schema2.ToJsonString());
public class ParentObject
{
public int Id { get; set; }
public List<ChildObject> Children { get; set; } = [];
}
public class ChildObject
{
public int Id { get; set; }
public required ParentObject Parent { get; set; }
}
ParentObject:
{
"type": [
"object",
"null"
],
"properties": {
"id": {
"type": [
"string",
"integer"
],
"pattern": "^-?(?:0|[1-9]\\d*)$"
},
"children": {
"type": [
"array",
"null"
],
"items": {
"type": [
"object",
"null"
],
"properties": {
"id": {
"type": [
"string",
"integer"
],
"pattern": "^-?(?:0|[1-9]\\d*)$"
},
"parent": {
"type": [
"object",
"null"
],
"properties": {
"id": {
"type": [
"string",
"integer"
],
"pattern": "^-?(?:0|[1-9]\\d*)$"
},
"children": {
"$ref": "#/properties/children" // Same as below
}
}
}
},
"required": [
"parent"
]
}
}
}
}
List
{
"type": [
"array",
"null"
],
"items": {
"type": [
"object",
"null"
],
"properties": {
"id": {
"type": [
"string",
"integer"
],
"pattern": "^-?(?:0|[1-9]\\d*)$"
},
"children": {
"type": [
"array",
"null"
],
"items": {
"type": [
"object",
"null"
],
"properties": {
"id": {
"type": [
"string",
"integer"
],
"pattern": "^-?(?:0|[1-9]\\d*)$"
},
"parent": {
"type": [
"object",
"null"
],
"properties": {
"id": {
"type": [
"string",
"integer"
],
"pattern": "^-?(?:0|[1-9]\\d*)$"
},
"children": {
"$ref": "#/items/properties/children" // This is the same as children not in a list
}
}
}
},
"required": [
"parent"
]
}
}
}
}
}
@@ -102,7 +102,7 @@ internal sealed class OpenApiSchemaService( | |||
// "nested": "#/properties/nested" becomes "nested": "#/components/schemas/NestedType" | |||
if (jsonPropertyInfo.PropertyType == jsonPropertyInfo.DeclaringType) | |||
{ | |||
return new JsonObject { [OpenApiSchemaKeywords.RefKeyword] = createSchemaReferenceId(context.TypeInfo) }; | |||
schema[OpenApiSchemaKeywords.RefKeyword] = createSchemaReferenceId(context.TypeInfo); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to override just the ref keyword so we can keep the other transformations that happened before this LoC intact (like setting the x-schema-id property).
@BrennanConroy Added snapshot tests and put in explainers next to each code delta. Ready for review! |
Hi @captainsafia. Please make sure you've updated the PR description to use the Shiproom Template. Also, make sure this PR is not marked as a draft and is ready-to-merge. To learn more about how to prepare a servicing PR click here. |
@@ -505,6 +505,58 @@ await VerifyOpenApiDocument(builder, document => | |||
// Assert that $ref is used for related LocationDto | |||
var addressSchema = locationSchema.Properties["address"].GetEffective(document); | |||
Assert.Equal("LocationDto", addressSchema.Properties["relatedLocation"].Reference.Id); | |||
|
|||
// Assert that only expected schemas are generated at the top-level | |||
Assert.Equal(["AddressDto", "LocationContainer", "LocationDto"], document.Components.Schemas.Keys); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there relevant/similar tests for all these in main?
|
||
if (xLastIndexOf != -1 && yLastIndexOf != -1) | ||
{ | ||
return xFullReferencePath[xLastIndexOf..] == yFullReferencePath[yLastIndexOf..];; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This compiles into 2 substring calls, should use span comparison.
Description
This PR expands relatives references in the schema generated by System.Text.Json since the Microsoft.OpenApi doesn't support resolving them correctly in v1.6. It also updates the comparison operators to handle relative references that are required for recursive types.
Fixes #60339
Customer Impact
Customer workarounds for this issue require post-processing on the document or changing the property order on types. These workarounds are hard to discover.
Regression?
Risk
Low risk becase:
Verification
Packaging changes reviewed?