Skip to content

Add PydanticJSONResponse#14299

Closed
eltoder wants to merge 3 commits intofastapi:masterfrom
eltoder:feature/pydantic-json-response
Closed

Add PydanticJSONResponse#14299
eltoder wants to merge 3 commits intofastapi:masterfrom
eltoder:feature/pydantic-json-response

Conversation

@eltoder
Copy link

@eltoder eltoder commented Nov 5, 2025

When using this response class, json serialization is done using Pydantic's built-in json serialization (dump_json(r)) instead of generating an intermediate dict that is later serialized using a json library (json.dumps(dump_python(r))).

In my testing this is 3-4x faster than using the standard json library (the default) and 50% faster than using orjson, without requiring any extra dependencies. This also allows configuring serialization behavior per model using Pydantic's model_config.

@eltoder eltoder force-pushed the feature/pydantic-json-response branch from 19edd42 to 3fd70a1 Compare November 5, 2025 21:23
@eltoder
Copy link
Author

eltoder commented Nov 5, 2025

A simple benchmark script:

from datetime import date, timedelta
import json, orjson, ujson
import timeit
from pydantic import BaseModel, TypeAdapter

class Response(BaseModel):
    values: dict[int, dict[date, float]]

def make_response(n: int, m: int) -> Response:
    base = date(2025, 1, 1)
    values = {
        i + 1: {base + timedelta(i + j): (i + j) / 12.3 for j in range(m)}
        for i in range(n)
    }
    return Response(values=values)

ta = TypeAdapter(Response)
r = make_response(2, 20)
cases = {
    "json": "json.dumps(ta.dump_python(r, mode='json'), ensure_ascii=False).encode()",
    "ujson": "ujson.dumps(ta.dump_python(r, mode='json'), ensure_ascii=False).encode()",
    "orjson": "orjson.dumps(ta.dump_python(r, mode='json'), option=orjson.OPT_NON_STR_KEYS)",
    "pydantic": "ta.dump_json(r)",
}
for case, code in cases.items():
    print(case, min(timeit.repeat(code, number=10000, globals=globals())))

Results:

json 0.23686759299016558
ujson 0.12213927501579747
orjson 0.09217712600366212
pydantic 0.06348165299277753

@YuriiMotov YuriiMotov added the feature New feature or request label Nov 5, 2025
Copy link
Member

@YuriiMotov YuriiMotov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eltoder, thanks you for working on this!

Looks good to me in general, but I think we should add a bit more tests:

  • test that it raises with Pydantic V1 models (when V1 installed or with from pydantic.v1 import BaseModel) (done)
  • I would add some tests with different data and values of response_model_** parameters to show the resulted JSON comparing to result with regular Response class. Results should be equal except some cases when json.dumps raises but Pydantic handles it without error.
  • show that response is validated using response model

@YuriiMotov

This comment was marked as resolved.

@eltoder
Copy link
Author

eltoder commented Nov 7, 2025

@YuriiMotov thanks for taking a look! This PR was intentionally minimal just to demonstrate the idea. I'm happy to add more tests and update documentation if the fastapi team agrees on the approach, especially since this is a user visible feature.

@eltoder eltoder force-pushed the feature/pydantic-json-response branch 2 times, most recently from b339d32 to 964beb4 Compare November 9, 2025 00:23
@eltoder eltoder requested a review from YuriiMotov November 9, 2025 00:27
@eltoder
Copy link
Author

eltoder commented Nov 9, 2025

@YuriiMotov I addressed your comments. Please take another look.

@github-actions github-actions bot added the conflicts Automatically generated when a PR has a merge conflict label Dec 6, 2025
@github-actions

This comment was marked as resolved.

@YuriiMotov
Copy link
Member

I forgot to inform - we discussed this idea with Sebastian and he said he'd like to take a look at this later as he has his own thoughts regarding the implementation. So, let's wait.

@eltoder
Copy link
Author

eltoder commented Dec 9, 2025

