Why Go says invalid character ‘<’ looking for beginning of value
json.Unmarshal and json.Decoder return invalid character '<' looking for beginning of value when a value should start and the byte there is not {, [, ", a digit, -, t, f or n. The quoted character tells you what you received:
'<': HTML or XML. An error or login page, a proxy’s 502 page, or an XML error such as the expired presigned-URL response from S3 in the checker above.'N'or'I':NaNorInfinity, which Python’sjson.dumpswrites by default, or a plain-textNot FoundorInternal Server Error.'F','U'and other capitals: a plain-text error that starts with its status text, the wayhttp.Error(w, http.StatusText(code), code)writesForbiddenorUnauthorized.'ï'or'\ufeff': a UTF-8 byte order mark at the start of the body.'\''followed bylooking for beginning of object key string: single quotes, often a Python dict written withstr().
Two near misses have their own text. An empty body gives unexpected end of JSON input from Unmarshal and io.EOF from a Decoder. Go’s default 404 body, 404 page not found, gives invalid character 'p' after top-level value, because 404 parses as a number first.
How to fix it
Check the response before decoding
func fetchReport(url string) (*Report, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return nil, err
}
mediaType, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if resp.StatusCode != http.StatusOK || mediaType != "application/json" {
return nil, fmt.Errorf("GET %s: %s, %s: %.200s", url, resp.Status, mediaType, body)
}
body = bytes.TrimPrefix(body, []byte("\xef\xbb\xbf")) // UTF-8 BOM
var r Report
if err := json.Unmarshal(body, &r); err != nil {
return nil, fmt.Errorf("decode %s: %w", url, err)
}
return &r, nil
}
For the sample above, the error now reads 403 Forbidden, application/xml: <?xml version=... instead of a bare syntax error. json.NewDecoder(resp.Body).Decode(&r) works too, but it consumes the body, so keep the status and type checks in front of it.
Find the bad byte
*json.SyntaxError carries Offset, the number of bytes read up to and including the bad one:
var se *json.SyntaxError
if errors.As(err, &se) {
start := max(0, int(se.Offset)-30)
end := min(len(body), int(se.Offset)+30)
log.Printf("byte %d: %q", se.Offset, body[start:end])
}
Or paste the body into the JSON validator, which shows the line and column.
Fix the sender
NaN and Infinity are not JSON. In Python, replace them with None before json.dumps, or pass allow_nan=False so the sender fails instead of the receiver.
Go tools
Terraform, Docker, kubectl and Grafana are written in Go, so this message also shows up in their output when a configured endpoint answers with HTML. Check the URL, the proxy settings and the credentials the tool uses.
Same error in other languages
- Python: JSONDecodeError: Expecting value: line 1 column 1 (char 0).
- Chrome and Node.js: Unexpected token ‘<’, “<!DOCTYPE “… is not valid JSON.
- Safari: JSON Parse error: Unrecognized token ‘<’.
- Empty input: Go’s
unexpected end of JSON inputmatches V8’s Unexpected end of JSON input.
The full list by language: common JSON errors.