httpx post json vs data vs content (tested)

Use httpx.post(url, json={...}) for a JSON body. data= sends a form; a JSON string in data= goes out with no Content-Type, so use content= plus a header. If you pass both json= and data=, the JSON is dropped.

In httpx, json= sends a JSON body, data= sends an HTML form, and content= sends raw text or bytes. Measured with httpx 0.28.1 on Python 3.13.5 against a local http.server (srv.py) that echoes back the Content-Type and body it received.

1. httpx post json

import httpx
from srv import start
base = start()

r = httpx.post(base + "/api", json={"name": "Ada", "tags": ["x"], "ok": True})
e = r.json()
print(e["content-type"])
print(e["body"])

Output:

application/json
{"name":"Ada","tags":["x"],"ok":true}

json= serializes the object and sets Content-Type: application/json for you. Do not also call json.dumps() yourself, or the server receives a JSON string instead of an object.

2. data= sends a form, not JSON

import httpx
from srv import start
base = start()

r = httpx.post(base + "/api", data={"name": "Ada", "tags": ["x", "y"]})
e = r.json()
print(e["content-type"])
print(e["body"])

Output:

application/x-www-form-urlencoded
name=Ada&tags=x&tags=y

A dict in data= is form-encoded (application/x-www-form-urlencoded), and a list repeats the key. An API that expects JSON usually answers this with 400, 415 or 422.

3. httpx post json string: use content=, not data=

import json, httpx
from srv import start
base = start()

payload = json.dumps({"name": "Ada"})

# a JSON string passed to data= : httpx 0.28 sends it without a Content-Type
r = httpx.post(base + "/api", data=payload)
print("data=str:   ", r.json()["content-type"], r.json()["body"])

# raw text/bytes go in content=, and you set the header yourself
r = httpx.post(base + "/api", content=payload, headers={"Content-Type": "application/json"})
print("content=str:", r.json()["content-type"], r.json()["body"])

Output:

data=str:    None {"name": "Ada"}
content=str: application/json {"name": "Ada"}

A pre-serialized JSON string in data= is sent with no Content-Type at all, so many servers will not parse it as JSON. httpx 0.28.1 also raises a DeprecationWarning ("Use 'content=<...>' to upload raw bytes/text content"), but it is hidden by default; run with -W error::DeprecationWarning to see it. Use content= and set the header yourself.

4. JSON body format differs from requests

import httpx
from srv import start
base = start()

# httpx: compact separators, non-ASCII sent as UTF-8
r = httpx.post(base + "/api", json={"city": "Zürich", "n": 1})
print("httpx:   ", r.json()["body"])

import requests
r = requests.post(base + "/api", json={"city": "Zürich", "n": 1})
print("requests:", r.json()["body"])

from datetime import date
try:
    httpx.post(base + "/api", json={"d": date(2026, 10, 23)})
except TypeError as e:
    print("TypeError:", e)

Output:

httpx:    {"city":"Zürich","n":1}
requests: {"city": "Z\u00fcrich", "n": 1}
TypeError: Object of type date is not JSON serializable

httpx 0.28 sends compact JSON with non-ASCII characters as UTF-8; requests adds spaces and escapes them as \u00fc. Both are valid JSON, but it matters if you sign the body (HMAC webhooks) or compare it byte for byte. Types the json module cannot encode, such as date, raise TypeError before anything is sent; convert them first (for example d.isoformat()).

5. Gotcha: json= and data= together

import httpx
from srv import start
base = start()

r = httpx.post(base + "/api", json={"a": 1}, data={"b": 2})
e = r.json()
print(e["content-type"], e["body"])

Output:

application/x-www-form-urlencoded b=2

When both are passed, httpx 0.28.1 sends the form and silently drops the JSON. Pick one.

6. AsyncClient post json

import asyncio, httpx
from srv import start
base = start()

async def main():
    async with httpx.AsyncClient(base_url=base) as client:
        r = await client.post("/api", json={"name": "Ada"})
        r.raise_for_status()
        print(r.status_code, r.json()["body"])

asyncio.run(main())

Output:

200 {"name":"Ada"}

Related: httpx params, httpx vs requests differences, httpx timeout, FastAPI 422 Unprocessable Entity.

Frequently asked questions

How do I post JSON with httpx?

httpx.post(url, json=obj). httpx serializes it and sets Content-Type: application/json.

What is the difference between json= and data= in httpx?

json= sends a JSON body; data= with a dict sends a form (application/x-www-form-urlencoded).

How do I send a JSON string with httpx?

Use content=json_string with headers={'Content-Type': 'application/json'}. data=str is deprecated and sends no Content-Type.

Is the httpx JSON body the same as requests?

No. httpx 0.28 sends compact JSON with UTF-8 characters; requests adds spaces and escapes non-ASCII, which matters for signed bodies.

Written with AI assistance. Measured on 2026-10-23 with httpx 0.28.1 (and requests 2.32 for comparison) on Python 3.13.5 against a local http.server that echoes the request body. Each block was run verbatim and the output pasted unedited. PlainHeaders is our extension; httpx is free.