-
-
Notifications
You must be signed in to change notification settings - Fork 840
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Custom JSON library #717
Comments
You'd need to do that explicitly. I think it'd look like this to encode the request... httpx.post(headers={'Content-Type': 'application/json'}, data=orjson.dumps(...)) ...and like this, to decode the response: orjson.loads(response.text) |
Alternatively, for a more automated solution, you could probably get away with a Here's an example — it uses a wrapper module to add verification-only print statements, but you can skip it and just use # spy_orjson.py
import orjson
def loads(text):
print("It works! Loading...")
return orjson.loads(text)
def dumps(text):
print("It works! Dumping...")
return orjson.dumps(text) # main.py
import sys
import spy_orjson
sys.modules["json"] = spy_orjson
import httpx
request = httpx.Request("GET", "https://example.org")
content = b'{"message": "Hello, world"}'
response = httpx.Response(
200, content=content, headers={"Content-Type": "application/json"}, request=request
)
print(response.json()) Output:
|
That's great! Thanks!! |
@florimondmanca such an ugly hack... Why not implement this feature? Looking at I'll try to implement this. |
I'd be okay with us providing an easy way to patch this in, if it follows something similar to how |
I'm looking at the code currently, don't see an easy way... Probably I'll create a
|
There were two PRs closed because they were stale, so I'm going to just reopen this one for us to have a conclusion. What about adding a new parameter to the client? Something like import httpx
import orjson
httpx.Client(json_lib=orjson) |
Maybe it'd be best to be able to specify dumps/loads separately both for user control and to avoid doing
|
It would be great to use the same names of Pydantic httpx.Client(json_loads=orjson_loads, json_dumps=orjson_dumps) |
If somebody else comes accross this, to be compatible with mypy and the have correct typing one has to use
|
3 years later... |
@xbeastx I think it's quite clearly articulated at #1352 (comment) why that pull request went stale. There is some additional helpful context at #1730 (comment) and discussion at #1740 |
I've not been sufficiently happy with any of the API proposal so far, and I've essentially been veto'ing them. Let me nudge something here that could be viable(?)... client = httpx.Client(request_class=..., response_class=...) I can explain why I (potentially) like that if needed. Perhaps the design sense will speak for itself. Edit 8th Sept 2023: That API would allow for this kind of customization... class APIClient(httpx.Client):
request_class = APIRequest
response_class = APIResponse
class APIRequest(httpx.Request):
def __init__(self, *args, **kwargs):
if 'json' in kwargs:
content = orjson.dumps(kwargs.pop('json'))
headers = kwargs.get('headers', {})
headers['Content-Length'] = len(content)
kwargs['content'] = content
kwargs['headers'] = headers
return super().__init__(*args, **kwargs)
class APIResponse(httpx.Response):
def json(self):
return orjson.loads(self.content) |
This comment was marked as outdated.
This comment was marked as outdated.
I'm hitting this issue as I type, and yikes, this is so complicated. 95% of use-cases would be solved if you could just do something like |
I do see that. The issue with that approach is that you introduce subtly different JSON handling at a distance. Installing a new dependancy to your project could end up altering the behaviour of an API client without that being visible anywhere obvious in the project codebase.
Do you have more than one client instance across the codebase? |
This is a very normal situation in microservice environment. This is a reason this issue exists. |
This comment suggests an API that I wouldn't object too. Once you've added that code you'd be able to use It's not exactly what some of y'all are requesting, but the critical sticking point here is this: I can't see myself doing anything other than veto'ing proposals that use a form of global state. |
I strongly agree that global state is not a good path forward for the library. I like the For those who want to configure the JSON library globally in your projects, it'd be trivial to subclass the |
Well yes, I'm doing
That's probably what I'll do, just wrap the client. It's not immediately obvious that this is what you should do, though. Maybe make it a recipe in the docs? At least until there is a settled solution. Overall, I'll say this is a classic case of pragmatism vs purity and I'm not sure a convenience function is where you want to spend cycles achieving purity. But that's not for me to say, and I appreciate your hard work and trust you'll make the best decision. Thank you. |
+1. We have |
My use case is sending pydantic models from my test to a webapp using the httpx client. I am currently doing a roundtrip conversion for each |
@chbndrhnns well, your case seems to be simple: https://docs.pydantic.dev/latest/concepts/serialization/#modelmodel_dump_json -- just overload this (or |
Ok, let's take this as a simplified example for my use case: import httpx
from pydantic import BaseModel
def test():
class Address(BaseModel):
zip: str
street: str
city: str
payload = {
"name": "me",
"address": Address(zip="0000", street="this street", city="my city")
}
_ = httpx.post("http://127.0.0.1:8000/", json=payload) It fails unless I call
|
It sounds like you don't own this web app, but normally you should define the payload in pydantic as well and call model_dump() on the root. If that sounds tedious to you, consider that strong typing is generally a compromise of being tedious in exchange for being correct |
having the ability to customize the response class will be great, any schedule on its implementation? :) |
One consideration that I haven't seen proposed - it's entirely reasonable for this library to check if the value of Lines 176 to 181 in 7354ed7
so it could look something like: def encode_json(json: Any) -> tuple[dict[str, str], ByteStream]:
body = json if isinstance(json, bytes) else json_dumps(json).encode("utf-8")
content_length = str(len(body))
content_type = "application/json"
headers = {"Content-Length": content_length, "Content-Type": content_type}
return headers, ByteStream(body) This would allow developers the ability to handle json encoding and decoding external to the library, so we could do something like this: import httpx
import orjson
mydata = {...}
with httpx.ClientSession() as client:
encoded = orjson.dumps(mydata)
response = client.post("https://fake.url/data/", json=encoded)
result = orjson.loads(response.content) I know for a fact aiohttp does something similar, so there is precedent here. (It also allows you to pass in a json encoder and decoder, but we've been down that road here.) If this seems like a reasonable change, I'm happy to make the requisite PR. |
Any news? Why not just borrow a solution from aiohttp?
|
🌟 Introducing HTTPJ! 🚀 It's like HTTPX, but with built-in support for flexible JSON serialization/deserialization!
import datetime
import pprint
import httpj
import orjson
resp = httpj.post(
"https://postman-echo.com/post",
json={"dt": datetime.datetime.utcnow()},
json_serialize=lambda j: orjson.dumps(j, option=orjson.OPT_NAIVE_UTC), # optional
json_deserialize=orjson.loads, # optional
)
pprint.pprint(resp.json(), indent=4) p.s.: I'm tired of waiting for this feature for more than 4 years... |
I added the following snippet to my client module to allow for passing in bytes as json. Not a huge fan of monkey-patching, but it get the job done. def _patch_httpx(): # type: ignore
"""Monkey-patch httpx so that we can use our own json ser/des.
https://github.com/encode/httpx/issues/717
"""
from httpx._content import Any, ByteStream, json_dumps
def encode_json(json: Any) -> tuple[dict[str, str], ByteStream]:
body = json if isinstance(json, bytes) else json_dumps(json).encode("utf-8")
content_length = str(len(body))
content_type = "application/json"
headers = {"Content-Length": content_length, "Content-Type": content_type}
return headers, ByteStream(body)
# This makes the above function look and act like the original.
encode_json.__globals__.update(httpx._content.__dict__)
encode_json.__module__ = httpx._content.__name__
httpx._content.encode_json = encode_json
_patch_httpx() |
So, here's an API proposal. Yep, I'll happily help someone get a pull request merged against that proposal. |
Does it mean that |
Please no. 😬 |
Custom request & response classes would be great but this trivial change change would also be useful. 👍 |
Hi!
Is there a way to use an alternative JSON library to decode the request? Like orjson for example?
Thanks!
The text was updated successfully, but these errors were encountered: