json-ng¶
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:
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:
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:
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:
// 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:
// 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:
$.keyor$['key']— child by name$[0]or$[-1]— array index (negative counts from end)$[*]or$.*— wildcard (all children)$..keyor$..*— 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 orJsonNotFound
Serialization¶
JsonObject and JsonArray implement Stringable. Compact output
uses string(); indented output uses pretty_string():
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:
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.
Public Types¶
- class JsonArray
- class JsonLens
- class JsonNav
- primitive JsonNotFound
- class JsonObject
- class JsonParseError
- primitive JsonParser
- class JsonPath
- class JsonPathParseError
- primitive JsonPathParser
- type JsonToken
- primitive JsonTokenArrayEnd
- primitive JsonTokenArrayStart
- primitive JsonTokenFalse
- primitive JsonTokenKey
- interface JsonTokenNotify
- primitive JsonTokenNull
- primitive JsonTokenNumber
- primitive JsonTokenObjectEnd
- primitive JsonTokenObjectStart
- class JsonTokenParser
- primitive JsonTokenString
- primitive JsonTokenTrue
- type JsonValue