litestar-org / litestar

Production-ready, Light, Flexible and Extensible ASGI API framework | Effortlessly Build Performant APIs
https://litestar.dev/
MIT License
5.66k stars 384 forks source link

Bug: Optional types generate incorrect OpenAPI schemas #1210

Closed nilsso closed 1 year ago

nilsso commented 1 year ago

Describe the bug

  1. Optional types in query parameters don't seem to play well with OpenAPI, resulting in a bad schema, except that upon some more testing if there is more than one non-None type in the annotation it works.

A query parameter annotated int | None produces incorrectly:

{ "oneOf": [
    { "type": null" },
    { "oneOf": [] }
]}

While one annotated int | str | date | None produces correctly:

{ "oneOf": [
    { "type": "null" },
    { "type": "integer" },
    { "type": "string" },
    { "type": "string", "format": "date" }
]}
  1. Additionally, optional types are being assumed to be optional parameters, resulting in "required": false regardless of whether the parameter has a default value or not.

  2. And finally, route return types that are optional are mostly correct, but for some reason include an empty node (e.g. { "oneOf": [ { "type": "null" }, { "type": "integer" }, {} }). This results in /schema/redoc including a possible type of Any.

To Reproduce

(I ran this on 3.11, but IIRC 3.8+ for | None should work. I also tried with Optional and got the same results.)

As a minimal example:

# starlite_optionals.py
from datetime import date

from starlite import Starlite, get

@get("/a")
def a(v: int | None) -> int | None:
    return v

@get("/b")
def b(v: int | str | date | None) -> int | str | date | None:
    return v

@get("/c")
def c(v: int | None = None) -> int | None:
    return v

@get("/d")
def d(v: int | str | date | None = None) -> int | str | date | None:
    return v

app = Starlite(route_handlers=[a, b, c, d], debug=True)

Check the generated schema below, or by running uvicorn starlite_optionals:app and navigating to http://127.0.0.1:8000/schema/openapi.json and/or http://127.0.0.1:8000/schema/.

Generated OpenAPI schema. ```json { "openapi": "3.1.0", "info": { "title": "Starlite API", "version": "1.0.0" }, "servers": [ { "url": "/" } ], "paths": { "/a": { "get": { "operationId": "AA", "parameters": [ { "name": "v", "in": "query", "required": false, "deprecated": false, "allowEmptyValue": false, "allowReserved": false, "schema": { "oneOf": [ { "type": "null" }, { "oneOf": [] } ] } } ], "responses": { "200": { "description": "Request fulfilled, document follows", "headers": {}, "content": { "application/json": { "schema": { "oneOf": [ { "type": "null" }, { "type": "integer" }, {} ] } } } }, "400": { "description": "Bad request syntax or unsupported method", "content": { "application/json": { "schema": { "properties": { "status_code": { "type": "integer" }, "detail": { "type": "string" }, "extra": { "additionalProperties": {}, "type": [ "null", "object", "array" ] } }, "type": "object", "required": [ "detail", "status_code" ], "description": "Validation Exception", "examples": [ { "status_code": 400, "detail": "Bad Request", "extra": {} } ] } } } } }, "deprecated": false } }, "/b": { "get": { "operationId": "BB", "parameters": [ { "name": "v", "in": "query", "required": false, "deprecated": false, "allowEmptyValue": false, "allowReserved": false, "schema": { "oneOf": [ { "type": "null" }, { "type": "integer" }, { "type": "string" }, { "type": "string", "format": "date" } ] } } ], "responses": { "200": { "description": "Request fulfilled, document follows", "headers": {}, "content": { "application/json": { "schema": { "oneOf": [ { "type": "null" }, { "type": "integer" }, { "type": "string" }, { "type": "string", "format": "date" }, {} ] } } } }, "400": { "description": "Bad request syntax or unsupported method", "content": { "application/json": { "schema": { "properties": { "status_code": { "type": "integer" }, "detail": { "type": "string" }, "extra": { "additionalProperties": {}, "type": [ "null", "object", "array" ] } }, "type": "object", "required": [ "detail", "status_code" ], "description": "Validation Exception", "examples": [ { "status_code": 400, "detail": "Bad Request", "extra": {} } ] } } } } }, "deprecated": false } }, "/c": { "get": { "operationId": "CC", "parameters": [ { "name": "v", "in": "query", "required": false, "deprecated": false, "allowEmptyValue": false, "allowReserved": false, "schema": { "oneOf": [ { "type": "null" }, { "oneOf": [] } ] } } ], "responses": { "200": { "description": "Request fulfilled, document follows", "headers": {}, "content": { "application/json": { "schema": { "oneOf": [ { "type": "null" }, { "type": "integer" }, {} ] } } } }, "400": { "description": "Bad request syntax or unsupported method", "content": { "application/json": { "schema": { "properties": { "status_code": { "type": "integer" }, "detail": { "type": "string" }, "extra": { "additionalProperties": {}, "type": [ "null", "object", "array" ] } }, "type": "object", "required": [ "detail", "status_code" ], "description": "Validation Exception", "examples": [ { "status_code": 400, "detail": "Bad Request", "extra": {} } ] } } } } }, "deprecated": false } }, "/d": { "get": { "operationId": "DD", "parameters": [ { "name": "v", "in": "query", "required": false, "deprecated": false, "allowEmptyValue": false, "allowReserved": false, "schema": { "oneOf": [ { "type": "null" }, { "type": "integer" }, { "type": "string" }, { "type": "string", "format": "date" } ] } } ], "responses": { "200": { "description": "Request fulfilled, document follows", "headers": {}, "content": { "application/json": { "schema": { "oneOf": [ { "type": "null" }, { "type": "integer" }, { "type": "string" }, { "type": "string", "format": "date" }, {} ] } } } }, "400": { "description": "Bad request syntax or unsupported method", "content": { "application/json": { "schema": { "properties": { "status_code": { "type": "integer" }, "detail": { "type": "string" }, "extra": { "additionalProperties": {}, "type": [ "null", "object", "array" ] } }, "type": "object", "required": [ "detail", "status_code" ], "description": "Validation Exception", "examples": [ { "status_code": 400, "detail": "Bad Request", "extra": {} } ] } } } } }, "deprecated": false } } } } ```
nilsso commented 1 year ago

@all-contributors please add @nilsso for code

allcontributors[bot] commented 1 year ago

@nilsso

I've put up a pull request to add @nilsso! :tada: