@@ -68,6 +68,8 @@ public async Task OpenApiDocumentIsValid(string documentName, OpenApiSpecVersion
6868 Assert . Empty ( errors ) ;
6969 }
7070
71+ // The test below can be removed when https://github.com/microsoft/OpenAPI.NET/issues/2453 is implemented
72+
7173 [ Theory ] // See https://github.com/dotnet/aspnetcore/issues/63090
7274 [ MemberData ( nameof ( OpenApiDocuments ) ) ]
7375 public async Task OpenApiDocumentReferencesAreValid ( string documentName , OpenApiSpecVersion version )
@@ -79,146 +81,90 @@ public async Task OpenApiDocumentReferencesAreValid(string documentName, OpenApi
7981 var document = result . Document ;
8082 var documentNode = JsonNode . Parse ( json ) ;
8183
82- var actual = new List < string > ( ) ;
83-
84- // TODO What other parts of the document should also be validated for references to be comprehensive?
85- // Likely also needs to be recursive to validate all references in schemas, parameters, etc.
86- if ( document . Components is { Schemas . Count : > 0 } components )
84+ var ruleName = "OpenApiDocumentReferencesAreValid" ;
85+ var rule = new ValidationRule < OpenApiDocument > ( ruleName , ( context , item ) =>
8786 {
88- foreach ( var schema in components . Schemas )
89- {
90- if ( schema . Value . Properties is { Count : > 0 } properties )
91- {
92- foreach ( var property in properties )
93- {
94- if ( property . Value is not OpenApiSchemaReference reference )
95- {
96- continue ;
97- }
98-
99- var id = reference . Reference . ReferenceV3 ;
100-
101- if ( ! IsValidSchemaReference ( id , documentNode ) )
102- {
103- actual . Add ( $ "Reference '{ id } ' on property '{ property . Key } ' of schema '{ schema . Key } ' is invalid.") ;
104- }
105- }
106- }
87+ var visitor = new OpenApiSchemaReferenceVisitor ( ruleName , context , documentNode ) ;
10788
108- if ( schema . Value . AllOf is { Count : > 0 } allOf )
109- {
110- foreach ( var child in allOf )
111- {
112- if ( child is not OpenApiSchemaReference reference )
113- {
114- continue ;
115- }
116-
117- var id = reference . Reference . ReferenceV3 ;
118-
119- if ( ! IsValidSchemaReference ( id , documentNode ) )
120- {
121- actual . Add ( $ "Reference '{ id } ' for AllOf of schema '{ schema . Key } ' is invalid.") ;
122- }
123- }
124- }
89+ var walker = new OpenApiWalker ( visitor ) ;
90+ walker . Walk ( item ) ;
91+ } ) ;
12592
126- if ( schema . Value . AnyOf is { Count : > 0 } anyOf )
127- {
128- foreach ( var child in anyOf )
129- {
130- if ( child is not OpenApiSchemaReference reference )
131- {
132- continue ;
133- }
134-
135- var id = reference . Reference . ReferenceV3 ;
136-
137- if ( ! IsValidSchemaReference ( id , documentNode ) )
138- {
139- actual . Add ( $ "Reference '{ id } ' for AnyOf of schema '{ schema . Key } ' is invalid.") ;
140- }
141- }
142- }
93+ var ruleSet = new ValidationRuleSet ( ) ;
94+ ruleSet . Add ( typeof ( OpenApiDocument ) , rule ) ;
14395
144- if ( schema . Value . OneOf is { Count : > 0 } oneOf )
145- {
146- foreach ( var child in oneOf )
147- {
148- if ( child is not OpenApiSchemaReference reference )
149- {
150- continue ;
151- }
152-
153- var id = reference . Reference . ReferenceV3 ;
154-
155- if ( ! IsValidSchemaReference ( id , documentNode ) )
156- {
157- actual . Add ( $ "Reference '{ id } ' for OneOf of schema '{ schema . Key } ' is invalid.") ;
158- }
159- }
160- }
96+ var errors = document . Validate ( ruleSet ) ;
16197
162- if ( schema . Value . Discriminator is { Mapping . Count : > 0 } discriminator )
163- {
164- foreach ( var child in discriminator . Mapping )
165- {
166- if ( child . Value is not OpenApiSchemaReference reference )
167- {
168- continue ;
169- }
170-
171- var id = reference . Reference . ReferenceV3 ;
172-
173- if ( ! IsValidSchemaReference ( id , documentNode ) )
174- {
175- actual . Add ( $ "Reference '{ id } ' for Discriminator '{ child . Key } ' of schema '{ schema . Key } ' is invalid.") ;
176- }
177- }
178- }
98+ Assert . Empty ( errors ) ;
99+ }
100+
101+ private async Task < string > GetOpenApiDocument ( string documentName , OpenApiSpecVersion version )
102+ {
103+ var documentService = fixture . Services . GetRequiredKeyedService < OpenApiDocumentService > ( documentName ) ;
104+ var scopedServiceProvider = fixture . Services . CreateScope ( ) ;
105+ var document = await documentService . GetOpenApiDocumentAsync ( scopedServiceProvider . ServiceProvider ) ;
106+ return await document . SerializeAsJsonAsync ( version ) ;
107+ }
108+
109+ private sealed class OpenApiSchemaReferenceVisitor (
110+ string ruleName ,
111+ IValidationContext context ,
112+ JsonNode document ) : OpenApiVisitorBase
113+ {
114+ public override void Visit ( IOpenApiReferenceHolder referenceHolder )
115+ {
116+ if ( referenceHolder is OpenApiSchemaReference { Reference . IsLocal : true } reference )
117+ {
118+ ValidateSchemaReference ( reference ) ;
119+ }
120+ }
121+
122+ public override void Visit ( IOpenApiSchema schema )
123+ {
124+ if ( schema is OpenApiSchemaReference { Reference . IsLocal : true } reference )
125+ {
126+ ValidateSchemaReference ( reference ) ;
179127 }
180128 }
181129
182- foreach ( var path in document . Paths )
130+ private void ValidateSchemaReference ( OpenApiSchemaReference reference )
183131 {
184- foreach ( var operation in path . Value . Operations )
132+ var id = reference . Reference . ReferenceV3 ;
133+
134+ if ( id is { Length : > 0 } && ! IsValidSchemaReference ( id , document ) )
185135 {
186- if ( operation . Value . Parameters is not { Count : > 0 } parameters )
136+ var isValid = false ;
137+
138+ // Sometimes ReferenceV3 is not a JSON valid JSON pointer, but the $ref
139+ // associated with it still points to a valid location in the document.
140+ // In these cases, we need to find it manually to verify that fact before
141+ // generating a warning that the schema reference is indeed invalid.
142+ var parent = Find ( PathString , document ) ;
143+ var @ref = parent [ OpenApiSchemaKeywords . RefKeyword ] ;
144+
145+ if ( @ref is not null && @ref . GetValueKind ( ) is System . Text . Json . JsonValueKind . String &&
146+ @ref . GetValue < string > ( ) is { Length : > 0 } refId )
187147 {
188- continue ;
148+ id = refId ;
149+ isValid = IsValidSchemaReference ( id , document ) ;
189150 }
190151
191- foreach ( var parameter in parameters )
152+ if ( ! isValid )
192153 {
193- if ( parameter . Schema is not OpenApiSchemaReference reference )
194- {
195- continue ;
196- }
197-
198- var id = reference . Reference . ReferenceV3 ;
199-
200- if ( ! IsValidSchemaReference ( id , documentNode ) )
201- {
202- actual . Add ( $ "Reference '{ id } ' on parameter '{ parameter . Name } ' of path '{ path . Key } ' of operation '{ operation . Key } ' is invalid.") ;
203- }
154+ context . Enter ( PathString [ 2 ..] ) ; // Trim off the leading "#/" as the context is already at the root
155+ context . CreateWarning ( ruleName , $ "The schema reference '{ id } ' does not point to an existing schema.") ;
156+ context . Exit ( ) ;
204157 }
205158 }
206- }
207159
208- Assert . Empty ( actual ) ;
160+ static bool IsValidSchemaReference ( string id , JsonNode baseNode )
161+ => Find ( id , baseNode ) is not null ;
209162
210- static bool IsValidSchemaReference ( string id , JsonNode baseNode )
211- {
212- var pointer = new JsonPointer ( id . Replace ( "#/" , "/" ) ) ;
213- return pointer . Find ( baseNode ) is not null ;
163+ static JsonNode Find ( string id , JsonNode baseNode )
164+ {
165+ var pointer = new JsonPointer ( id . Replace ( "#/" , "/" ) ) ;
166+ return pointer . Find ( baseNode ) ;
167+ }
214168 }
215169 }
216-
217- private async Task < string > GetOpenApiDocument ( string documentName , OpenApiSpecVersion version )
218- {
219- var documentService = fixture . Services . GetRequiredKeyedService < OpenApiDocumentService > ( documentName ) ;
220- var scopedServiceProvider = fixture . Services . CreateScope ( ) ;
221- var document = await documentService . GetOpenApiDocumentAsync ( scopedServiceProvider . ServiceProvider ) ;
222- return await document . SerializeAsJsonAsync ( version ) ;
223- }
224170}
0 commit comments