From a5e28f80d618daff8ad428001c6c7e537cbda864 Mon Sep 17 00:00:00 2001 From: Lukas Rajala Date: Wed, 23 Apr 2025 11:47:30 +0200 Subject: [PATCH 1/7] Adding type values following the allowed values from json schema 2020-12 --- fastapi/openapi/constants.py | 8 ++++++++ fastapi/openapi/models.py | 5 +++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/fastapi/openapi/constants.py b/fastapi/openapi/constants.py index d724ee3cf..78a7ba735 100644 --- a/fastapi/openapi/constants.py +++ b/fastapi/openapi/constants.py @@ -1,3 +1,11 @@ +from typing import Annotated, Literal + 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..8ed5206ce 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) From f25844d9d87738d2e7ef376ce3682ad84c1127e9 Mon Sep 17 00:00:00 2001 From: Lukas Rajala Date: Wed, 23 Apr 2025 14:06:54 +0200 Subject: [PATCH 2/7] changing annotated import. --- fastapi/openapi/constants.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fastapi/openapi/constants.py b/fastapi/openapi/constants.py index 78a7ba735..01f543869 100644 --- a/fastapi/openapi/constants.py +++ b/fastapi/openapi/constants.py @@ -1,4 +1,6 @@ -from typing import Annotated, Literal +from typing import Literal + +from typing_extensions import Annotated METHODS_WITH_BODY = {"GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"} REF_PREFIX = "#/components/schemas/" From cfd09726e76c7f084f5460f8b3fbb71b5e79cf1e Mon Sep 17 00:00:00 2001 From: Lukas Rajala Date: Wed, 23 Apr 2025 15:19:19 +0200 Subject: [PATCH 3/7] changing to List from typing module. --- fastapi/openapi/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py index 8ed5206ce..d7eb4a04a 100644 --- a/fastapi/openapi/models.py +++ b/fastapi/openapi/models.py @@ -158,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[Union[TypeValue, list[TypeValue]]] = 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) From 56be13a48bf3c4d314bf5da40b40d37d275da859 Mon Sep 17 00:00:00 2001 From: Lukas Rajala Date: Thu, 24 Jul 2025 15:08:50 +0200 Subject: [PATCH 4/7] Adding simple tests for type values. --- tests/test_openapi_schema_type.py | 50 +++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 tests/test_openapi_schema_type.py diff --git a/tests/test_openapi_schema_type.py b/tests/test_openapi_schema_type.py new file mode 100644 index 000000000..85a07d8a0 --- /dev/null +++ b/tests/test_openapi_schema_type.py @@ -0,0 +1,50 @@ +import itertools +from typing import List + +import pytest +from fastapi.openapi.constants import TypeValue +from fastapi.openapi.models import Schema + +# Define all possible type values +TYPE_VALUES: List[TypeValue] = [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string", +] + +# Generate all combinations of 2 or more types +TYPE_COMBINATIONS = [ + list(combo) + for size in range(2, len(TYPE_VALUES) + 1) + for combo in itertools.combinations(TYPE_VALUES, size) +] + + +@pytest.mark.parametrize("type_val", TYPE_VALUES) +def test_schema_type_single_type_value(type_val: TypeValue) -> None: + """Test that Schema accepts single TypeValue for type field.""" + schema = Schema(type=type_val) + assert schema.type == type_val + + +@pytest.mark.parametrize("type_list", TYPE_COMBINATIONS) +def test_schema_type_multiple_type_value(type_list: List[TypeValue]) -> None: + """Test all possible combinations of TypeValue for Schema type field.""" + schema = Schema(type=type_list) + assert schema.type == type_list + + +def test_schema_type_none_value() -> None: + """Test that Schema accepts None for type field (Optional).""" + schema = Schema(type=None) + assert schema.type is None + + +def test_schema_default_type() -> None: + """Test that Schema defaults to None for type field if not specified.""" + schema_default = Schema() + assert schema_default.type is None From 3cd4f272784318865875e0fe6d4753111621cfb8 Mon Sep 17 00:00:00 2001 From: Lukas Rajala Date: Thu, 24 Jul 2025 15:54:49 +0200 Subject: [PATCH 5/7] Adding tests from similiar pr, Co-authored-by: dlax --- tests/test_custom_schema_fields.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) 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} From 93795069e32c1c00b0dc074e132c2c71f824d447 Mon Sep 17 00:00:00 2001 From: Lukas Rajala Date: Thu, 24 Jul 2025 17:11:37 +0200 Subject: [PATCH 6/7] Reducing tests. --- tests/test_openapi_schema_type.py | 65 ++++++++++--------------------- 1 file changed, 21 insertions(+), 44 deletions(-) diff --git a/tests/test_openapi_schema_type.py b/tests/test_openapi_schema_type.py index 85a07d8a0..aad699925 100644 --- a/tests/test_openapi_schema_type.py +++ b/tests/test_openapi_schema_type.py @@ -1,50 +1,27 @@ -import itertools -from typing import List +from typing import List, Optional, Union import pytest from fastapi.openapi.constants import TypeValue from fastapi.openapi.models import Schema -# Define all possible type values -TYPE_VALUES: List[TypeValue] = [ - "array", - "boolean", - "integer", - "null", - "number", - "object", - "string", -] -# Generate all combinations of 2 or more types -TYPE_COMBINATIONS = [ - list(combo) - for size in range(2, len(TYPE_VALUES) + 1) - for combo in itertools.combinations(TYPE_VALUES, size) -] - - -@pytest.mark.parametrize("type_val", TYPE_VALUES) -def test_schema_type_single_type_value(type_val: TypeValue) -> None: - """Test that Schema accepts single TypeValue for type field.""" - schema = Schema(type=type_val) - assert schema.type == type_val - - -@pytest.mark.parametrize("type_list", TYPE_COMBINATIONS) -def test_schema_type_multiple_type_value(type_list: List[TypeValue]) -> None: - """Test all possible combinations of TypeValue for Schema type field.""" - schema = Schema(type=type_list) - assert schema.type == type_list - - -def test_schema_type_none_value() -> None: - """Test that Schema accepts None for type field (Optional).""" - schema = Schema(type=None) - assert schema.type is None - - -def test_schema_default_type() -> None: - """Test that Schema defaults to None for type field if not specified.""" - schema_default = Schema() - assert schema_default.type is None +@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_invlid_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] From ba1a39cdb42897a18676d0b433ee4aa811cac104 Mon Sep 17 00:00:00 2001 From: sammasak Date: Thu, 24 Jul 2025 21:43:41 +0200 Subject: [PATCH 7/7] Update tests/test_openapi_schema_type.py Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> --- tests/test_openapi_schema_type.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_openapi_schema_type.py b/tests/test_openapi_schema_type.py index aad699925..9030eaa88 100644 --- a/tests/test_openapi_schema_type.py +++ b/tests/test_openapi_schema_type.py @@ -21,7 +21,7 @@ def test_allowed_schema_type( assert schema.type == type_value -def test_invlid_type_value() -> None: +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]