json
Parse and serialize JSON, read and write JSON files, and work with nested maps by dotted path.
introduce json| Function | Signature | Returns |
|---|---|---|
parse | parse(s) | Oxigen value |
stringify | stringify(val) | str |
read | read(path) | Oxigen value |
write | write(path, val) | None |
get_in | get_in(obj, path) | value or None |
set_in | set_in(obj, path, val) | the root map |
del_in | del_in(obj, path) | the root map |
has_key_in | has_key_in(obj, path) | bool |
set_where | set_where(arr, key, val, fields) | array |
merge | merge(a, b) | map |
deep_merge | deep_merge(a, b) | map |
Basic map operations are global builtins — no import needed:
has(m, key), keys(m), values(m), insert(m, key, val), remove(m, key).
#Type mapping
| JSON | Oxigen |
|---|---|
null | None |
true / false | True / False |
| number (integer) | Integer (Float past the range of a 64-bit int) |
| number (fractional or with an exponent) | Float |
"string" | String |
[array] | Array |
{object} | Map |
#Parsing and serializing
#parse(s)
Parse a JSON string. Objects become maps, arrays stay arrays, null becomes
None.
introduce json
d := json.parse("\{\"a\": [1, 2], \"b\": null, \"c\": 1.5\}")
println(d) // {a: [1, 2], b: None, c: 1.5}
println(type(d["c"])) // FLOATNote the \{ escapes: a bare { inside an Oxigen string opens an
interpolation, so a JSON literal written inline has to escape its braces. Most
of the time the JSON comes from a file or a response body and the question does
not arise.
Invalid JSON is an Error:
r := <type<Error> || <Value>>(json.parse("not json"))
println(is_error(r)) // TrueThe parser follows RFC 8259 strictly: numbers like 01, 1. or .5, unknown
escapes like \q, and a \u escape that is not four hex digits are errors.
A character outside the Basic Multilingual Plane written as a surrogate pair
(\ud83d\ude00) decodes to that one character; a lone surrogate is an error.
Arrays and objects may nest at most 512 levels deep. Deeper input is an
error rather than a crash, which matters when the JSON comes from a client:
a request body of a few million [ would otherwise exhaust the stack.
#stringify(val)
Serialize any Oxigen value to a compact JSON string (no spaces, no trailing newline).
println(json.stringify({"name": "Oxigen", "version": 1}))
// {"name":"Oxigen","version":1}Map key order is preserved as inserted. JSON keys are strings, so a number,
boolean or char key is written as its text ({1: 2} becomes {"1":2}); any
other key type is an error.
A structure that contains itself (a := [1]; a[0] = a) is an error rather than
an endless recursion, and so is nesting deeper than 512 levels.
#read(path)
parse(io.read_file(path)) — read a file and parse it in one step.
config := json.read("config.json")
println(config.debug)#write(path, val)
io.write_file(path, stringify(val)) — serialize and write in one step.
Replaces the file if it exists.
json.write("config.json", {"debug": True, "level": 3})
println(json.read("config.json")) // {debug: True, level: 3}#Nested access by dotted path
The four *_in functions address nested structure with "a.b.c" strings, so
you do not need to check each level yourself.
#get_in(obj, path)
The value at a dotted path, or None if any segment is missing or is not a
map.
user := {"name": "Alice", "address": {"city": "NYC", "zip": "10001"}}
println(json.get_in(user, "address.city")) // NYC
println(json.get_in(user, "address.nope")) // None
println(json.get_in(user, "name.city")) // NoneNone is also a legitimate stored value, so a None result means "missing or
null". Use has_key_in when you need to tell those apart.
A path addresses maps only — it does not descend into arrays, so
"items.0.name" will not work. For an array of records, find the record with
array.find_where and carry on from there;
to update or add one, use set_where.
#set_in(obj, path, val)
Set the value at a dotted path, creating intermediate maps as needed. Returns the root map.
user := {"name": "Alice", "address": {"city": "NYC"}}
updated := json.set_in(user, "address.state", "NY")
println(updated)
// {name: Alice, address: {city: NYC, state: NY}}
// Intermediate levels are created:
println(json.set_in({}, "a.b.c", 1)) // {a: {b: {c: 1}}}Maps are mutable and shared.
set_inmodifies the map you passed and returns that same map —userabove is changed too, not justupdated. The same is true ofdel_in,merge,deep_merge, and theinsert/removebuiltins. If you need the original preserved, copy it first (for examplejson.parse(json.stringify(user))).
#del_in(obj, path)
Remove the key at a dotted path. Returns the root map. A path that does not exist is a no-op.
user := {"name": "Alice", "address": {"city": "NYC", "zip": "10001"}}
println(json.del_in(user, "address.zip"))
// {name: Alice, address: {city: NYC}}#has_key_in(obj, path)
Does the dotted path exist? True even when the stored value is None.
println(json.has_key_in(user, "address.city")) // True
println(json.has_key_in(user, "a.b")) // False#Arrays of records
JSON data is usually a list of records — {"information": [{...}, {...}]} —
and a dotted path stops at the array. These two cover it: find the record with
array.find_where, and update or add one
with set_where.
Given example.json:
{"information": [
{"person": "john", "info": {"age": 32, "job": "unemployed"}},
{"person": "jane"},
{"info": {"age": 28, "job": "engineer"}}
]}#set_where(arr, key, val, fields)
Update the first record whose key equals val by deep-merging fields into
it — or append a new record when there is none. This is the same rule as
set_in, which also creates what is missing.
introduce json
data := json.read("example.json")
// john got a job — age survives, because the merge is recursive
data.information = json.set_where(data.information, "person", "john",
{"info": {"job": "developer"}})
// ada is new — appended as {"person": "ada", "info": {...}}
data.information = json.set_where(data.information, "person", "ada",
{"info": {"age": 36, "job": "mathematician"}})
// jane has no "info" at all — it gets created
data.information = json.set_where(data.information, "person", "jane",
{"info": {"age": 41}})
json.write("example.json", data){"information": [
{"person": "john", "info": {"age": 32, "job": "developer"}},
{"person": "jane", "info": {"age": 41}},
{"info": {"age": 28, "job": "engineer"}},
{"person": "ada", "info": {"age": 36, "job": "mathematician"}}
]}Notes:
- Records without the key are skipped, not errors. The third entry above
has no
"person"field and is simply passed over — ragged data does not need a guard at every element. - Only the first match is updated.
- It returns the array, edited in place (arrays are shared references, like maps). Assigning the result back is the clearer form and costs nothing.
json.writerewrites the whole file; there is no partial update.
#Editing a record directly
When you only want to change one field, find the record and assign — what
find_where hands back is the record in the array, not a copy:
introduce json
introduce array
data := json.read("example.json")
john := array.find_where(data.information, "person", "john")
john.info.job = "developer"
json.write("example.json", data) // data already sees the changeset_where is the version that also handles "…and add them if they are not
there yet".
#Removing a record
data.information = array.remove_where(data.information, "person", "john")#Merging
#merge(a, b)
Shallow merge: every key in b overwrites the same key in a.
defaults := {"port": 8080, "debug": False}
overrides := {"debug": True, "host": "0.0.0.0"}
println(json.merge(defaults, overrides))
// {port: 8080, debug: True, host: 0.0.0.0}A nested map in b replaces the whole nested map in a — that is what
"shallow" means.
#deep_merge(a, b)
Recursive merge: where both sides have a map under the same key, the maps are
merged; anything else in b overwrites.
a := {"server": {"host": "localhost", "port": 8080}}
b := {"server": {"port": 9090}}
println(json.deep_merge(a, b))
// {server: {host: localhost, port: 9090}}This is the one to use for layering config: defaults, then a file, then environment overrides.
#Worked example
introduce json
introduce net
introduce array
// Fetch a JSON list, keep the interesting fields, and save it.
resp := net.get("https://api.example.com/users")
users := json.parse(resp.body)
fun summary(u <map>) {
{"id": u["id"], "city": json.get_in(u, "address.city")}
}
json.write("users.json", array.map(users, summary))Layering configuration:
introduce json
introduce io
introduce os
defaults := {"port": 8000, "log": {"level": "info", "file": "app.log"}}
file_cfg := option {
io.file_exists("app.json") -> { json.read("app.json") }
{ {} }
}
config := json.deep_merge(defaults, file_cfg)
config = json.set_in(config, "log.level", os.env_get("LOG_LEVEL")) when os.env_get("LOG_LEVEL") != None
println(json.get_in(config, "log.level"))See also: toml — the same API for TOML, net and api for sending and receiving JSON over HTTP, io for raw file access.