ool arguments with stringified JSON arrays/objects fail Zod validation #5379

Closed
opened 2026-02-16 17:52:00 -05:00 by yindo · 0 comments
Owner

Originally created by @kruxigt on GitHub (Jan 13, 2026).

Originally assigned to: @thdxr on GitHub.

Description

When AI models output tool arguments with nested arrays/objects as JSON strings instead of actual JSON structures, tool validation fails. This is a known behavior where some models double-encode nested JSON.

Steps to Reproduce

  1. Use the question tool (AskUserQuestion)
  2. Model outputs arguments like:

{
"questions": "[{"question": "What did you mean?", "header": "Clarify", "options": [...]}]"
}

  1. Validation fails because questions is a string, not an array

Expected Behavior

The tool should detect and parse stringified JSON before Zod validation.

Actual Behavior

Error: The question tool was called with invalid arguments: [
{
"expected": "array",
"code": "invalid_type",
"path": ["questions"],
"message": "Invalid input: expected array, received string"
}
]

The model retries repeatedly with the same format, causing a loop.

Proposed Fix

In packages/opencode/src/tool/tool.ts, add a helper to parse stringified JSON before validation:

function parseStringifiedJson(value: unknown): unknown {
if (typeof value === "string") {
const trimmed = value.trim()
if ((trimmed.startsWith("[") && trimmed.endsWith("]")) ||
(trimmed.startsWith("{") && trimmed.endsWith("}"))) {
try {
return parseStringifiedJson(JSON.parse(trimmed))
} catch {
return value
}
}
return value
}

if (Array.isArray(value)) {
  return value.map(parseStringifiedJson)
}

if (value !== null && typeof value === "object") {
  const result: Record<string, unknown> = {}
  for (const [key, val] of Object.entries(value)) {
    result[key] = parseStringifiedJson(val)
  }
  return result
}

return value

}

Then in Tool.define() around line 56:

toolInfo.execute = async (args, ctx) => {
const fixedArgs = parseStringifiedJson(args) // Add this
try {
toolInfo.parameters.parse(fixedArgs)
} catch (error) {
// ...
}
const result = await execute(fixedArgs, ctx)
// ...
}

Environment

  • OpenCode version: latest
  • Model: Claude (Anthropic API)
  • Affected tools: question (and potentially any tool with nested array/object params)

Additional Context

This is a common issue with LLM tool use where models sometimes serialize nested structures as strings rather than native JSON. Other frameworks handle this by detecting and parsing stringified JSON before schema validation.

Originally created by @kruxigt on GitHub (Jan 13, 2026). Originally assigned to: @thdxr on GitHub. Description When AI models output tool arguments with nested arrays/objects as JSON strings instead of actual JSON structures, tool validation fails. This is a known behavior where some models double-encode nested JSON. Steps to Reproduce 1. Use the question tool (AskUserQuestion) 2. Model outputs arguments like: { "questions": "[{\"question\": \"What did you mean?\", \"header\": \"Clarify\", \"options\": [...]}]" } 3. Validation fails because questions is a string, not an array Expected Behavior The tool should detect and parse stringified JSON before Zod validation. Actual Behavior Error: The question tool was called with invalid arguments: [ { "expected": "array", "code": "invalid_type", "path": ["questions"], "message": "Invalid input: expected array, received string" } ] The model retries repeatedly with the same format, causing a loop. Proposed Fix In packages/opencode/src/tool/tool.ts, add a helper to parse stringified JSON before validation: function parseStringifiedJson(value: unknown): unknown { if (typeof value === "string") { const trimmed = value.trim() if ((trimmed.startsWith("[") && trimmed.endsWith("]")) || (trimmed.startsWith("{") && trimmed.endsWith("}"))) { try { return parseStringifiedJson(JSON.parse(trimmed)) } catch { return value } } return value } if (Array.isArray(value)) { return value.map(parseStringifiedJson) } if (value !== null && typeof value === "object") { const result: Record<string, unknown> = {} for (const [key, val] of Object.entries(value)) { result[key] = parseStringifiedJson(val) } return result } return value } Then in Tool.define() around line 56: toolInfo.execute = async (args, ctx) => { const fixedArgs = parseStringifiedJson(args) // Add this try { toolInfo.parameters.parse(fixedArgs) } catch (error) { // ... } const result = await execute(fixedArgs, ctx) // ... } Environment - OpenCode version: latest - Model: Claude (Anthropic API) - Affected tools: question (and potentially any tool with nested array/object params) Additional Context This is a common issue with LLM tool use where models sometimes serialize nested structures as strings rather than native JSON. Other frameworks handle this by detecting and parsing stringified JSON before schema validation.
yindo added the bug label 2026-02-16 17:52:00 -05:00
yindo closed this issue 2026-02-16 17:52:00 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#5379