Watch at EaseStories. Answers. Perspective.

Technology · PRACTICAL GUIDE

jq says ‘cannot index string with string’? Prove the JSON shape before changing the filter

Capture jq's input mode and the type at every path, then guard only the proven mixed-shape boundary while preserving rejected records.

Original diagram tracing raw JSON, jq input mode, path traversal and object-key access.
Original illustration: prove the value type at every path before indexing. Watch at Ease

A jq key lookup fails when that step receives a string, array, number or boolean instead of an object. The dangerous part is that a generator can print an earlier valid value and then fail on a later value, leaving partial output beside a nonzero exit status.

Preserve the raw bytes, jq input mode and exact filter. Build a path-and-type ledger, then either fail on schema drift or guard only a legitimately mixed boundary while keeping every rejected record visible.

Freeze the bytes, jq mode and exact filter

Save a representative input file before piping it through another formatter. Record jq --version, the complete filter, shell quoting, upstream command, and whether -R, -s or -n is present. The jq manual says -R passes each input line as a string, while -s collects the full input stream into one array; either option can deliberately change the value a later path receives.

Validate that the preserved file is JSON, then keep the failing record and one working record side by side. A successful line printed before exit status 5 does not make the run successful: generators can emit an earlier value and fail on a later value of another type.

Build a path-and-type ledger

Run type at the root and after each traversal step. jq names six types: null, boolean, number, string, array and object. RFC 8259 independently defines objects as name/value collections and arrays as ordered sequences whose elements do not have to share one type.

Record path, observed type and count instead of describing the payload as merely nested. If .[] expands an object, it yields every member value; a later .name will then be attempted against objects, strings, arrays or booleans that came from different members.

Distinguish the three common shape failures

A wrong root traversal indexes an array as though it were an object or expands an object too early. A mixed collection contains both the expected object and scalar exceptions. A stringified object is a string whose characters happen to look like JSON; it requires a provenance decision before any parsing step.

Do not use shell echo output as the authoritative shape when quoting or escape processing may have changed bytes. Feed the preserved file directly to jq and compare its type ledger with the producer's documented response schema.

Guard only the proven boundary

When a collection legitimately mixes types, select values whose type is object before reading an object key, and send the rest to a reject ledger with their path and type. When every record should be an object, fail on the first scalar instead of silently dropping it.

A broad try, catch or optional lookup can remove the visible error by producing empty output. Use those operators only when missing or non-object data is an accepted contract and the rejected count is still measured. Otherwise they convert schema drift into silent data loss.

Require output, reject and exit proofs

Test an object, array, string, boolean, null, a missing key and a mixed array. Compare the accepted-record count plus rejected-record count with the input cardinality, and assert the jq exit status in the same shell that runs production.

The exit condition is not merely no red text. It is a documented input mode, stable path types, an expected output count, zero unexplained rejects and a nonzero failure when a required object becomes a scalar. Keep the failing fixture so future producer changes cannot reintroduce the same silent loss.

Original diagram showing classification, narrow object selection, field extraction and output reconciliation.
Original illustration: guard the proven boundary and keep unexpected values visible. Watch at Ease