@YuriiMotov sounds good. thanks for letting me know

When using this response class, json serialization is done using
Pydantic's built-in json serialization (`dump_json(r)`) instead of
generating an intermediate dict that is later serialized using a json
library (`json.dumps(dump_python(r))`).

In my testing this is 3-4x faster than using the standard json library
(the default) and 50% faster than using orjson, without requiring any
extra dependencies. This also allows configuring serialization behavior
per model using Pydantic's model_config.
@eltoder eltoder force-pushed the feature/pydantic-json-response branch from 964beb4 to 3e3a402 Compare December 14, 2025 17:29
@github-actions github-actions bot removed the conflicts Automatically generated when a PR has a merge conflict label Dec 14, 2025
@github-actions github-actions bot added the conflicts Automatically generated when a PR has a merge conflict label Dec 27, 2025
@github-actions

This comment was marked as resolved.

@github-actions github-actions bot removed the conflicts Automatically generated when a PR has a merge conflict label Jan 26, 2026
@codspeed-hq
Copy link

codspeed-hq bot commented Jan 26, 2026

Merging this PR will not alter performance

✅ 20 untouched benchmarks


Comparing eltoder:feature/pydantic-json-response (6706439) with master (8c32e91)

Open in CodSpeed

@eltoder
Copy link
Author

eltoder commented Jan 26, 2026

Hi @YuriiMotov! Do you have any ETA on when this is going to be reviewed?

@YuriiMotov
Copy link
Member

Hi @YuriiMotov! Do you have any ETA on when this is going to be reviewed?

Unfortunately no ETA

@github-actions
Copy link
Contributor

This pull request has a merge conflict that needs to be resolved.

@github-actions github-actions bot added the conflicts Automatically generated when a PR has a merge conflict label Feb 11, 2026
@YuriiMotov
Copy link
Member

Since FastAPI 0.130.0 if you specify response model and don't specify response class, FastAPI bypasses json_serializer and serialize response data on Pydantic's side.

See: #14962

So, this PR is not needed anymore and may be closed.

@eltoder, thanks for your efforts!

@YuriiMotov YuriiMotov closed this Feb 23, 2026
@eltoder
Copy link
Author

eltoder commented Feb 23, 2026

@YuriiMotov what if I need a custom content type?

@YuriiMotov
Copy link
Member

YuriiMotov commented Feb 23, 2026

@YuriiMotov what if I need a custom content type?

I see what you mean..

As I see it, the ability to bypass jsonable_encoder would be a solution here (something like disable_jsonable_encoder parameter. Or even disable_jsonable_encoder attribute of response class), right?
This would enable the ability to use your own response class and have full control over the serialization.

This would also solve use cases like #14929

@eltoder
Copy link
Author

eltoder commented Feb 23, 2026

I agree that this should be controlled by the response class. This is what I did in this PR. Instead of a boolean attribute this is usually done with a marker class like NoJsonableEncoderResponse which one can inherit into their response classes. This is similar to the current solution, but will be more ergonomic.

Back on the original subject. Note that automatically enabling pydantic json serialization like in #14962 is a backwards incompatible change. Pydantic's serialization behavior is different from the previous behavior.

@eltoder
Copy link
Author

eltoder commented Feb 23, 2026

@YuriiMotov it appears that #14962 was reverted. Please re-open this PR.

EDIT: I take it back. The revert commit a0a2d84 is not on any branch.

@YuriiMotov
Copy link
Member

@YuriiMotov it appears that #14962 was reverted. Please re-open this PR.

EDIT: I take it back. The revert commit a0a2d84 is not on any branch.

It was in this PR just to test performance impact one more time. That PR was closed

@YuriiMotov
Copy link
Member

Back on the original subject. Note that automatically enabling pydantic json serialization like in #14962 is a backwards incompatible change. Pydantic's serialization behavior is different from the previous behavior.

I think I agree it might be breaking, but could you please share a use case you have in mind?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

conflicts Automatically generated when a PR has a merge conflict feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants