mcpField
J
jo

jo

github.com/petr-korobeinikovDevelopment167 installsUpdated 2w ago
Repository

About jo

A shell-focused skill for constructing JSON with `jo` instead of manually quoted JSON strings. It covers value typing, nested structures, arrays, file input, and common command-line patterns, with guidance to verify non-trivial syntax against the manual.

Key features

  1. 01Build JSON from shell values
  2. 02Infer or coerce JSON value types
  3. 03Compose nested objects and arrays
  4. 04Read and embed file contents
  5. 05Update existing JSON documents
  6. 06Examples for curl and kubectl

Use cases

  1. 01Create API request bodies with shell variables
  2. 02Generate nested JSON in scripts
  3. 03Patch Kubernetes resources from the command line

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/petr-korobeinikov/skills --skill jo

After installing, type/join Claude Code to confirm it loaded.

Bundled filesreferences/options.md

Details

From the project README

jo

Build JSON in shell snippets with jo, not by hand-typing a quoted JSON literal. jo handles JSON quoting internally, so shell variables interpolate cleanly and you never juggle nested single/double quotes.

Authoritative documentation: https://github.com/jpmens/jo (README + jo.md man page). Verify any non-trivial form against the manual before relying on it.

Why

  • Hand-written JSON in a shell string is a quote-escaping minefield. Inserting $VAR into '{"user":"x"}' either breaks the single quotes or forces an awkward '"$VAR"' pattern that is hard to read and easy to mistype.
  • Typos in inline JSON (missing comma, stray quote, trailing comma) fail at the server, not at parse time — hard to spot in a long single-line payload.
  • jo figures out types from the value: numbers, booleans, null, nested objects, and arrays are detected automatically; override with -s/-n/-b per word when the guess would be wrong (e.g. a numeric ID that must stay a string).
  • jo composes: shell variables, command substitution, bracket-path shorthands, and nested jo calls all work without re-escaping.

How jo types a value

Per the man page, the value pipeline is:

  1. Try to parse the value as JSON. 5 → number, true/false → boolean, null → null, {...} / [...] → embedded object/array, strings that look numeric but aren't valid floats (e.g. 3.14159.26) → string.
  2. If JSON parse fails, check special value prefixes: @file, %file, :file (see Reading from files below).
  3. Otherwise it's a literal string.

A missing/empty value (k=) becomes null. Pass -B to disable detection of the literal strings true/false/null. Globally, -n drops keys with empty values (not to be confused with the per-word -n coercion after --).

Direct substitutions

Don'tDo
'{"name":"alice"}'"$(jo name=alice)"
'{"name":"'"$USER"'"}'"$(jo name="$USER")"
'{"count":5,"active":true}'"$(jo count=5 active=true)"
'{"value":null}'"$(jo value=null)" (or "$(jo value=)")
'{"tags":["a","b","c"]}'"$(jo tags="$(jo -a a b c)")"
'{"point":[1,2]}'"$(jo 'point[]=1' 'point[]=2')"
'{"geo":{"lat":10,"lon":20}}'"$(jo 'geo[lat]=10' 'geo[lon]=20')"
'{"user":{"id":1,"name":"alice"}}'"$(jo user="$(jo id=1 name=alice)")"
'{"code":"12345"}' (string ID)"$(jo -- -s code=12345)"

Bracket-path forms (key[]=, key[sub]=) are one level deep only — 'a[b][c]=1' silently drops [c] and produces {"a":{"b":1}}. For deeper nesting, use nested jo via command substitution.

Syntax cheatsheet

Value forms
  • k=v — JSON-parsed then prefix-checked then literal string (see How jo types a value).
  • k@v — boolean from a truthy value (T/t or non-zero numeric → true, else false); handy for flag@$ENABLED.
  • k=@file — load file contents as a string value.
  • k=%file — load file as base64-encoded string.
  • k=:file — parse file as JSON and embed.
  • k:=file or k:=- — same as k=:file but as an operator; - reads JSON from stdin.

To use a literal @, %, or : at the start of a string value, escape it with \: twitter='\@jpmens', vimcmd='\:split', uri='\\%20'.

