boop

Object-oriented programming for bash 4.3+ — real classes, objects, inheritance, and a standard library, in pure bash.


Project maintained by ydbxmhc Hosted on GitHub Pages

Data.JSON – JSON Parser and Serializer

Pure bash JSON parser and serializer. No external dependencies (no jq, no python). Parses JSON into a Collection.Map.Fast flat store for O(1) point lookups via compound keys.

NUL bytes. Bash variables cannot hold or detect NUL bytes. Any JSON string value containing a NUL will be silently truncated at the first one. See GOTCHAS.md.

Contents


Quick Start

. boop Data::JSON

# Parse
into=doc Data.JSON.parse '{"name":"Alice","scores":[10,20,30]}'
into=name $doc.get "name"         # "Alice"
into=s1 $doc.get "scores.1"      # "20"

# Stringify
into=json Data.JSON.stringify "$doc"
printf "%s\n" "$json"
# {"name":"Alice","scores":[10,20,30]}

API

Data.JSON.parse jsonString

Parses a JSON string into a Collection.Map.Fast object. Returns the object ID via boop.pass.

Compound keys use . as separator:

  • Object keys: "user.name", "user.address.city"
  • Array indices: "items.0", "items.1"
  • Mixed: "users.0.name", "matrix.1.2"

All values are stored as strings. Type information is preserved by convention:

  • Numbers: stored as-is ("42", "-3.14")
  • Booleans: "true" / "false"
  • Null: empty string ""

Data.JSON.stringify docObject

Serializes a Collection.Map.Fast back to a JSON string. Infers structure from compound keys:

  • All-numeric children = JSON array
  • String children = JSON object

Type reconstruction:

  • Values matching ^-?[0-9]+\.?[0-9]*([eE][+-]?[0-9]+)?$ = unquoted number
  • "true" / "false" = unquoted boolean
  • Empty string = null
  • Everything else = quoted string

Key order: documents created via Data.JSON.parse preserve insertion (parse) order. Documents built by hand with $doc.set use hash-iteration order (undefined but consistent per bash version).

Diffing tip: stringify produces compact output with no whitespace. When diffing JSON output, use your diff tool’s whitespace-ignore flag (diff -w, git diff -w, git diff --ignore-all-space) or pretty-print both sides with a formatter first — otherwise a key-order change in an outer object can make the entire value appear changed even when content is identical.

Data.JSON.validate jsonString

Checks whether a string is valid JSON. Returns 0 if valid, 1 if not. No subshell needed — the parser runs in-process with errors routed to a return code instead of _Crash.

When invalid, the error message is passed via into= (or stdout):

# Simple check
Data.JSON.validate '{"ok":true}'    # exit 0
Data.JSON.validate 'not json'       # exit 1

# Capture the error message
into=err Data.JSON.validate '{"x":}'
printf '%s\n' "$err"   # Data.JSON: unexpected character '}' at position 5

# Conditional in a script
if Data.JSON.validate "$payload"; then
  into=doc Data.JSON.parse "$payload"
  # ... use $doc
else
  printf 'Invalid JSON: %s\n' "$err" >&2
fi

Not Instantiable

Data.JSON is a static-only class. Data.JSON.new crashes with a helpful message. Use Data.JSON.parse directly.

Supported JSON Features

  • Objects: {"key": "value"}
  • Arrays: [1, 2, 3]
  • Strings: with escape sequences (\n, \t, \\, \", \/, \uXXXX)
  • Numbers: integers, floats, negative, scientific notation
  • Booleans: true, false
  • Null: null
  • Nested structures: arbitrary depth
  • Whitespace: ignored between tokens
  • Unicode: \uXXXX BMP escapes and \uXXXX\uXXXX surrogate pairs (emoji, extended CJK). Encoded to UTF-8 bytes via pure bash arithmetic – locale-independent, no external tools.

Limitations

  • Key order for hand-built (non-parsed) docs is hash-iteration order
  • No streaming/incremental parse (whole string in memory)
  • Duplicate keys: last value wins (no error)
  • No parseDeep yet (full Map/List object tree – planned)

Performance

  • Parser: no subshells, no forks. Pure string indexing via ${str:pos:1}. State shared via globals.
  • Stringify: no subshells for normal documents. Builds result via string concatenation in a shared variable. Recursive function calls with local vars for stack isolation. Depth guard at 50 spawns a subshell to reset the stack for pathological nesting.

Error Handling

Parse errors crash with position information:

  • "Data.JSON: unterminated string"
  • "Data.JSON: expected ':'"
  • "Data.JSON: expected ',' or '}'"
  • "Data.JSON: unexpected character 'X' at position N"
  • "Data.JSON: unexpected end of input"
  • "Data.JSON: invalid token"

Examples

# Config file
into=cfg Data.JSON.parse '{"host":"localhost","port":8080,"debug":true}'
into=host $cfg.get "host"
into=port $cfg.get "port"

# API response
into=resp Data.JSON.parse "$api_output"
into=status $resp.get "status"
into=first_item $resp.get "data.items.0.name"

# Build and serialize
into=doc Collection.Map.Fast.new
$doc.set "name" "Bob"
$doc.set "scores.0" "95"
$doc.set "scores.1" "87"
into=json Data.JSON.stringify "$doc"
# {"name":"Bob","scores":[95,87]}

↑ Site map