neondatabase/neon · warning · ApiError
Failed to parse json request: {e}
Error message
Failed to parse json request: {e} What it means
Returned as HTTP 400 BadRequest by json_request in neon's http-utils when the non-empty request body fails JSON deserialization into the endpoint's typed request struct. The error is produced by serde_path_to_error, so the message includes both the serde error and the JSON path of the offending field, and it is intentionally stringified (Display) so python-side logs stay readable. It fires for syntax errors, type mismatches, and missing required fields alike.
Source
Thrown at libs/http-utils/src/json.rs:27
pub async fn json_request<T: for<'de> Deserialize<'de>>(
request: &mut Request<Body>,
) -> Result<T, ApiError> {
let body = hyper::body::aggregate(request.body_mut())
.await
.context("Failed to read request body")
.map_err(ApiError::BadRequest)?;
if body.remaining() == 0 {
return Err(ApiError::BadRequest(anyhow::anyhow!(
"missing request body"
)));
}
let mut deser = serde_json::de::Deserializer::from_reader(body.reader());
serde_path_to_error::deserialize(&mut deser)
// intentionally stringify because the debug version is not helpful in python logs
.map_err(|e| anyhow::anyhow!("Failed to parse json request: {e}"))
.map_err(ApiError::BadRequest)
}
/// Parse a json request body and deserialize it to the type `T`. If the body is empty, return `T::default`.
pub async fn json_request_maybe<T: for<'de> Deserialize<'de> + Default>(
request: &mut Request<Body>,
) -> Result<T, ApiError> {
let body = hyper::body::aggregate(request.body_mut())
.await
.context("Failed to read request body")
.map_err(ApiError::BadRequest)?;
if body.remaining() == 0 {
return Ok(T::default());
}
let mut deser = serde_json::de::Deserializer::from_reader(body.reader());
View on GitHub (pinned to 8f60b04da4)
Solutions
- Read the message: the part after 'Failed to parse json request:' gives the serde reason plus the JSON path of the bad field
- Fix that field's type/shape — quote IDs, supply required fields, remove trailing commas/comments
- Compare your payload against the endpoint's request struct in the neon source (the handler's json_request::<T> names T)
- If the API version changed, update the client to the current schema
Example fix
# before
curl -X POST .../failpoints -d '{ name: "fp1", actions: "return" }' # unquoted keys -> 400
# after
curl -X POST .../failpoints -H 'Content-Type: application/json' \
-d '[{"name":"fp1","actions":"return"}]' Defensive patterns
Strategy: validation
Validate before calling
// Validate the payload against the endpoint's schema client-side first:
const req = { tenant_id: tenantId }; // must be string UUID if the API type is TenantId
if (!/^[0-9a-fA-F-]{36}$/.test(req.tenant_id)) throw new Error("tenant_id must be a UUID string");
await fetch(url, { method: "POST", body: JSON.stringify(req) }); Type guard
function isTimelineCreateRequest(b) {
return typeof b === "object" && b !== null &&
(b.new_timeline_id === undefined || /^[0-9a-f-]{36}$/.test(b.new_timeline_id));
} Try / catch
// Parse the 400 body to surface the serde path to the operator:
const resp = await fetch(url, req);
if (resp.status === 400) {
const msg = (await resp.json()).error ?? await resp.text();
throw new Error(`request rejected: ${msg}`); // message contains the offending JSON path
} Prevention
- Stringify IDs explicitly; never let numbers become JSON numbers for UUID-typed fields
- Run payloads through a JSON schema or typed client generated from the API structs
- The error message names the exact JSON path — read it before changing anything else
When it happens
Trigger: POSTing {"tenant_id": 123} where the handler's struct wants TenantId (a stringified UUID), sending invalid JSON like a trailing comma, or omitting a required field — serde_path_to_error names the path (e.g. .tenant_id) in the message.
Common situations: Sending UUIDs without quotes (JSON number instead of string); null for a non-Option field; snake_case/camelCase mixups after a client library change; hand-written JSON with comments or trailing commas; version skew where the API added a required field.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- missing request body
- failed to parse {param_name}
- could not parse config file: {}
- invalid format {format}
- duration must be 1-60 secs
AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16).
Data as JSON: /api/errors/a597859090d46b0b.
Report an issue: GitHub.