Unexpected end of JSON input — a complete, cause-by-language guide
Nearly every "Unexpected end of JSON input" is one of three things: you parsed an empty body, the response was truncated before it finished, or a bracket was never closed. This guide shows how that same root cause looks different in JavaScript, Python, PHP, Go, Java and curl — and gives you a repeatable way to find which one you hit.
The short version: this error is not about bad characters — every character the parser saw was valid JSON. It simply ran out of input before the value was finished. That is what separates it from Unexpected token, where the parser hit something it could not accept. Here the problem is absence: an empty string, a cut-off stream, or an opening { / [ that never got its partner.
Below is the error cause-by-cause, then language-by-language, then a five-minute localization routine you can run every time. When you have a fragment in hand, you can paste it into the interactive repair tool to see exactly where the structure is left open.
The three root causes (and how to tell them apart)
1. Empty or whitespace-only input
This is the most common one by a wide margin. JSON.parse(""), json.loads(""), json_decode("") — all of them reach end-of-input immediately because there is no value to read at all. The usual source is an HTTP response with no body: a 204 No Content, a 304 Not Modified, a redirect you followed, or a request that failed before the server wrote anything. The tell: log the length of the string you are about to parse. If it is 0 (or it is all spaces/newlines), you have found it, and no amount of bracket-counting will help.
2. Truncated payload
The JSON started correctly but was cut off partway. Causes include a dropped connection, a proxy or gateway timeout that returns a partial body, a response-size limit, gzip that was decoded incompletely, or — very often — reading a stream before it finished. The tell: the length is non-zero, the string starts with valid JSON, but the last 50 characters end mid-token (inside a string, or right after a comma). Always log text.slice(-50) / text[-50:]; the tail tells you instantly whether the end looks deliberate.
3. Unclosed bracket in data you built yourself
If you are assembling JSON by hand (string concatenation, a template, a logfile you are trying to parse), a single missing } or ] leaves the parser waiting forever. The tell: the string is complete and intentional, but counting opening vs closing brackets does not balance. Paste it into the tool above — it closes the open structures and shows you the completed shape, which points straight at the missing one.
What it looks like in each language
JavaScript — fetch and res.json()
The classic trap is calling res.json() on a response that has no body:
const res = await fetch("/api/save", { method: "POST" });
const data = await res.json(); // SyntaxError: Unexpected end of JSON input (204 body)
A 204 or an empty 200 both do this. Guard with the content first, and never parse a body you have not confirmed exists:
const res = await fetch("/api/save", { method: "POST" });
if (res.status === 204 || res.headers.get("content-length") === "0") return null;
const text = await res.text();
const data = text ? JSON.parse(text) : null;
A second JS-specific trap: calling res.json() (or res.text()) twice. The body stream can only be read once; the second call returns an empty string and throws this exact error. And JSON.parse(undefined) coerces to the string "undefined"... no, it actually throws here too — undefined stringifies to nothing meaningful for the parser. Always parse a variable you know is a non-empty string.
Python — requests and json.loads
In Python the error reads json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0) for the empty case, and an unterminated-structure message for a truncated one — same three causes underneath.
import requests, json
r = requests.post("https://api.example.com/save")
data = r.json() # raises if r.text is "" (204/empty)
Guard on the raw text, and prefer handling the empty case explicitly:
r = requests.post("https://api.example.com/save")
data = r.json() if r.content and r.status_code != 204 else None
If you stream with iter_content / iter_lines and parse before the generator is exhausted, you will parse a partial chunk — that is the truncated case. Accumulate the whole body first, or use a streaming JSON parser (e.g. ijson) designed for incremental input.
PHP — json_decode
json_decode("") returns null rather than throwing, which silently hides the problem until something downstream breaks. Make it loud:
$data = json_decode($body, true);
if (json_last_error() !== JSON_ERROR_NONE) {
// JSON_ERROR_SYNTAX on truncated input; empty $body -> null + JSON_ERROR_NONE in old PHP
throw new RuntimeException("Bad JSON: " . json_last_error_msg() . " len=" . strlen($body));
}
On PHP 7.3+ you can pass JSON_THROW_ON_ERROR to turn this into a real JsonException. Note that an empty string is a special case: check strlen($body) === 0 before decoding so an empty response is not confused with a literal null value.
Go — encoding/json
Go reports unexpected end of JSON input verbatim from json.Unmarshal when the byte slice is empty or ends mid-value:
body, _ := io.ReadAll(resp.Body)
if len(body) == 0 {
return nil // empty body — don't Unmarshal
}
var data map[string]any
if err := json.Unmarshal(body, &data); err != nil {
log.Printf("json error: %v (last 50: %q)", err, body[max(0,len(body)-50):])
}
Always check len(body) after io.ReadAll, and never ignore the error from the read itself — a read error is exactly how you end up with a truncated slice.
Java — Jackson / Gson
Jackson throws MismatchedInputException: No content to map due to end-of-input on an empty stream; Gson throws EOFException: End of input. Both are the empty/truncated cases. Buffer the response, check it is non-empty, and log its length and tail before mapping.
curl / bash pipelines
When you pipe curl into jq and see this, the usual cause is that curl followed a redirect or hit an error page with no JSON body, or the connection was reset. Add -sS so curl prints transport errors, -f so an HTTP 4xx/5xx becomes a non-zero exit instead of an error-page body, and inspect the raw bytes before piping:
curl -sSf "https://api.example.com/data" | tee /tmp/raw.json | jq .
wc -c /tmp/raw.json # 0 bytes => empty body, the real problem
A five-minute localization routine
Run these in order; stop at the first that explains it:
- Log the length. Zero (or all whitespace) ⇒ empty-body case. Go fix the request/status handling, not the JSON.
- Log the last 50 characters. If it ends mid-string or right after a comma ⇒ truncated. Look at timeouts, size limits, and whether you read the stream to completion.
- Count brackets. If length is fine and the tail looks intentional but
{≠}or[≠]⇒ unclosed structure in data you generated. Paste it into the repair tool to see where it stays open. - Check for a double read. In JS/Python, confirm you did not consume the body stream twice.
- Reproduce with the raw bytes. Save the exact string to a file and parse the file. If the file parses but your code does not, the bug is in how you obtain the string, not in the JSON.
How to stop it coming back
Three habits remove this error from a codebase for good: (1) never call a JSON parser on an unchecked string — guard for empty and for the right status code first; (2) treat 204/304/empty-body as a first-class, expected outcome that returns null, not an exception path; (3) when you build JSON yourself, serialize with your language's encoder instead of string concatenation, so brackets can never be unbalanced.
When you just need to see the damage on a concrete fragment, the Unexpected-end-of-JSON-input repair tool runs entirely in your browser, closes the open brackets, and shows the completed structure — no upload, no sign-up.
FAQ
What is the single most common cause of "Unexpected end of JSON input"?
Parsing an empty response body — a 204 No Content, a redirect, or a failed request that returned nothing. Log the length of the string before parsing; if it is zero, that is your cause.
How do I tell a truncated response from an unclosed bracket?
Log the last 50 characters. A truncated payload ends mid-token (inside a string or right after a comma) because transport cut it off. An unclosed bracket is intentional, complete data whose opening { or [ never got a matching } or ].
Why does fetch throw this when the request seemed to succeed?
res.json() on a 204 or empty 200 body throws it, and so does reading the body stream twice — the second read returns an empty string. Read res.text() once, check it is non-empty, then parse.
PHP's json_decode returns null instead of throwing — how do I catch it?
Check json_last_error() after decoding, or pass JSON_THROW_ON_ERROR (PHP 7.3+) to get a JsonException. Also test strlen($body) === 0 first so an empty body is not mistaken for a literal null value.