Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Cannot control json serialization with custom response_class #8947

Discussion options

class CustomJSONResponse(JSONResponse):
    media_type = "application/json"

    def render(self, content: typing.Any) -> bytes:
        return dumps(content)

With dumps being a custom function, managing datetime values specifically.

@router.post("/requests")
async def insert_user_request(request: Request):
   return CustomJSONResponse(content={"timestamp": datetime.now()}, status_code=HTTP_201_CREATED)

Will work as expected but

@router.post("/requests", response_class=CustomJSONResponse, status_code=HTTP_201_CREATED)
async def insert_user_request(request: Request):
    return {"timestamp": datetime.now()}

Will fail to use the custom dump function.

The cause is in https://github.com/tiangolo/fastapi/blob/master/fastapi/routing.py#L190 : serialize_response (which calls jsonable_encoder) is called before the response_class instantiation (https://github.com/tiangolo/fastapi/blob/master/fastapi/routing.py#L201) so datetime values are converted to str prematurely.

You must be logged in to vote

Thanks for this write up! It did help me to figure out the issue I had.
This could be boiled down to the following test, which passes if I use Pydantic v1 and fails for Pydantic 2:

def test_serialized_date():
    from fastapi.encoders import jsonable_encoder

    class A(pydantic.BaseModel):
        d: datetime.datetime

    serialized_datetime = "2021-04-20T22:33:44.123456+00:00"
    a = A(d=datetime.datetime.fromisoformat(serialized_datetime))
    e = jsonable_encoder(a)
    assert e["d"] == serialized_datetime

