Skip to content

[Open API] Nullability set on component schema level and not operation. Causing multiple component schemas being generated. #58617

@marinasundstrom

Description

@marinasundstrom

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

Deal-breaker for migration. I discovered this while porting my API from using NSwag.

One component schema per type, not distinct schemas based om nullability. And I think this is a part of a bigger story about dealing with nullability.

I think this also affects Minimal APIs.

In this particular case, it seems like nullability is set on a component schema-level and not the schema on on parameter and response-level

This happens because of the action (Test) with return type TestDto regardless of whether it is nullable or not. So no concern for explicit nullability either.

Following is for OpenAPI 3.

Consider this controller:

[ApiController]
[Route([controller]")]
public class StatisticsController : ControllerBase
{

    // TestDto corresponds to component schema "TestDto2" with nullable: true

    [HttpGet("Test")]
    public TestDto Test()
    {
        return default!;
    }

/*
    // Not relevant
    [HttpGet("Test2")]
    public TestDto? Test2()
    {
        return default!;
    }
*/

    // TestDto corresponds to component schema "TestDto" (with nullable: false)

    [HttpGet("Test3")]
    public KeyValuePair<int, TestDto> Test3()
    {
        return default!;
    }
}

Instead of one TestDto schema, it results in two distinct schemas TestDto and TestDto2 where one is nullable.

The response on the operation should be nullable at an operation-level, not at component schema level:

components:
  schemas:
    TestDto:
      required:
        - x
      type: object
      properties:
        x:
          type: string
    TestDto2:
      required:
        - x
      type: object
      properties:
        x:
          type: string
      nullable: true <—- THIS IS WHY THE TYPE HAS TWO SCHEMAS:

With the operation being tis:

paths:
  /Statistics/Test2:
    get:
      tags:
        - Statistics
      responses:
        '200':
          description: OK
          content:
            text/plain:
              schema:
                $ref: '#/components/schemas/TestDto2’
            application/json:
              schema:
                $ref: '#/components/schemas/TestDto2’
            text/json:
              schema:
                $ref: '#/components/schemas/TestDto2’

Expected Behavior

I would expect this operation with the shared component schema, and the nullability set on the operation:

paths:
  /Statistics/Test2:
    get:
      tags:
        - Statistics
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/TestDto'
                nullable: true
components:
  schemas:
    TestDto:
      required:
        - x
      type: object
      properties:
        x:
          type: string

Meaning the use of ”allOf” and ”nullable: true” on the response.

This allows for maximal re-use of DTOs. Not having two otherwise identical schemas that are different in nullability.

Steps To Reproduce

var builder = WebApplication.CreateBuilder();

builder.Services.AddControllers();

builder.Services.AddOpenApi();

var app = builder.Build();

app.MapControllers();

app.MapOpenApi();

app.Run();

Exceptions (if any)

No response

.NET Version

9.0.100-rc.2.24474.11

Anything else?

The bigger story would be to write a transformer that deals with nullability for parameters and responses.

I'm on my way prototyping for my own project.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-mvcIncludes: MVC, Actions and Controllers, Localization, CORS, most templatesfeature-openapi

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions