@@ -174,6 +174,114 @@ public async Task GetOpenApiResponse_HandlesNullablePocoResponse()
174174 builder . MapGet ( "/api" , GetTodo ) ;
175175#nullable restore
176176
177+ // Assert
178+ await VerifyOpenApiDocument ( builder , document =>
179+ {
180+ var operation = document . Paths [ "/api" ] . Operations [ HttpMethod . Get ] ;
181+ var responses = Assert . Single ( operation . Responses ) ;
182+ var response = responses . Value ;
183+ Assert . True ( response . Content . TryGetValue ( "application/json" , out var mediaType ) ) ;
184+ var schema = mediaType . Schema ;
185+ Assert . NotNull ( schema . AllOf ) ;
186+ Assert . Equal ( 2 , schema . AllOf . Count ) ;
187+ // Check that the allOf consists of a nullable schema and the GetTodo schema
188+ Assert . Collection ( schema . AllOf ,
189+ item =>
190+ {
191+ Assert . NotNull ( item ) ;
192+ Assert . Equal ( JsonSchemaType . Null , item . Type ) ;
193+ } ,
194+ item =>
195+ {
196+ Assert . NotNull ( item ) ;
197+ Assert . Equal ( JsonSchemaType . Object , item . Type ) ;
198+ Assert . Collection ( item . Properties ,
199+ property =>
200+ {
201+ Assert . Equal ( "id" , property . Key ) ;
202+ Assert . Equal ( JsonSchemaType . Integer , property . Value . Type ) ;
203+ Assert . Equal ( "int32" , property . Value . Format ) ;
204+ } ,
205+ property =>
206+ {
207+ Assert . Equal ( "title" , property . Key ) ;
208+ Assert . Equal ( JsonSchemaType . String | JsonSchemaType . Null , property . Value . Type ) ;
209+ } ,
210+ property =>
211+ {
212+ Assert . Equal ( "completed" , property . Key ) ;
213+ Assert . Equal ( JsonSchemaType . Boolean , property . Value . Type ) ;
214+ } ,
215+ property =>
216+ {
217+ Assert . Equal ( "createdAt" , property . Key ) ;
218+ Assert . Equal ( JsonSchemaType . String , property . Value . Type ) ;
219+ Assert . Equal ( "date-time" , property . Value . Format ) ;
220+ } ) ;
221+ } ) ;
222+
223+ } ) ;
224+ }
225+
226+ [ Fact ]
227+ public async Task GetOpenApiResponse_HandlesNullablePocoTaskResponse ( )
228+ {
229+ // Arrange
230+ var builder = CreateBuilder ( ) ;
231+
232+ // Act
233+ #nullable enable
234+ static Task < Todo ? > GetTodoAsync ( ) => Task . FromResult ( Random . Shared . Next ( ) < 0.5 ? new Todo ( 1 , "Test Title" , true , DateTime . Now ) : null ) ;
235+ builder . MapGet ( "/api" , GetTodoAsync ) ;
236+ #nullable restore
237+
238+ // Assert
239+ await VerifyOpenApiDocument ( builder , document =>
240+ {
241+ var operation = document . Paths [ "/api" ] . Operations [ HttpMethod . Get ] ;
242+ var responses = Assert . Single ( operation . Responses ) ;
243+ var response = responses . Value ;
244+ Assert . True ( response . Content . TryGetValue ( "application/json" , out var mediaType ) ) ;
245+ var schema = mediaType . Schema ;
246+ Assert . Equal ( JsonSchemaType . Object , schema . Type ) ;
247+ Assert . Collection ( schema . Properties ,
248+ property =>
249+ {
250+ Assert . Equal ( "id" , property . Key ) ;
251+ Assert . Equal ( JsonSchemaType . Integer , property . Value . Type ) ;
252+ Assert . Equal ( "int32" , property . Value . Format ) ;
253+ } ,
254+ property =>
255+ {
256+ Assert . Equal ( "title" , property . Key ) ;
257+ Assert . Equal ( JsonSchemaType . String | JsonSchemaType . Null , property . Value . Type ) ;
258+ } ,
259+ property =>
260+ {
261+ Assert . Equal ( "completed" , property . Key ) ;
262+ Assert . Equal ( JsonSchemaType . Boolean , property . Value . Type ) ;
263+ } ,
264+ property =>
265+ {
266+ Assert . Equal ( "createdAt" , property . Key ) ;
267+ Assert . Equal ( JsonSchemaType . String , property . Value . Type ) ;
268+ Assert . Equal ( "date-time" , property . Value . Format ) ;
269+ } ) ;
270+ } ) ;
271+ }
272+
273+ [ Fact ]
274+ public async Task GetOpenApiResponse_HandlesNullablePocoValueTaskResponse ( )
275+ {
276+ // Arrange
277+ var builder = CreateBuilder ( ) ;
278+
279+ // Act
280+ #nullable enable
281+ static ValueTask < Todo ? > GetTodoValueTaskAsync ( ) => ValueTask . FromResult ( Random . Shared . Next ( ) < 0.5 ? new Todo ( 1 , "Test Title" , true , DateTime . Now ) : null ) ;
282+ builder . MapGet ( "/api" , GetTodoValueTaskAsync ) ;
283+ #nullable restore
284+
177285 // Assert
178286 await VerifyOpenApiDocument ( builder , document =>
179287 {
@@ -231,6 +339,95 @@ await VerifyOpenApiDocument(builder, document =>
231339 } ) ;
232340 }
233341
342+ [ Fact ]
343+ public async Task GetOpenApiResponse_HandlesNullableValueTypeResponse ( )
344+ {
345+ // Arrange
346+ var builder = CreateBuilder ( ) ;
347+
348+ // Act
349+ #nullable enable
350+ static Point ? GetNullablePoint ( ) => Random . Shared . Next ( ) < 0.5 ? new Point { X = 10 , Y = 20 } : null ;
351+ builder . MapGet ( "/api/nullable-point" , GetNullablePoint ) ;
352+
353+ static Coordinate ? GetNullableCoordinate ( ) => Random . Shared . Next ( ) < 0.5 ? new Coordinate ( 1.5 , 2.5 ) : null ;
354+ builder . MapGet ( "/api/nullable-coordinate" , GetNullableCoordinate ) ;
355+ #nullable restore
356+
357+ // Assert
358+ await VerifyOpenApiDocument ( builder , document =>
359+ {
360+ // Verify nullable Point response
361+ var pointOperation = document . Paths [ "/api/nullable-point" ] . Operations [ HttpMethod . Get ] ;
362+ var pointResponses = Assert . Single ( pointOperation . Responses ) ;
363+ var pointResponse = pointResponses . Value ;
364+ Assert . True ( pointResponse . Content . TryGetValue ( "application/json" , out var pointMediaType ) ) ;
365+ var pointSchema = pointMediaType . Schema ;
366+ Assert . NotNull ( pointSchema . AllOf ) ;
367+ Assert . Equal ( 2 , pointSchema . AllOf . Count ) ;
368+ Assert . Collection ( pointSchema . AllOf ,
369+ item =>
370+ {
371+ Assert . NotNull ( item ) ;
372+ Assert . Equal ( JsonSchemaType . Null , item . Type ) ;
373+ } ,
374+ item =>
375+ {
376+ Assert . NotNull ( item ) ;
377+ Assert . Equal ( JsonSchemaType . Object , item . Type ) ;
378+ Assert . Collection ( item . Properties ,
379+ property =>
380+ {
381+ Assert . Equal ( "x" , property . Key ) ;
382+ Assert . Equal ( JsonSchemaType . Integer , property . Value . Type ) ;
383+ Assert . Equal ( "int32" , property . Value . Format ) ;
384+ } ,
385+ property =>
386+ {
387+ Assert . Equal ( "y" , property . Key ) ;
388+ Assert . Equal ( JsonSchemaType . Integer , property . Value . Type ) ;
389+ Assert . Equal ( "int32" , property . Value . Format ) ;
390+ } ) ;
391+ } ) ;
392+
393+ // Verify nullable Coordinate response
394+ var coordinateOperation = document . Paths [ "/api/nullable-coordinate" ] . Operations [ HttpMethod . Get ] ;
395+ var coordinateResponses = Assert . Single ( coordinateOperation . Responses ) ;
396+ var coordinateResponse = coordinateResponses . Value ;
397+ Assert . True ( coordinateResponse . Content . TryGetValue ( "application/json" , out var coordinateMediaType ) ) ;
398+ var coordinateSchema = coordinateMediaType . Schema ;
399+ Assert . NotNull ( coordinateSchema . AllOf ) ;
400+ Assert . Equal ( 2 , coordinateSchema . AllOf . Count ) ;
401+ Assert . Collection ( coordinateSchema . AllOf ,
402+ item =>
403+ {
404+ Assert . NotNull ( item ) ;
405+ Assert . Equal ( JsonSchemaType . Null , item . Type ) ;
406+ } ,
407+ item =>
408+ {
409+ Assert . NotNull ( item ) ;
410+ Assert . Equal ( JsonSchemaType . Object , item . Type ) ;
411+ Assert . Collection ( item . Properties ,
412+ property =>
413+ {
414+ Assert . Equal ( "latitude" , property . Key ) ;
415+ Assert . Equal ( JsonSchemaType . Number , property . Value . Type ) ;
416+ Assert . Equal ( "double" , property . Value . Format ) ;
417+ } ,
418+ property =>
419+ {
420+ Assert . Equal ( "longitude" , property . Key ) ;
421+ Assert . Equal ( JsonSchemaType . Number , property . Value . Type ) ;
422+ Assert . Equal ( "double" , property . Value . Format ) ;
423+ } ) ;
424+ } ) ;
425+
426+ // Assert that Point and Coordinates are the only schemas defined at the top-level
427+ Assert . Equal ( [ "Coordinate" , "Point" ] , [ .. document . Components . Schemas . Keys ] ) ;
428+ } ) ;
429+ }
430+
234431 [ Fact ]
235432 public async Task GetOpenApiResponse_HandlesInheritedTypeResponse ( )
236433 {
@@ -732,4 +929,22 @@ private class ClassWithObjectProperty
732929 [ DefaultValue ( 32 ) ]
733930 public object AnotherObject { get ; set ; }
734931 }
932+
933+ private struct Point
934+ {
935+ public int X { get ; set ; }
936+ public int Y { get ; set ; }
937+ }
938+
939+ private readonly struct Coordinate
940+ {
941+ public double Latitude { get ; }
942+ public double Longitude { get ; }
943+
944+ public Coordinate ( double latitude , double longitude )
945+ {
946+ Latitude = latitude ;
947+ Longitude = longitude ;
948+ }
949+ }
735950}
0 commit comments