---
hide:
  - toc
search:
  exclude: true
---
```````pony linenums="1"
"""
# json

JSON library for Pony. All JSON values are `val` — construction
uses chained method calls that return new values with structural sharing
via persistent collections. Three access patterns are available for
reading and modifying JSON structures, from simple one-shot lookups to
composable paths to string-based multi-match queries.

## Building JSON

`JsonObject` and `JsonArray` are constructed via chained method calls.
Each call returns a new value; the original is unchanged:

```pony
use json = "json"

let doc = json.JsonObject
  .update("name", "Alice")
  .update("age", I64(30))
  .update("active", true)
  .update("tags", json.JsonArray
    .push("admin")
    .push("developer"))
  .update("address", json.JsonObject
    .update("city", "Portland")
    .update("state", "OR"))
```

Values in the `JsonValue` union — `JsonObject`, `JsonArray`, `String`,
`I64`, `F64`, `Bool`, and `None` — can be stored in objects and arrays.
JSON null is Pony's `None`.

## Parsing JSON

`JsonParser.parse()` returns errors as data — no exceptions to catch:

```pony
match json.JsonParser.parse(source)
| let j: json.JsonValue =>
  // j is the parsed document (object, array, or scalar)
  match j
  | let obj: json.JsonObject =>
    env.out.print("Root is object with " + obj.size().string() + " keys")
  end
| let err: json.JsonParseError =>
  env.out.print("Error at offset " + err.offset.string() + ": "
    + err.message)
end
```

## Reading Values: JsonNav

`JsonNav` wraps a value for chained read-only access. If any step
in the chain fails (missing key, out-of-bounds index, type mismatch),
`JsonNotFound` propagates silently through the rest of the chain — no
partial failures or exceptions:

```pony
let nav = json.JsonNav(doc)

// Chained access — returns the value or JsonNotFound
try
  let city = nav("address")("city").as_string()?
  env.out.print("City: " + city)
end

// JsonNotFound propagates — no crash, just JsonNotFound at the end
let missing = nav("nonexistent")("deep")("path")
if not missing.found() then
  env.out.print("Path not found")
end
```

Terminal extractors — `as_string()`, `as_i64()`, `as_f64()`,
`as_bool()`, `as_null()`, `as_object()`, `as_array()` — unwrap the
value or raise if the type doesn't match or the nav holds JsonNotFound.

## Composable Paths: JsonLens

`JsonLens` describes a reusable path (not tied to a specific document).
It supports reading, updating, and removing values. `compose()` chains
two lenses sequentially; `or_else()` tries a primary lens and falls
back to an alternative:

```pony
// Define a reusable path
let host_lens = json.JsonLens("config")("database")("host")

// Read
match host_lens.get(doc)
| let host: json.JsonValue => env.out.print("Host: " + host.string())
| json.JsonNotFound => env.out.print("no host configured")
end

// Update — returns a new document with the value changed
match host_lens.set(doc, "prod.example.com")
| let updated: json.JsonValue =>
  // updated is a new doc; original doc is unchanged
  None
| json.JsonNotFound => env.out.print("path doesn't exist")
end

// Remove a key
let debug_lens = json.JsonLens("config")("debug")
match debug_lens.remove(doc)
| let updated: json.JsonValue => None // debug key removed
| json.JsonNotFound => None // path didn't exist
end

// Compose two lenses
let db_lens = json.JsonLens("config")("database")
let port_lens = db_lens.compose(json.JsonLens("port"))

// Fallback: try primary, fall back to alternative
let fallback = json.JsonLens("primary_host")
  .or_else(json.JsonLens("fallback_host"))
```

## String-Based Queries: JsonPath

`JsonPath` implements a subset of RFC 9535 — string-based query
expressions that can match multiple values via wildcards, recursive
descent, and slicing. Parse a path string once, then apply it to any
number of documents:

```pony
// Parse returns errors as data (consistent with JsonParser)
match json.JsonPathParser.parse("$.store.book[*].author")
| let path: json.JsonPath =>
  let authors = path.query(doc) // Array[JsonValue] val
  for author in authors.values() do
    env.out.print(author.string())
  end
