can1357/oh-my-pi · error · ToolError
${XD_URL_PREFIX}${device.name} content must be a JSON object
Error message
${XD_URL_PREFIX}${device.name} content must be a JSON object, got ${Array.isArray(parsed) ? "array" : typeof parsed}. What it means
After JSON parsing succeeds, parseDeviceArgs requires the parsed value to be a plain JSON object (null, arrays, and primitives are rejected) because device arguments are keyed by parameter name. It throws this ToolError stating what was received instead (array, null, string, number, etc.).
Source
Thrown at packages/coding-agent/src/tools/xdev.ts:164
* schema declares one. Throws ToolError; schema-mismatch errors carry `docs()`
* for repair.
*/
function parseDeviceArgs(
device: AiTool,
content: string,
toolCallId: string,
docs: () => string,
): Record<string, unknown> {
let parsed: unknown;
try {
parsed = JSON.parse(content);
} catch (error) {
throw new ToolError(
`${XD_URL_PREFIX}${device.name} expects a JSON args object as content (${error instanceof Error ? error.message : String(error)}). Write \`?\` for docs.`,
);
}
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new ToolError(
`${XD_URL_PREFIX}${device.name} content must be a JSON object, got ${Array.isArray(parsed) ? "array" : typeof parsed}.`,
);
}
// The harness only injects the intent field into top-level schemas; strip a
// habitual `i` from inner args unless the wrapped schema really declares it.
const args: Record<string, unknown> = { ...(parsed as Record<string, unknown>) };
if ("i" in args && !schemaDeclaresIntentField(toolWireSchema(device))) delete args.i;
try {
return validateToolArguments(device, {
type: "toolCall",
id: toolCallId,
name: device.name,
arguments: args,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new ToolError(`Invalid args for ${XD_URL_PREFIX}${device.name}: ${message}\n\n${docs()}`);
}View on GitHub (pinned to 9690622007)
Solutions
- Wrap the payload in a JSON object with named fields matching the device schema: {"items": [...]}.
- Write `?` as content to `xd://<device>` to see the expected schema.
- Use the device's docs to find the object field that should hold a list, instead of sending a top-level array.
Example fix
// before
write({ path: "xd://search", content: "[\"query\"]" })
// after
write({ path: "xd://search", content: "{\"query\":\"...\"}" }) Defensive patterns
Strategy: type-guard
Validate before calling
const parsed = JSON.parse(content); if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("xd:// args must be a JSON object"); Type guard
function isJsonObject(v: unknown): v is Record<string, unknown> { return typeof v === "object" && v !== null && !Array.isArray(v); } Try / catch
try { await write({ path, content }); } catch (e) { if (String(e.message).includes("must be a JSON object, got")) { const parsed = JSON.parse(content); await write({ path, content: JSON.stringify({ items: parsed }) }); } else throw e; } Prevention
- Wrap list payloads in an object field instead of sending top-level arrays.
- Never send bare scalars/strings as device args.
- Check the device schema via `?` content before composing args.
- Use JSON.stringify on a Record<string, unknown>, never on unknown shapes.
When it happens
Trigger: Writing `[]`, `[1,2]`, `"text"`, `42`, `true`, or `null` as content to `xd://<device>` — syntactically valid JSON but not an object.
Common situations: An agent sends a JSON array of items when the device schema expects named fields, or sends a bare JSON string/number it believed was already the payload.
Related errors
- ${XD_URL_PREFIX}${device.name} expects a JSON args object as
- Invalid args for ${XD_URL_PREFIX}${device.name}: ${message}\
- Missing or invalid field "${field}" in catalog: ${filePath}
- Marketplace catalog at ${filePath} must be a JSON object
- Invalid cmux socket response: expected object
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/23236c634eb814d4.
Report an issue: GitHub.