From 5961a8c61c795ee5a9a238cdfd3848e40161a38d Mon Sep 17 00:00:00 2001 From: Pedro Lobato <69770518+Lob26@users.noreply.github.com> Date: Wed, 18 Jun 2025 16:49:10 -0500 Subject: [PATCH 1/2] Better jsonable encoder for iterables --- fastapi/encoders.py | 22 ++++++++++++++++++---- tests/test_jsonable_encoder.py | 17 ++++++++++++++++- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/fastapi/encoders.py b/fastapi/encoders.py index 451ea0760..4b4133c7f 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -14,14 +14,26 @@ from ipaddress import ( from pathlib import Path, PurePath from re import Pattern from types import GeneratorType -from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union +from typing import ( + Any, + Callable, + Dict, + List, + Mapping, + Optional, + Sequence, + Tuple, + Type, + Union, +) from uuid import UUID from fastapi.types import IncEx from pydantic import BaseModel -from pydantic.color import Color from pydantic.networks import AnyUrl, NameEmail from pydantic.types import SecretBytes, SecretStr +from pydantic_extra_types.color import Color +from pydantic_extra_types.coordinate import Coordinate from typing_extensions import Annotated, Doc from ._compat import PYDANTIC_V2, UndefinedType, Url, _model_dump @@ -58,6 +70,7 @@ def decimal_encoder(dec_value: Decimal) -> Union[int, float]: ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = { bytes: lambda o: o.decode(), Color: str, + Coordinate: str, datetime.date: isoformat, datetime.datetime: isoformat, datetime.time: isoformat, @@ -261,7 +274,7 @@ def jsonable_encoder( return obj if isinstance(obj, UndefinedType): return None - if isinstance(obj, dict): + if isinstance(obj, Mapping): encoded_dict = {} allowed_keys = set(obj.keys()) if include is not None: @@ -296,7 +309,8 @@ def jsonable_encoder( ) encoded_dict[encoded_key] = encoded_value return encoded_dict - if isinstance(obj, (list, set, frozenset, GeneratorType, tuple, deque)): + + if isinstance(obj, (Sequence, GeneratorType)): encoded_list = [] for item in obj: encoded_list.append( diff --git a/tests/test_jsonable_encoder.py b/tests/test_jsonable_encoder.py index 1906d6bf1..e112a1681 100644 --- a/tests/test_jsonable_encoder.py +++ b/tests/test_jsonable_encoder.py @@ -4,7 +4,7 @@ from datetime import datetime, timezone from decimal import Decimal from enum import Enum from pathlib import PurePath, PurePosixPath, PureWindowsPath -from typing import Optional +from typing import Optional, Sequence, Union import pytest from fastapi._compat import PYDANTIC_V2, Undefined @@ -316,3 +316,18 @@ def test_encode_deque_encodes_child_models(): def test_encode_pydantic_undefined(): data = {"value": Undefined} assert jsonable_encoder(data) == {"value": None} + + +def test_encode_sequence(): + class SequenceModel(Sequence[str]): + def __init__(self, items: list[str]): + self._items = items + + def __getitem__(self, index: Union[int, slice]) -> Union[str, Sequence[str]]: + return self._items[index] + + def __len__(self) -> int: + return len(self._items) + + seq = SequenceModel(["item1", "item2", "item3"]) + assert jsonable_encoder(seq) == ["item1", "item2", "item3"] From fe7af11a78ea96eaa2d2b78194b162fba9334d44 Mon Sep 17 00:00:00 2001 From: Pedro Lobato <69770518+Lob26@users.noreply.github.com> Date: Wed, 2 Jul 2025 22:39:45 -0500 Subject: [PATCH 2/2] Update encoders.py --- fastapi/encoders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/encoders.py b/fastapi/encoders.py index 4b4133c7f..a4d2e7569 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -30,9 +30,9 @@ from uuid import UUID from fastapi.types import IncEx from pydantic import BaseModel +from pydantic.color import Color from pydantic.networks import AnyUrl, NameEmail from pydantic.types import SecretBytes, SecretStr -from pydantic_extra_types.color import Color from pydantic_extra_types.coordinate import Coordinate from typing_extensions import Annotated, Doc