pydantic: `anyOf` causing problems with Swagger/ReDoc on primitive types
Initial Checks
- I confirm that I’m using Pydantic V2
Description
If we have an object, and a primite type, we are still creating an anyOf. That unfortunately doesn’t work well with neither ReDoc nor Swagger.
From the code below, the following OpenAPI spec is generated:
Details
{
"components": {
"schemas": {
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"title": "Detail",
"type": "array"
}
},
"title": "HTTPValidationError",
"type": "object"
},
"ValidationError": {
"properties": {
"loc": {
"items": {
"type": ["string", "integer"]
},
"title": "Location",
"type": "array"
},
"msg": {
"title": "Message",
"type": "string"
},
"type": {
"title": "Error Type",
"type": "string"
}
},
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError",
"type": "object"
}
}
},
"info": {
"title": "FastAPI",
"version": "0.1.0"
},
"openapi": "3.1.0",
"paths": {
"/": {
"get": {
"operationId": "root__get",
"parameters": [
{
"description": "Date to query",
"in": "query",
"name": "at",
"required": false,
"schema": {
"anyOf": [
{
"enum": [
"today",
"tomorrow",
"yesterday"
],
"type": "string"
},
{
"type": "null"
}
],
"description": "Date to query",
"title": "At"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {}
}
},
"description": "Successful Response"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
"description": "Validation Error"
}
},
"summary": "Root"
}
}
}
}
Ideally, we’d have the following schema:
Details
{
"components": {
"schemas": {
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"title": "Detail",
"type": "array"
}
},
"title": "HTTPValidationError",
"type": "object"
},
"ValidationError": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"title": "Location",
"type": "array"
},
"msg": {
"title": "Message",
"type": "string"
},
"type": {
"title": "Error Type",
"type": "string"
}
},
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError",
"type": "object"
}
}
},
"info": {
"title": "FastAPI",
"version": "0.1.0"
},
"openapi": "3.1.0",
"paths": {
"/": {
"get": {
"operationId": "root__get",
"parameters": [
{
"description": "Date to query",
"in": "query",
"name": "at",
"required": false,
"schema": {
"enum": [
"today",
"tomorrow",
"yesterday"
],
"type": ["string", "null"]
"description": "Date to query",
"title": "At"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {}
}
},
"description": "Successful Response"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
"description": "Validation Error"
}
},
"summary": "Root"
}
}
}
}
As you can see above, the type becomes an array of primitive types. This is mentioned in the Assertions and Instance Primitive Types on the JSON Schema Draft 2020-12.
Example Code
from typing import Literal, Annotated
from fastapi import Query, FastAPI
app = FastAPI()
@app.get("/")
def root(
at: Annotated[
Literal["today", "tomorrow", "yesterday"] | None,
Query(
description="Date to query",
),
] = None
):
return at
Python, Pydantic & OS Version
pydantic version: 2.0.2
pydantic-core version: 2.2.0 release build profile
install path: /Users/marcelotryle/dev/pydantic/pydantic/pydantic
python version: 3.11.1 (main, Apr 20 2023, 11:08:52) [Clang 14.0.3 (clang-1403.0.22.14.1)]
platform: macOS-13.4-arm64-arm-64bit
optional deps. installed: ['devtools', 'email-validator', 'typing-extensions']
Ref.:
- https://github.com/tiangolo/fastapi/discussions/9709#discussioncomment-6415698
- https://github.com/tiangolo/fastapi/discussions/9709#discussioncomment-6437341
Selected Assignee: @dmontagu
About this issue
- Original URL
- State: closed
- Created a year ago
- Reactions: 5
- Comments: 15 (13 by maintainers)
I think the solution of modifying FastAPI to use a custom
GenerateJsonSchema(the PR that @Kludex opened) is going to be preferable for fixing swagger, as it won’t require anything special in user code, and I think it’s not reasonable to change what we are currently generating as the JSON schema forOptional[int]since it’s technically more correct for the type annotation, is definitely what you want for Body parameters, and FastAPI currently generates the openapi spec using functions from pydantic that couldn’t be aware of this distinction.However, once #6798 is merged, it will be possible to do:
which will then let you do:
which will get you a JSON schema / OpenAPI schema that looks the way it did with Pydantic V1. (And as you can see, still type-checks properly.)
Given this, I think after merging that PR it will make sense to close this issue in Pydantic, and instead open a FastAPI issue for the bug (and/or just wait/hope for https://github.com/tiangolo/fastapi/pull/9873 to be merged).
Rendering is borked for
type:[…,"null"]in path/query … most likely everything else as well. I’ll create a demo and report. https://github.com/swagger-api/swagger-ui/issues/9056v2:
age: int = Noneequals v1:age: Optional[int]- so I expect it’ll behave all the same.Similar issue where previously
nullwas shown by removing from therequiredlist. Instead there is now ananyOfwithnullas one of the fields.When using pydantic v1 it returns this OpenAPI schema for RequestModel
When using pydantic v2 it returns this OpenAPI schema for RequestModel
I understand that both are basically the same meaing for my purposes however I have code generation that runs off the OpenAPI schema and the former generates better TypeScript code. Is there a way to generate the former?
We have a solution using Pydantic only: #6653.
We are also working on a solution for FastAPI without the need of
SkipJsonSchema: https://github.com/tiangolo/fastapi/pull/9873.You missed the anyOf in ValidationError. And I’d expect null to be part of the enum due to https://json-schema.org/draft/2020-12/json-schema-validation.html#name-enum