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

  1. Read the message: the part after 'Failed to parse json request:' gives the serde reason plus the JSON path of the bad field
  2. Fix that field's type/shape — quote IDs, supply required fields, remove trailing commas/comments
  3. Compare your payload against the endpoint's request struct in the neon source (the handler's json_request::<T> names T)
  4. 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

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

Related errors


AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16). Data as JSON: /api/errors/a597859090d46b0b. Report an issue: GitHub.