diff --git a/fastapi/openapi/constants.py b/fastapi/openapi/constants.py index d724ee3cf..01f543869 100644 --- a/fastapi/openapi/constants.py +++ b/fastapi/openapi/constants.py @@ -1,3 +1,13 @@ +from typing import Literal + +from typing_extensions import Annotated + METHODS_WITH_BODY = {"GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"} REF_PREFIX = "#/components/schemas/" REF_TEMPLATE = "#/components/schemas/{model}" + + +TypeValue = Annotated[ + Literal["array", "boolean", "integer", "null", "number", "object", "string"], + "Allowed type values of an object as specified in the JSON Schema https://json-schema.org/draft/2020-12/json-schema-validation#section-6.1.1", +] diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py index ed07b40f5..d7eb4a04a 100644 --- a/fastapi/openapi/models.py +++ b/fastapi/openapi/models.py @@ -10,6 +10,7 @@ from fastapi._compat import ( with_info_plain_validator_function, ) from fastapi.logger import logger +from fastapi.openapi.constants import TypeValue from pydantic import AnyUrl, BaseModel, Field from typing_extensions import Annotated, Literal, TypedDict from typing_extensions import deprecated as typing_deprecated @@ -145,7 +146,7 @@ class Schema(BaseModelWithConfig): dependentSchemas: Optional[Dict[str, "SchemaOrBool"]] = None prefixItems: Optional[List["SchemaOrBool"]] = None # TODO: uncomment and remove below when deprecating Pydantic v1 - # It generales a list of schemas for tuples, before prefixItems was available + # It generates a list of schemas for tuples, before prefixItems was available # items: Optional["SchemaOrBool"] = None items: Optional[Union["SchemaOrBool", List["SchemaOrBool"]]] = None contains: Optional["SchemaOrBool"] = None @@ -157,7 +158,7 @@ class Schema(BaseModelWithConfig): unevaluatedProperties: Optional["SchemaOrBool"] = None # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-structural # A Vocabulary for Structural Validation - type: Optional[str] = None + type: Optional[Union[TypeValue, List[TypeValue]]] = None enum: Optional[List[Any]] = None const: Optional[Any] = None multipleOf: Optional[float] = Field(default=None, gt=0) diff --git a/tests/test_custom_schema_fields.py b/tests/test_custom_schema_fields.py index ee51fc7ff..d890291b1 100644 --- a/tests/test_custom_schema_fields.py +++ b/tests/test_custom_schema_fields.py @@ -1,7 +1,13 @@ +from typing import Optional + from fastapi import FastAPI from fastapi._compat import PYDANTIC_V2 from fastapi.testclient import TestClient from pydantic import BaseModel +from typing_extensions import Annotated + +if PYDANTIC_V2: + from pydantic import WithJsonSchema app = FastAPI() @@ -10,12 +16,17 @@ class Item(BaseModel): name: str if PYDANTIC_V2: + description: Annotated[ + Optional[str], WithJsonSchema({"type": ["string", "null"]}) + ] = None + model_config = { "json_schema_extra": { "x-something-internal": {"level": 4}, } } else: + description: Optional[str] = None # type: ignore[no-redef] class Config: schema_extra = { @@ -42,7 +53,11 @@ item_schema = { "name": { "title": "Name", "type": "string", - } + }, + "description": { + "title": "Description", + "type": ["string", "null"] if PYDANTIC_V2 else "string", + }, }, } @@ -57,4 +72,4 @@ def test_response(): # For coverage response = client.get("/foo") assert response.status_code == 200, response.text - assert response.json() == {"name": "Foo item"} + assert response.json() == {"name": "Foo item", "description": None} diff --git a/tests/test_openapi_schema_type.py b/tests/test_openapi_schema_type.py new file mode 100644 index 000000000..9030eaa88 --- /dev/null +++ b/tests/test_openapi_schema_type.py @@ -0,0 +1,27 @@ +from typing import List, Optional, Union + +import pytest +from fastapi.openapi.constants import TypeValue +from fastapi.openapi.models import Schema + + +@pytest.mark.parametrize( + "type_value", + [ + "array", + ["string", "null"], + None, + ], +) +def test_allowed_schema_type( + type_value: Optional[Union[TypeValue, List[TypeValue]]], +) -> None: + """Test that Schema accepts TypeValue, List[TypeValue] and None for type field.""" + schema = Schema(type=type_value) + assert schema.type == type_value + + +def test_invalid_type_value() -> None: + """Test that Schema raises ValueError for invalid type values.""" + with pytest.raises(ValueError, match="2 validation errors for Schema"): + Schema(type=True) # type: ignore[arg-type]