Bracket paths (one level)
  • 'name[]=v1' 'name[]=v2' — appends to the name array.
  • 'obj[k]=v' — creates {"obj":{"k":v}}.
  • Combine -d. (path separator) for dot notation: jo -d. geo.lat=10 geo.lon=20 is equivalent to jo 'geo[lat]=10' 'geo[lon]=20'.
  • Always quote bracket forms — unquoted [ / ] get caught by zsh globbing (and bash too, if any expansion matches).
Arrays
  • jo -a v1 v2 v3 — array of auto-typed values.
  • jo -a -- -s 123 hello -n "five" — per-word coercion inside an array (here: ["123","hello",4] — -n on a non-numeric string yields its length).
Type coercion
  • jo -- -s k=v — force string. Use for numeric IDs, version strings, phone numbers, ZIPs.
  • jo -- -n k=v — force number (non-numeric strings become their length).
  • jo -- -b k=v — force boolean (empty → false, anything else → true).
  • -- is mandatory before the word list whenever the first word starts with -; pass it always for consistency with the manual.
Global options

The two most-used:

  • -p — pretty-print (drop for production payloads).
  • -a — emit an array instead of an object.

For the full option list (-B, -D, -n global, -d sep, -f file, -e, -o, -v/-V, plus per-word coercion semantics after --) see references/options.md.

Idioms

curl POST with a body:

curl -X POST https://api.example.com/v1/users \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d "$(jo name="$NAME" email="$EMAIL" active=true)"

Bracket shorthand for one-level nesting (no nested jo substitution needed):

curl -X POST https://api.example.com/v1/orders \
    -H "Authorization: Bearer $TOKEN" \
    -d "$(jo \
        'customer[id]='"$CUSTOMER_ID" \
        'customer[name]='"$CUSTOMER_NAME" \
        'items[]='"$SKU_A" \
        'items[]='"$SKU_B")"

Nested deeper than one level — build via command substitution (or stash in a variable):

payload=$(jo \
    name="$NAME" \
    address="$(jo street="$STREET" city="$CITY")" \
    tags="$(jo -a admin user)")

curl -X POST -d "$payload" https://api.example.com/v1/users

kubectl patch:

kubectl patch deployment web \
    --type=merge \
    --patch "$(jo spec="$(jo replicas=3)")"

Numeric ID that must stay a string (otherwise jo emits "id":12345 as a number):

curl -X POST -d "$(jo -- -s id=12345 name="$NAME")" \
    https://api.example.com/v1/users

Modify an existing JSON document:

curl -sSL https://api.example.com/v1/config \
    | jo -f - updated_at="$(date -Iseconds)" version="$NEW_VERSION"

When NOT to use jo

Large, fixed JSON document

A heredoc with literal JSON is clearer than a long chain of jo calls, and the JSON can be copy-pasted from API docs without translation:

curl -X POST -d @- https://api.example.com/v1/config <<'JSON'
{
  "feature_flags": {
    "alpha": true,
    "beta": false
  },
  "limits": {
    "rps": 100,
    "burst": 200
  }
}
JSON
jo not available on the host

Some minimal CI images or locked-down servers lack it. Fall back to a heredoc; do not fall back to fragile inline quoted JSON.

Installing jo

  • macOS: brew install jo or sudo port install jo
  • Debian/Ubuntu: apt install jo
  • Fedora: dnf install jo
  • Alpine: apk add jo
  • Gentoo: emerge jo
  • Arch: pacman -S jo (in extra)
  • Snap: snap install jo
  • Windows: scoop install jo
  • Docker: docker run --rm jpmens/jo ...
  • From source: https://github.com/jpmens/jo

jo is not in the default aqua/mise short-name registry; on a mise-managed host install via the package manager or via ubi:jpmens/jo.

Scope

Applies anywhere a shell snippet emits JSON:

  • Bash tool invocations.
  • Snippets in SKILL.md / README.md / docs.
  • Scripts under scripts/.
  • Makefile recipes.
  • CI configs.
  • API smoke-test instructions.

If you encounter a hand-written inline JSON string while editing a file, replace it with the equivalent jo form as part of the same edit.

Comments

Markdown supported