diff --git a/fastapi/encoders.py b/fastapi/encoders.py index 451ea0760..a4d2e7569 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -14,7 +14,18 @@ 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 @@ -22,6 +33,7 @@ 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.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"]