| let err: json.JsonPathParseError =>
  env.out.print(err.string())
end

// compile() raises on invalid input — use for known-valid paths
try
  let prices = json.JsonPathParser.compile("$.store..price")?
  let results = prices.query(doc)
end
```

Supported JSONPath syntax:

* `$.key` or `$['key']` — child by name
* `$[0]` or `$[-1]` — array index (negative counts from end)
* `$[*]` or `$.*` — wildcard (all children)
* `$..key` or `$..*` — recursive descent
* `$[0:3]` — slice (start inclusive, end exclusive)
* `$[::2]` or `$[::-1]` — slice with step (forward or reverse)
* `$[0,2,4]` — union (multiple indices or names)
* `$[?@.price < 10]` — filter by comparison
* `$[?@.author]` — filter by existence (member present)
* `$[?@.a > 1 && @.b < 2]` — logical AND, OR (`||`), NOT (`!`)
* `$[?@.type == $.default]` — absolute query (`$`) in filters
* `$[?match(@.name, "[A-Z].*")]` — function extensions (`length`, `count`,
  `match`, `search`, `value`)
* `query_one()` — convenience returning first match or `JsonNotFound`

## Serialization

`JsonObject` and `JsonArray` implement `Stringable`. Compact output
uses `string()`; indented output uses `pretty_string()`:

```pony
let obj = json.JsonObject
  .update("a", I64(1))
  .update("b", json.JsonArray.push(I64(2)).push(I64(3)))

env.out.print(obj.string())
// {"a":1,"b":[2,3]}

env.out.print(obj.pretty_string())
// {
//   "a": 1,
//   "b": [
//     2,
//     3
//   ]
// }

// Custom indent string (default is two spaces)
env.out.print(obj.pretty_string("\t"))
```

## Choosing an Access Pattern

* **`JsonNav`** — one-shot chained access. Read-only. Best for
  "grab this one value." Wraps a specific document; JsonNotFound propagates
  through chains.

* **`JsonLens`** — reusable path with get/set/remove. Best for
  "define a path once, apply it to many documents." Supports
  composition (`compose`) and fallbacks (`or_else`). Not tied to a
  specific document.

* **`JsonPath`** — string-based query language (RFC 9535 subset). Best
  for "find all values matching a pattern." Supports wildcards,
  recursive descent, and slicing. Returns arrays of results.

Start with `JsonNav` for simple reads. Move to `JsonLens` when you
need to modify values or reuse paths. Use `JsonPath` when you need
multi-match queries, wildcard selection, or user-provided path strings.

## Token Parser

`JsonTokenParser` is a streaming alternative to `JsonParser`. Instead
of building a complete document tree, it emits tokens to a callback
as they are encountered. Use this when you need to process large
documents without materializing the full tree, or when you need custom
processing logic:

```pony
let parser = json.JsonTokenParser(
  object is json.JsonTokenNotify
    fun ref apply(parser': json.JsonTokenParser, token: json.JsonToken) =>
      match token
      | json.JsonTokenKey => env.out.print("Key: " + parser'.last_string)
      | json.JsonTokenString => env.out.print("String: " + parser'.last_string)
      | json.JsonTokenNumber =>
        match parser'.last_number
        | let n: I64 => env.out.print("Int: " + n.string())
        | let f: F64 => env.out.print("Float: " + f.string())
        end
      | json.JsonTokenObjectStart => env.out.print("{")
      | json.JsonTokenObjectEnd => env.out.print("}")
      | json.JsonTokenArrayStart => env.out.print("[")
      | json.JsonTokenArrayEnd => env.out.print("]")
      end
  end)
try parser.parse(source)? end
```

For most use cases, `JsonParser.parse()` is simpler and sufficient.

"""

use "collections/persistent"

type JsonValue is (JsonObject | JsonArray | String | I64 | F64 | Bool | None)

```````