windmill-labs/windmill · error

Invalid JSON for ${field}: ${errorMessage}

Error message

Invalid JSON for ${field}: ${errorMessage}

What it means

parseOptionalJsonArg accepts a value that may already be parsed or a JSON string, and throws when JSON.parse of the string fails, embedding the underlying parser message. It is used by parsedModule and flowTools to coerce string tool-call arguments into objects.

Source

Thrown at frontend/src/lib/components/copilot/chat/flow/core.ts:250

)

const specialFlowModuleFields = {
	preprocessor_module: SPECIAL_MODULE_IDS.PREPROCESSOR,
	failure_module: SPECIAL_MODULE_IDS.FAILURE
} as const

type SpecialFlowModuleField = keyof typeof specialFlowModuleFields

function parseOptionalJsonArg(value: unknown, field: string): unknown {
	if (value === undefined || value === null) {
		return value
	}

	try {
		return typeof value === 'string' ? JSON.parse(value) : value
	} catch (e) {
		const errorMessage = e instanceof Error ? e.message : String(e)
		throw new Error(`Invalid JSON for ${field}: ${errorMessage}`)
	}
}

function validateSpecialFlowModule(
	module: unknown,
	field: SpecialFlowModuleField
): FlowModule | null | undefined {
	if (module === undefined || module === null) {
		return module
	}

	const result = flowModuleSchema.safeParse(module)
	if (!result.success) {
		const errors = result.error.issues.slice(0, 5).map((issue) => {
			const path = issue.path.length > 0 ? issue.path.join('.') : field
			return `${path}: ${issue.message}`
		})
		throw new Error(`Invalid ${field}:\n${errors.join('\n')}`)

View on GitHub (pinned to e474e8803c)

Solutions

  1. Validate/repair the JSON string before calling (JSON.parse in a try, or a lenient repair like jsonrepair).
  2. Have the model retry the tool call with the argument as a proper JSON object rather than a string.
  3. Check for truncation if the string looks cut off mid-structure and re-issue with fewer/larger chunks.

Example fix

// before
await flowTools.updateFlow({ flow: rawStringFromModel })
// after
let flowArg
try { flowArg = JSON.parse(rawStringFromModel) }
catch { flowArg = JSON.parse(jsonrepair(rawStringFromModel)) }
await flowTools.updateFlow({ flow: flowArg })
Defensive patterns

Strategy: validation

Validate before calling

let parsed: unknown
if (typeof value === 'string') {
  try { parsed = JSON.parse(value) } catch { parsed = JSON.parse(jsonrepair(value)) }
} else parsed = value

Type guard

function isJsonObject(v: unknown): v is Record<string, unknown> {
  return typeof v === 'object' && v !== null && !Array.isArray(v)
}

Try / catch

try {
  await tool(args)
} catch (e) {
  if (String(e.message).startsWith('Invalid JSON for')) {
    return requestModelRetryWithJsonInstruction(e.message)
  }
  throw e
}

Prevention

When it happens

Trigger: A tool argument for `field` arrives as a string that is not valid JSON — typically an LLM-produced tool call with truncated, single-quoted, or commented JSON — and typeof value === 'string'.

Common situations: Model emits JSON with trailing commas or unquoted keys; argument was truncated by a token limit; caller double-encoded (passed a JSON string of a JSON string) or passed plain text where JSON was expected.

Understand the failure class

Related errors


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/caf8ed9a14c00b8b. Report an issue: GitHub.