JuliusBrussee/caveman · error
body must be a JSON object
Error message
body must be a JSON object
What it means
parseOpenAIRoot unmarshals the request body into map[string]json.RawMessage. A syntactically valid JSON document that is not an object — `null`, an array, a string, a number — either fails Unmarshal or yields a nil map, which this guard turns into 'body must be a JSON object'. The OpenAI transform only operates on object-shaped payloads.
Source
Thrown at engine/pixel/transform_openai.go:690
Compress: opts.Compress,
CompressTools: opts.CompressTools,
MinCompressChars: opts.MinCompressChars,
Cols: min(opts.Cols, ResolveGptProfile(model).StripCols),
MultiCol: 1,
CharsPerToken: opts.CharsPerToken,
Reflow: opts.Reflow,
CollapseHistory: opts.CollapseHistory,
History: history,
}
}
func parseOpenAIRoot(body []byte) (map[string]json.RawMessage, error) {
var root map[string]json.RawMessage
if err := json.Unmarshal(body, &root); err != nil {
return nil, err
}
if root == nil {
return nil, errors.New("body must be a JSON object")
}
return root, nil
}
func openAIModel(root map[string]json.RawMessage, opts TransformOptions) string {
if raw, ok := root["model"]; ok {
var model string
if json.Unmarshal(raw, &model) == nil && model != "" {
return model
}
}
return opts.Model
}
func openAIChatContentText(content any) string {
if s, ok := content.(string); ok {
return s
}View on GitHub (pinned to 27d5a3981a)
Solutions
- Ensure the caller sends a JSON object body ({...}) for this transform
- Route array/batch payloads to the batch-capable path instead of the single-object transform
- Log the first bytes of the offending body at the proxy boundary to find which upstream is malformed
Example fix
// before
root, err := parseOpenAIRoot(body) // body = `[{"role":"user",...}]`
// after
trimmed := bytes.TrimSpace(body)
if len(trimmed) == 0 || trimmed[0] != '{' {
return errors.New("expected a JSON object body")
}
root, err := parseOpenAIRoot(body) Defensive patterns
Strategy: validation
Validate before calling
if len(body) == 0 || body[0] != '{' {
return errors.New("request body must be a JSON object")
}
root, err := parseOpenAIRoot(body) Type guard
func isJSONObject(b []byte) bool {
t := bytes.TrimSpace(b)
return len(t) > 0 && t[0] == '{'
} Try / catch
root, err := parseOpenAIRoot(body)
if err != nil {
if !isJSONObject(body) {
// reject/reroute the payload (e.g. batch endpoint)
}
return err
} Prevention
- Validate object shape at the proxy ingress before transforming
- Keep single-request and batch routes strictly separated
When it happens
Trigger: Feeding the transform a JSON array (e.g. a batch), a bare `null`, or a scalar where a chat-completions-style object is required; a proxy bug that forwards the wrong body; truncated bodies that happen to parse as non-objects.
Common situations: Batch APIs misrouted through the single-request transform; clients sending `null` for an absent payload; middleware replacing the body with a JSON-encoded string.
Related errors
- cacheengine: request root must be object
- empty render text
- message %d: %w
- cachebench: observation line %d: duplicate or invalid JSON
- verbs is not an array
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/4e1e4a13698335b9.
Report an issue: GitHub.