Repository: tiangolo/fastapi

Issue:
asyncpg uuid implementation not recognized by fastapi because of too strict typechecking
### Describe the bug

An alternative implementation of the `UUID` type is not serialized by fastapi but throws an error instead. This is because it is of type `asyncpg.pgproto.pgproto.UUID` instead of type `uuid.UUID`. However it is implemented in such a way that it does the exact same things as the regular UUID but just faster. Also,
```python
isinstance(asyncpg.pgproto.pgproto.UUID(), uuid.UUID) == True
```
this should make fastapi able to recognize it as a uuid.

### To Reproduce
1. Create a file called uuid_error.py with:

```Python
import uuid

from fastapi import FastAPI
from asyncpg.pgproto import pgproto

app = FastAPI()


@app.get("/normal_uuid")
def return_normal_uuid():
    regular_uuid = uuid.UUID("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
    assert isinstance(regular_uuid, uuid.UUID)
    return {"normal_uuid": regular_uuid}


@app.get("/fast_uuid")
def return_fast_uuid():
    asyncpg_uuid = pgproto.UUID("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
    assert isinstance(asyncpg_uuid, uuid.UUID)
    return {"fast_uuid": asyncpg_uuid}
```
2. Run it with uvicorn uuid_error:app
3. Open the browser and call the endpoint `/fast_uuid`.
4. It returns 'internal server error' .
5. But I expected it to return `{"fast_uuid":"a10ff360-3b1e-4984-a26f-d3ab460bdb51"}`.

### Expected behavior

I expected fastapi to jsonify the uuid and send, but instead it returns 'internal server error'. The precise error being thrown in the server is:
```
ValueError: [KeyError(<class 'asyncpg.pgproto.pgproto.UUID'>), TypeError("'asyncpg.pgproto.pgproto.UUID' object is not iterable"), TypeError('vars() argument must have __dict__ attribute')]
```

### Environment

- OS: Linux Mint 19 Tara
- FastAPI Version: 0.44.0
- Python version: 3.8.0

### Additional context

I initially made an issue in the asyncpg repo asking them if they could fix it: https://github.com/MagicStack/asyncpg/issues/512 I think it's usefull as additional context for this issue.
The problem can be solved here in the fastapi code by improving the code a little. I'm working on a PR, this line seems to be the issue:
https://github.com/tiangolo/fastapi/blob/c200bc2240d48a4a27e96fba536351038eafc44f/fastapi/encoders.py#L113
I think it shouldn't use `type()` but it should instead use `isinstance()` this will make for slightly looser typechecking but I think that might be a good thing in this case?



Discussion:

Resolving patch:
diff --git a/fastapi/encoders.py b/fastapi/encoders.py
--- a/fastapi/encoders.py
+++ b/fastapi/encoders.py
@@ -1,6 +1,6 @@
 from enum import Enum
 from types import GeneratorType
-from typing import Any, Dict, List, Set, Union
+from typing import Any, Callable, Dict, List, Set, Tuple, Union
 
 from fastapi.utils import PYDANTIC_1, logger
 from pydantic import BaseModel
@@ -10,6 +10,21 @@
 DictIntStrAny = Dict[Union[int, str], Any]
 
 
+def generate_encoders_by_class_tuples(
+    type_encoder_map: Dict[Any, Callable]
+) -> Dict[Callable, Tuple]:
+    encoders_by_classes: Dict[Callable, List] = {}
+    for type_, encoder in type_encoder_map.items():
+        encoders_by_classes.setdefault(encoder, []).append(type_)
+    encoders_by_class_tuples: Dict[Callable, Tuple] = {}
+    for encoder, classes in encoders_by_classes.items():
+        encoders_by_class_tuples[encoder] = tuple(classes)
+    return encoders_by_class_tuples
+
+
+encoders_by_class_tuples = generate_encoders_by_class_tuples(ENCODERS_BY_TYPE)
+
+
 def jsonable_encoder(
     obj: Any,
     include: Union[SetIntStr, DictIntStrAny] = None,
@@ -105,24 +120,31 @@ def jsonable_encoder(
                 )
             )
         return encoded_list
+
+    if custom_encoder:
+        if type(obj) in custom_encoder:
+            return custom_encoder[type(obj)](obj)
+        else:
+            for encoder_type, encoder in custom_encoder.items():
+                if isinstance(obj, encoder_type):
+                    return encoder(obj)
+
+    if type(obj) in ENCODERS_BY_TYPE:
+        return ENCODERS_BY_TYPE[type(obj)](obj)
+    for encoder, classes_tuple in encoders_by_class_tuples.items():
+        if isinstance(obj, classes_tuple):
+            return encoder(obj)
+
     errors: List[Exception] = []
     try:
-        if custom_encoder and type(obj) in custom_encoder:
-            encoder = custom_encoder[type(obj)]
-        else:
-            encoder = ENCODERS_BY_TYPE[type(obj)]
-        return encoder(obj)
-    except KeyError as e:
+        data = dict(obj)
+    except Exception as e:
         errors.append(e)
         try:
-            data = dict(obj)
+            data = vars(obj)
         except Exception as e:
             errors.append(e)
-            try:
-                data = vars(obj)
-            except Exception as e:
-                errors.append(e)
-                raise ValueError(errors)
+            raise ValueError(errors)
     return jsonable_encoder(
         data,
         by_alias=by_alias,


Test patch:
diff --git a/tests/test_inherited_custom_class.py b/tests/test_inherited_custom_class.py
new file mode 100644
--- /dev/null
+++ b/tests/test_inherited_custom_class.py
@@ -0,0 +1,73 @@
+import uuid
+
+import pytest
+from fastapi import FastAPI
+from pydantic import BaseModel
+from starlette.testclient import TestClient
+
+app = FastAPI()
+
+
+class MyUuid:
+    def __init__(self, uuid_string: str):
+        self.uuid = uuid_string
+
+    def __str__(self):
+        return self.uuid
+
+    @property
+    def __class__(self):
+        return uuid.UUID
+
+    @property
+    def __dict__(self):
+        """Spoof a missing __dict__ by raising TypeError, this is how
+        asyncpg.pgroto.pgproto.UUID behaves"""
+        raise TypeError("vars() argument must have __dict__ attribute")
+
+
+@app.get("/fast_uuid")
+def return_fast_uuid():
+    # I don't want to import asyncpg for this test so I made my own UUID
+    # Import asyncpg and uncomment the two lines below for the actual bug
+
+    # from asyncpg.pgproto import pgproto
+    # asyncpg_uuid = pgproto.UUID("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
+
+    asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
+    assert isinstance(asyncpg_uuid, uuid.UUID)
+    assert type(asyncpg_uuid) != uuid.UUID
+    with pytest.raises(TypeError):
+        vars(asyncpg_uuid)
+    return {"fast_uuid": asyncpg_uuid}
+
+
+class SomeCustomClass(BaseModel):
+    class Config:
+        arbitrary_types_allowed = True
+        json_encoders = {uuid.UUID: str}
+
+    a_uuid: MyUuid
+
+
+@app.get("/get_custom_class")
+def return_some_user():
+    # Test that the fix also works for custom pydantic classes
+    return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01"))
+
+
+client = TestClient(app)
+
+
+def test_dt():
+    with client:
+        response_simple = client.get("/fast_uuid")
+        response_pydantic = client.get("/get_custom_class")
+
+    assert response_simple.json() == {
+        "fast_uuid": "a10ff360-3b1e-4984-a26f-d3ab460bdb51"
+    }
+
+    assert response_pydantic.json() == {
+        "a_uuid": "b8799909-f914-42de-91bc-95c819218d01"
+    }