With Pydantic 2, the value of e["d"] is 2021-04-20T22:33:44.123456Z. In my case the models are under my control, so annotating the model solves my issue:

    class A(pydantic.Ba…

Replies: 9 comments · 2 replies

Comment options

datetime encoder is coming from pydantic : https://github.com/samuelcolvin/pydantic/blob/master/pydantic/json.py#L20 i guess it's still possible to alter this one if we need custom encoder for datetime.

You must be logged in to vote
0 replies
Comment options

@TTRh : the explicit return of an CustomJSONResponse instance works fine without having to tweak pydantic. My point is that I wouldn't have expected a different behavior between the two syntaxes.

You must be logged in to vote
0 replies
Comment options

Have the same issue. Did you solve it?

You must be logged in to vote
0 replies
Comment options

Have the same issue. Did you solve it?

Only a PR would really solve it, provided it's considered as an issue, hence my "question" here.
Two possible workarounds:

  1. Explicit instantiation as illustrated above
  2. Tweak pydantic as suggested by @TTRh, e.g. pydantic.json.ENCODERS_BY_TYPE[datetime] = lambda dt: dt.replace(tzinfo=pytz.utc).isoformat()
You must be logged in to vote
0 replies
Comment options

@kshilov also you can create a workaround with default=str and encode()

class CustomJSONResponse(JSONResponse):
    media_type = "application/json"

    def render(self, content: typing.Any) -> bytes:
        return dumps(content, default=str).encode()

This will work however, I agree with @tangi75 it is a serious bug and should be handled by a PR.

You must be logged in to vote
0 replies
Comment options

I have the same issue where I want Enums to return the name and not the value of the object. I ended up creating my own JSONResponse and had to wrap all my return in to a JSONResponse(...). It would have been nice to just use the response_class option, but because serialize_response is called before my custom JSONResponse, it does not work.

Looks like on the pydantic side there is also no real easy way to use our own JSON serializer - which would be very useful since people may want to do serialization differently (for instance when returning nan from pandas / numpy).

Any chance of that making to a PR or it that too much of a change that it would break things?

Thanks

You must be logged in to vote
0 replies
Comment options

Looks like on the pydantic side there is also no real easy way to use our own JSON serializer - which would be very useful since people may want to do serialization differently (for instance when returning nan from pandas / numpy).

+1

I'm currently using FastAPI for an API migration and want to use a custom json encoder for parity reasons.
I discovered this after diffing responses from both APIs.

A small example of what I'm seeing (this timestamp is from a pandas dataframe):

legacy timestamp: "2022-01-06 00:00:00+00:00"
fastapi timestamp: "2022-01-06T00:00:00+00:00"

Current FastAPI code

(shortened version of the current code in get_request_handler)

raw_response = await run_endpoint_function(...)
response_data = await serialize_response(response_content=raw_response, ...)
response = actual_response_class(response_data, **response_args)

serialize_response calls the default FastAPI jsonable_encoder meaning response_data has been encoded before the actual_response_class is invoked.

It seems like the recommendation in this thread is to explicitly return a custom Response inside each route -- that does work, but it would be nice if we could just set a default_response_class when initializing FastAPI and call it a day.

Would it be possible to either:

  • pass raw_response as a kwarg to actual_response_class?
  • pass along a custom json encoder that would be called inside serialize_response?
You must be logged in to vote
0 replies
Comment options

@nguyent +1 on this, I am using mashumaro.

You must be logged in to vote
0 replies
Comment options

I experienced this from a slightly different direction. Writing up my experience in case anyone has ideas for me or in case this will help anyone else:

I return responses including various custom types which aren't json-serializable and which aren't serializable by pydantic by default. Usually this looks something like this:

class SomeModel(BaseModel):
    regular_field: str
    nothing_special_here: int
    arbitrary_data: Dict[str, Any]

I didn't want to add custom serialization at the pydantic level in each model because it would be repetitive.
And I wasn't able to modify the types or subclass them to add custom pydantic serialization at the type level.

Before upgrading to pydantic 2, I added custom serialization at the FastAPI level using a custom response class and setting it as the default response class (along the lines of what was recommended here). Something like this:

def serialize_arbitrary_data(obj: Any) -> Any:
    ...

class MyJsonResponse(JSONResponse):
   def render(self, content: Any) -> bytes:
        return json.dumps(content, default=serialize_arbitrary_data)

This worked because the behavior with pydantic 1 is to leave unknown types alone when serializing.

When I upgraded to pydantic 2, this stopped working, because pydantic tries (and fails) to convert the custom types to json-serializable primitives before my custom response's render() is called. Even if the fields are typed as Any or annotated with SerializeAsAny this is the case, because FastAPI asks pydantic to model_dump(mode="json") before sending the data to the response class.

The same is true for adding encoders to fastapi.encoders.encoders_by_class_tuples; it is too late by the time they are used.

This is a reasonable design decision (to say that all conversion to json-serializable primitives must occur at the pydantic level, and only the JSON serialization itself can be controlled in the response class) but it definitely removed functionality that cannot be achieved in another way (that I have found so far).

In case any of the maintainers read this, would you be open to a feature allowing opt-in behavior of calling model_dump() with mode="python", or some similar workaround?

The next-best workaround I know of is to use the json_encoders config field in pydantic, but that's less than ideal for a few reasons:

  1. It's "deprecated and will likely be removed in the future" according to the docs. It's worth noting that the reason it was not deleted yet is that there is no "1:1 replacement", so it may be reasonable to hope that if/when it is removed, an equivalent or better mechanism will be added to replace it.
  2. If I want to get the behavior globally on all of my models, I need to subclass BaseModel and use it everywhere, or something like that. But that's also not great, since any models which define additional json_encoders would need to manually merge with the config of the base class, etc.
  3. It won't work for pydantic models from third-party packages whose source I don't control.

In the meantime, I have (very redundantly) done the serialization in pydantic by annotating all of the models, using something like this:

def serialize_arbitrary_data(obj: Any) -> Any:
    ...

def recursively_serialize_arbitrary_data(obj: Any) -> Any:
    # recursively walk a nested structure, calling serialize_arbitrary_data() on leaves
    ...

ArbitraryData = Annotated[
    Dict[str, Any], PlainSerializer(lambda x: recursively_serialize_arbitrary_data(x))
]

class SomeModel(BaseModel):
    regular_field: str
    nothing_special_here: int
    arbitrary_data: ArbitraryData
You must be logged in to vote
2 replies
@anton-daneyko-ultramarin
Comment options

Thanks for this write up! It did help me to figure out the issue I had.
This could be boiled down to the following test, which passes if I use Pydantic v1 and fails for Pydantic 2:

def test_serialized_date():
    from fastapi.encoders import jsonable_encoder

    class A(pydantic.BaseModel):
        d: datetime.datetime

    serialized_datetime = "2021-04-20T22:33:44.123456+00:00"
    a = A(d=datetime.datetime.fromisoformat(serialized_datetime))
    e = jsonable_encoder(a)
    assert e["d"] == serialized_datetime

With Pydantic 2, the value of e["d"] is 2021-04-20T22:33:44.123456Z. In my case the models are under my control, so annotating the model solves my issue:

    class A(pydantic.BaseModel):
        d: Annotated[datetime.datetime, PlainSerializer(lambda x: x.isoformat())]
Answer selected by YuriiMotov
@anton-daneyko-ultramarin
Comment options

@YuriiMotov I don't think it solves the original question and should be marked as an answer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
9 participants
Converted from issue

This discussion was converted from issue #2117 on February 28, 2023 17:12.

Morty Proxy This is a proxified and sanitized view of the page, visit original site.