Skip to content
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

Change default of DefaultResponseReferenceTypeNullHandling to NotNull #2215

Merged
merged 2 commits into from
Jun 10, 2019

Conversation

RicoSuter
Copy link
Owner

@RicoSuter RicoSuter commented Jun 7, 2019

This PR:

  • Changes the default of DefaultResponseReferenceTypeNullHandling from Null to NotNull.
  • Adds support for the "nullable" xml docs attribute

Details:

  • Currently there is no way to specify nullability of the response with ProducesResponseTypeAttribute. This could be solved with a new "nullable" attribute in the XML docs (which always wins if specified):
/// <summary>
/// Gets an order.
/// </summary>
/// <response code="200" nullable="true">Order created.</response>
...
  • Most of the time (I assume?) people want a not nullable response and use 404 in the not found case and do not return null
  • I think Swagger UI 3 still cannot handle nullable responses (spec in the first example)
  • In the Swagger 2 the difference is only in the x-nullable property and thus the impact is lower.

Sample operation:

[HttpGet("{petId}", Name = "FindPetById")]
[Produces("application/json")]
[ProducesResponseType(typeof(Pet), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(SerializableError), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Pet>> FindById(int petId)

With DefaultResponseReferenceTypeNullHandling = Null (current default, OpenAPI 3)

    "/pet/{petId}": {
      "get": {
        "operationId": "Pet_FindById",
        "parameters": [
          {
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "nullable": true,
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Pet"
                    }
                  ]
                }
              }
            }
          },

With DefaultResponseReferenceTypeNullHandling = NotNull (new default, OpenAPI 3)

    "/pet/{petId}": {
      "get": {
        "operationId": "Pet_FindById",
        "parameters": [
          {
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Pet"
                }
              }
            }
          },

Open questions:

  • Should we change the default?
    • Pro: Most devs probably already design their APIs so that they don't return null and thus expect the default to be NotNull
    • Con: NotNull does not really reflect what the actual API is allowed to return (i.e. it is a missmatch between the spec and the runtime)

/cc @pranavkm @dougbu What's your take on this?

@RicoSuter
Copy link
Owner Author

RicoSuter commented Jun 7, 2019

How does that impact the generated C#/TS code?

  • CSharp does not change because reference types cannot be made non nullable
  • TypeScript will change from Person | null as response to Person

You can still change this default back to how it is now (v12)

@RicoSuter
Copy link
Owner Author

RicoSuter commented Jun 7, 2019

I see another problem in the future:

Even with Nullable Reference Types (C# 8) you cannot describe nullability with the ProduceResponseTypeAttribute, because this [ProducesResponseType(typeof(Pet?), StatusCodes.Status200OK)] is not allowed (because it's just a Type object with no context and there is no place to generate the NRT NullableAttribute).

Maybe it would be best to also add an IsNullable attribute to the ProducesResponseTypeAttribute?
/cc @rynowak @dougbu @pranavkm

@StasPerekrestov
Copy link
Contributor

BTW, some good news concerning the mentioned UI issue
swagger-api/swagger-ui#3803 (comment)

@RicoSuter
Copy link
Owner Author

@StasPerekrestov good to know... what’s your opinion on the default?

@dougbu
Copy link
Contributor

dougbu commented Jun 10, 2019

Most of the time (I assume?) people want a not nullable response and use 404 in the not found case and do not return null

I don't know the whole history here but agree with this sentiment. Provide a well-documented override of this default and all would be well from my perspective.

you cannot describe nullability with the ProduceResponseTypeAttribute

Your point is valid but leave nullable reference types out of it: Knowing the compiler will allow a null assignment is very different from knowing the user will assign null in some cases.

Agree a ReturnsNull property or similar may be needed if we find users return null in a few but not all actions.

@RicoSuter
Copy link
Owner Author

Your point is valid but leave nullable reference types out of it: Knowing the compiler will allow a null assignment is very different from knowing the user will assign null in some cases.

The problem here is the inconsistency from a user perspective: As a user in an NRT world, I'd expect that typeof(Pet) is automatically treated as not nullable and typeof(Pet?) is nullable (but this probably does not compile, right?), but from a tooling perspective we cannot assume not nullable because typeof(Pet?) is not allowed. As a user I'd like to express nullability in the same way as I do it everywhere else (e.g. parameters) - not with an additional property on the attribute...

@StasPerekrestov
Copy link
Contributor

StasPerekrestov commented Jun 10, 2019 via email

@RicoSuter
Copy link
Owner Author

I guess, you are looking for something like Optional/Maybe to avoid
using attributes, however usage of these patterns isn't common in c#
world...

Or Nullable<T> but that's only for value types - however as a user I just expect that Pet vs Pet? just works everywhere and thus I'm a bit worried that people will be quite confused.

@StasPerekrestov
Copy link
Contributor

StasPerekrestov commented Jun 10, 2019 via email

@RicoSuter
Copy link
Owner Author

RicoSuter commented Jun 10, 2019

In c# world we don't have to many options to go with. We need either to
wrap instances, e.g. Maybe monad, but c# doesn't have appropriate sugar
syntax to deal with it, or use Attributes to express the disired behavior.

My point is that ideally this should have been solved with the C# 8 NRT feature somehow - right now typeof() cannot "store" nullability info, but e.g. the params in Method(string foo, string? bar) can (because params can have attributes and typeof is "produces" just a Type object)...

However this PR is not really about NRT or C# 8 but about changing the default of DefaultResponseReferenceTypeNullHandling to NotNull and adding the "nullable" attribute to the xml docs (this will solve the problems for now). My big question is: What is the best default for DefaultResponseReferenceTypeNullHandling?

@StasPerekrestov
Copy link
Contributor

What concerns me, is that XML docs now change the behavior of XML schema generation. This is something, not many developers are aware of and also may be a source of confusion.

From my point of view, I'd rather look for an appropriate .net attribute to change the required behavior than for an attribute of XML doc.

@StasPerekrestov
Copy link
Contributor

Regarding the new defaults, I'm OK with that. However, the information that describes the behavior and the way of changing it should be more easy to find.

@RicoSuter
Copy link
Owner Author

RicoSuter commented Jun 10, 2019

What concerns me, is that XML docs now change the behavior of XML schema generation. This is something, not many developers are aware of and also may be a source of confusion.

Yes, somehow you are right, but the alternative is extremely verbose:

/// <summary>
/// Gets an order.
/// </summary>
/// <response code="200">The order.</response>
[HttpGet("{orderId}", Name = "GetOrder")]
[ProducesResponseType(typeof(Pet), StatusCodes.Status200OK)]
[ProducesResponseTypeNullable(true, StatusCodes.Status200OK)]
public async Task<ActionResult<Order>> GetOrder(int orderId)

I think it's the still the nicest way to do as xml docs attribute as it is the only way to avoid having specifying a single response code three times...

Ideally there's just a IsNullable (or similar) property on the ProducesResponseTypeAttribute but for backward compat. we'd still keep the "nullable" xml docs attribute.

@RicoSuter
Copy link
Owner Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants