cloudpathlib
cloudpathlib copied to clipboard
Support FastAPI jsonable_encoder (through docs or Pydantic config)
By default, if I have AnyPath
as a type in a Pydantic model used by FastAPI in a response_model
config for a route, FastAPI cannot resolve an appropriate json encoder and fails with a recursion error.
So currently an example like the one below will fail with a recursion error in fastapi.encoders.jsonable_encoder
from typing import List
from cloudpathlib import Asset
from fastapi import FastAPI
from pydantic import BaseModel
class Asset(BaseModel):
name: str
path: AnyPath
app = FastAPI()
assets = [
Asset(name="my-asset", path="s3://my-bucket/my-asset")
]
@app.get("/assets", response_model=List[Asset])
def assets():
return assets
Pydantic allows setting custom json_encoders
in a model's Config
class so I can update my model to fix this error:
class Asset(BaseModel):
name: str
path: AnyPath
class Config:
json_encoders = {
AnyPath: str
}
and FastAPI will use that info in jsonable_encoder
to return the str repr of AnyPath
. But this is a bit inconvenient so maybe AnyPath
as a type should encode this for itself since it's an obvious encoder (always convert to it's str repr)
Maybe by patching pydantic.json.ENCODERS_BY_TYPE
? Not really sure.
But this issue should probably at least be documented since a lot of people using Pydantic also use FastAPI.
But this is a bit inconvenient so maybe AnyPath as a type should encode this for itself since it's an obvious encoder (always convert to it's str repr)
+1 for this request.
use jsonable_encoder
cannot really solve this issue; consider the following example.
class Asset(BaseModel):
name: str
path: AnyPath
class Config:
json_encoders = {
GSPath: str
}
class B(BaseModel):
asset: Asset
In [18]: a = Asset(name="123", path="gs://foo/bar")
In [19]: a.json()
Out[19]: '{"name": "123", "path": "gs://foo/bar"}'
In [20]: b = B(asset=a)
In [21]: b.json()
TypeError: Object of type 'GSPath' is not JSON serializable
Which means I need to change Config
for all classes that may contains CloudPath..
@lucemia Subclassing the BaseModel that you use is, I believe, the recommended way of doing this in Pydantic.
I'm not sure there's a good, supported implementation on our side for this at the moment. We'd rather not implement a solution that depends on the internals of Pydantic to work rather than a supported path for registering custom json encoders.
Here's an example:
from pathlib import Path
from cloudpathlib import AnyPath, CloudPath
from pydantic import BaseModel
class CustomBase(BaseModel):
class Config:
json_encoders = {
CloudPath: str,
Path: str
}
class Asset(CustomBase):
name: str
cloud: CloudPath
local: Path
anywhere: AnyPath
class B(CustomBase):
asset: Asset
a = Asset(name="123", cloud="gs://foo/bar", local="/hello/my/friend", anywhere="/yes/indeed.txt")
a
#> Asset(name='123', cloud=GSPath('gs://foo/bar'), local=PosixPath('/hello/my/friend'), anywhere=PosixPath('/yes/indeed.txt'))
b = B(asset=a)
b
#> B(asset=Asset(name='123', cloud=GSPath('gs://foo/bar'), local=PosixPath('/hello/my/friend'), anywhere=PosixPath('/yes/indeed.txt')))
print(b.json(indent=2))
#> {
#> "asset": {
#> "name": "123",
#> "cloud": "gs://foo/bar",
#> "local": "/hello/my/friend",
#> "anywhere": "/yes/indeed.txt"
#> }
#> }