Beginner 5 min HTTP 400

400 Bad Request — Malformed JSON Body

Symptoms

- API calls return 400 Bad Request with a body like `{"error":"JSON parse error — Expecting property name enclosed in double quotes"}`
- The request was working previously but broke after a recent code change that modified how the request body is built
- Copying the request body into a JSON validator reveals syntax errors such as trailing commas, single-quoted strings, or unescaped special characters
- The same endpoint works fine when called from Postman or curl with a manually crafted body, but fails from the application code
- Server logs show the request arrived but was rejected at the parsing stage before any application logic ran

Root Causes

  • Trailing comma after the last property or array element — valid in JavaScript object literals and Python dicts but illegal in JSON
  • Content-Type header missing or set to `application/x-www-form-urlencoded` instead of `application/json`, causing the server to misparse the body
  • String encoding issue — non-UTF-8 byte sequences in the body (e.g., latin-1 characters) that break JSON parsers expecting UTF-8
  • Template string or f-string interpolation producing invalid JSON — user-supplied strings with unescaped double quotes break the JSON structure
  • Empty request body sent when the endpoint requires a JSON object, or a `null` / `undefined` value serialized as the literal string `undefined`

Diagnosis

**Step 1 — Capture the exact request body with curl verbose output**

```bash
curl -v -X POST https://api.example.com/endpoint \
-H 'Content-Type: application/json' \
-d '{"key": "value"}'
# Look for: < HTTP/1.1 400 Bad Request
# And the response body with the parse error details
```

**Step 2 — Validate the JSON in isolation**

```bash
# Pipe the body through Python's JSON parser to get the exact error location
echo '{"key": "value",}' | python3 -m json.tool
# → json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes

# Or use jq:
echo '{"key": "value",}' | jq .
# → parse error (Expected another key-value pair or end of object)
```

**Step 3 — Verify the Content-Type header**

```bash
curl -v -X POST https://api.example.com/endpoint \
-d '{"key": "value"}'
# Without -H 'Content-Type: application/json', curl sends:
# Content-Type: application/x-www-form-urlencoded
# → Server tries to parse URL-encoded data as JSON → 400
```

**Step 4 — Check the response body for the specific parse error**

```bash
curl -s -X POST https://api.example.com/endpoint \
-H 'Content-Type: application/json' \
-d 'INVALID' | python3 -m json.tool
# Django REST Framework returns:
# {"detail": "JSON parse error - Expecting value: line 1 column 1 (char 0)"}
# FastAPI returns:
# {"detail": [{"loc": ["body"], "msg": "JSON decode error", ...}]}
```

**Step 5 — Inspect how the body is built in code**

```python
import json
data = {'key': 'value with "quotes"', 'trailing': 'comma',}
# Test: will json.dumps() raise?
print(json.dumps(data)) # Safe — Python handles escaping
# Danger: manual string building
body = f'{{"key": "{user_input}"}}'
# If user_input contains '"' → invalid JSON!
```

Resolution

**Fix 1: Always use a JSON serializer — never build JSON strings manually**

```python
import json
import httpx

# WRONG — fragile and injection-prone:
body = f'{{"name": "{name}", "age": {age}}}'

# RIGHT — serializer handles escaping automatically:
resp = httpx.post(
'https://api.example.com/endpoint',
json={'name': name, 'age': age}, # httpx sets Content-Type automatically
)
```

**Fix 2: Set Content-Type: application/json explicitly when using raw body**

```python
import httpx, json

resp = httpx.post(
'https://api.example.com/endpoint',
content=json.dumps(data),
headers={'Content-Type': 'application/json'},
)
```

**Fix 3: Handle encoding — ensure strings are UTF-8 before serializing**

```python
# If reading from files or external sources:
text = raw_bytes.decode('utf-8', errors='replace') # or 'ignore'
body = json.dumps({'message': text})
```

**Fix 4: Validate outbound JSON in tests**

```python
import json

def build_payload(data: dict) -> str:
payload = json.dumps(data)
# Verify it round-trips correctly
assert json.loads(payload) == data
return payload
```

Prevention

- **Never concatenate strings to build JSON** — always use `json.dumps()`, `JSON.stringify()`, or a serializer that handles escaping, encoding, and special characters automatically
- **Add a JSON schema validation step** in CI/CD for config files and API fixture data — `jsonschema` in Python or `ajv` in Node.js catch structural errors before they reach production
- **Test with boundary inputs** including empty strings, strings with quotes, null values, and non-ASCII characters to ensure your serialization handles edge cases correctly
- **Log the raw request body on 400 responses** on the server side to capture exactly what arrived, making debugging much faster in production

Related Status Codes

Related Terms