flowchart LR
S["text : StringView"] -->|"@json.parse<br/>raise ParseError"| J["Json (builtin enum)"]
L["Json literal in source<br/>{ "name": "Alice", "tags": [1, 2] }"] --> J
J -->|"@json.from_json (T : FromJson)<br/>raise JsonDecodeError, reports a JsonPath"| T["typed value T"]
T -->|"@json.to_json (T : ToJson)"| J
J -->|"stringify(escape_slash?, indent?, replacer?)"| O["String"]///|
test "parse and validate jsons" {
// Check if a string is valid JSON
assert_true(@json.valid("{\"key\": 42}"))
assert_true(@json.valid("[1, 2, 3]"))
assert_true(@json.valid("null"))
assert_true(@json.valid("false"))
// Parse JSON string into Json value
let json = @json.parse("{\"key\": 42}") catch {
(_ : @json.ParseError) => panic()
// _ => panic() // redundant, the type checker won't refine further
}
// Pretty print with indentation
inspect(
json.stringify(indent=2),
content={
let output =
#|{
#| "key": 42
#|}
output
},
)
}///|
test "json object navigation" {
let json = @json.parse(
(
#|{"string":"hello","number":42,"array":[1,2,3]}
),
)
// Access string
let string_opt = if json is { "string": String(str), .. } {
Some(str)
} else {
None
}
debug_inspect(
string_opt,
content=(
#|Some("hello")
),
)
// Access number
let number_opt = if json is { "number": Number(num, ..), .. } {
Some(num)
} else {
None
}
debug_inspect(number_opt, content="Some(42)")
// Access array
let array_opt = if json is { "array": Array(arr), .. } {
Some(arr)
} else {
None
}
debug_inspect(array_opt, content="Some([Number(1), Number(2), Number(3)])")
// Handle missing keys gracefully
json is { "value"? : value, .. }
debug_inspect(value, content="None")
}///|
test "json array navigation" {
let array = @json.parse("[1, 2, 3, 4, 5]")
// Access by index
let first = if array is Array([f, ..]) { Some(f) } else { None }
debug_inspect(first, content="Some(Number(1))")
// Access out of bounds
let missing = if array is Array(arr) { arr.get(10) } else { None }
debug_inspect(missing, content="None")
// Iterate through array
array is Array(values)
debug_inspect(
values.iter().to_array(),
content=(
#|[Number(1), Number(2), Number(3), Number(4), Number(5)]
),
)
}///|
test "json decode" {
// Decode basic types
let json_number = (42 : Json)
let number : Int = @json.from_json(json_number)
inspect(number, content="42")
// Decode arrays
let json_array = ([1, 2, 3] : Json)
let array : Array[Int] = @json.from_json(json_array)
debug_inspect(array, content="[1, 2, 3]")
// Decode maps
let json_map = ({ "a": 1, "b": 2 } : Json)
let map : Map[String, Int] = @json.from_json(json_map)
debug_inspect(
map,
content=(
#|{ "a": 1, "b": 2 }
),
)
}///|
test "json path" {
// Handle decode errors
try {
let _arr : Array[Int] = @json.from_json(([42, "not a number", 49] : Json))
panic()
} catch {
JsonDecodeError((path, msg)) => {
inspect(path, content="/1")
inspect(msg, content="Int::from_json: expected number")
}
}
}///|
test "stringify options" {
let json : Json = { "name": "Alice", "age": 30, "secret": "hidden" }
// compact (default)
inspect(
json.stringify(),
content=(
#|{"name":"Alice","age":30,"secret":"hidden"}
),
)
// pretty-printed
inspect(
json.stringify(indent=2),
content=(
#|{
#| "name": "Alice",
#| "age": 30,
#| "secret": "hidden"
#|}
),
)
}///|
test "replacer keep and exclude" {
let json : Json = { "name": "Alice", "age": 30, "secret": "hidden" }
// keep only specified keys
let kept = json.stringify(replacer=@json.Replacer::keep(["name", "age"]))
inspect(
kept,
content=(
#|{"name":"Alice","age":30}
),
)
// exclude specified keys
let excluded = json.stringify(replacer=@json.Replacer::exclude(["secret"]))
inspect(
excluded,
content=(
#|{"name":"Alice","age":30}
),
)
}///|
test "replacer custom" {
let json : Json = { "x": 1.0, "y": 2.0 }
// double all number values
let replaced = json.transform(
Replacer(fn(_key, value) {
match value {
Number(n, ..) => Some(Json::number(n * 2))
other => Some(other)
}
}),
)
inspect(replaced.stringify(), content="{\"x\":2,\"y\":4}")
}///|
test "parse errors" {
// InvalidChar
try {
let _ = @json.parse("{invalid")
panic()
} catch {
InvalidChar(pos, _ch) =>
debug_inspect((pos.line, pos.column), content="(1, 1)")
_ => panic()
}
// InvalidEof
try {
let _ = @json.parse("{\"a\":")
panic()
} catch {
InvalidEof => ()
_ => panic()
}
}///|
test "from_json" {
// decode a tuple
let json : Json = [1, "hello"]
let tuple : (Int, String) = @json.from_json(json)
debug_inspect(tuple, content="(1, \"hello\")")
// decode an optional
let opt : Int? = @json.from_json(null)
debug_inspect(opt, content="None")
// decode a Result
let json3 : Json = { "Ok": 42 }
let result : Result[Int, String] = @json.from_json(json3)
debug_inspect(result, content="Ok(42)")
}///|
test "json inspection" {
let null = null
// Simple json values
let json_value : Json = { "key": "value", "numbers": [1, 2, 3] }
@json.json_inspect(json_value, content={
"key": "value",
"numbers": [1, 2, 3],
})
// Null and boolean values
let json_special = { "null": null, "bool": true }
@json.json_inspect(json_special, content={ "null": null, "bool": true })
}impl FromJson for FixedArray[X]fn[T0 : FromJson, T1 : FromJson, T2 : FromJson, T3 : FromJson, T4 : FromJson, T5 : FromJson, T6 : FromJson, T7 : FromJson, T8 : FromJson, T9 : FromJson, T10 : FromJson, T11 : FromJson] from_json(json : Json, path : JsonPath) -> (T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11) raise JsonDecodeErrorfn[T0 : FromJson, T1 : FromJson, T2 : FromJson, T3 : FromJson, T4 : FromJson, T5 : FromJson, T6 : FromJson, T7 : FromJson, T8 : FromJson, T9 : FromJson, T10 : FromJson, T11 : FromJson, T12 : FromJson] from_json(json : Json, path : JsonPath) -> (T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12) raise JsonDecodeErrorfn[T0 : FromJson, T1 : FromJson, T2 : FromJson, T3 : FromJson, T4 : FromJson, T5 : FromJson, T6 : FromJson, T7 : FromJson, T8 : FromJson, T9 : FromJson, T10 : FromJson, T11 : FromJson, T12 : FromJson, T13 : FromJson] from_json(json : Json, path : JsonPath) -> (T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13) raise JsonDecodeErrorfn[T0 : FromJson, T1 : FromJson, T2 : FromJson, T3 : FromJson, T4 : FromJson, T5 : FromJson, T6 : FromJson, T7 : FromJson, T8 : FromJson, T9 : FromJson, T10 : FromJson, T11 : FromJson, T12 : FromJson, T13 : FromJson, T14 : FromJson] from_json(json : Json, path : JsonPath) -> (T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14) raise JsonDecodeErrorfn[T0 : FromJson, T1 : FromJson, T2 : FromJson, T3 : FromJson, T4 : FromJson, T5 : FromJson, T6 : FromJson, T7 : FromJson, T8 : FromJson, T9 : FromJson, T10 : FromJson, T11 : FromJson, T12 : FromJson, T13 : FromJson, T14 : FromJson, T15 : FromJson] from_json(json : Json, path : JsonPath) -> (T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15) raise JsonDecodeErrorimpl FromJson for StringViewtest {
// Transform numbers and exclude sensitive fields
ignore(
@json.Replacer((key, value) => {
match key {
"password" | "secret" => None // exclude sensitive fields
_ =>
match value {
Number(n, ..) => Some(Json::number(n * 2.0)) // double all numbers
_ => Some(value) // keep other values as-is
}
}
}),
)
}test {
let replacer = @json.Replacer::exclude(["password", "secret", "private"])
let json : Json = {
"name": "Alice",
"age": 30.0,
"password": "secret",
"email": "alice@example.com",
}
ignore(json.stringify(replacer~)) // {"name":"Alice","age":30,"email":"alice@example.com"}
}test {
let replacer = @json.Replacer::keep(["name", "age", "email"])
let json : Json = {
"name": "Alice",
"age": 30.0,
"email": "alice@example.com",
"password": "secret",
}
ignore(json.stringify(replacer~)) // {"name":"Alice","age":30,"email":"alice@example.com"}
}test {
let json : Json = { "a": 1.0, "b": 2.0, "c": 3.0, "password": "secret" }
// Keep only specific keys
let keep_replacer = @json.Replacer::keep(["a", "c"])
ignore(json.stringify(replacer=keep_replacer)) // {"a":1,"c":3}
// Exclude sensitive keys
let exclude_replacer = @json.Replacer::exclude(["password"])
ignore(json.stringify(replacer=exclude_replacer)) // {"a":1,"b":2,"c":3}
// Custom transformation
let transform_replacer = @json.Replacer((_key, value) => {
match value {
Number(n, ..) => Some(Json::number(n * 10.0)) // multiply numbers by 10
_ => Some(value) // keep other values unchanged
}
})
ignore(json.stringify(replacer=transform_replacer)) // {"a":10,"b":20,"c":30,"password":"secret"}
// Filter and transform
let filter_replacer = @json.Replacer((key, value) => {
match key {
"password" => None // exclude password
_ =>
match value {
Number(n, ..) => Some(Json::number(n + 100.0)) // add 100 to numbers
_ => Some(value)
}
}
})
ignore(json.stringify(replacer=filter_replacer)) // {"a":101,"b":102,"c":103}
}test {
let nested : Json = {
"user": { "name": "Alice", "password": "secret" },
"id": 123.0,
}
let safe_replacer = @json.Replacer::exclude(["password"])
ignore(nested.stringify(replacer=safe_replacer)) // {"user":{"name":"Alice"},"id":123}
}test {
let json : Json = {
"user": { "name": "Alice", "password": "secret", "age": 30.0 },
"id": 123.0,
}
// Remove sensitive data and double numeric values
ignore(
json.transform(
Replacer((key, value) => {
match key {
"password" => None // exclude password fields
_ =>
match value {
Number(n, ..) => Some(Json::number(n * 2.0)) // double numbers
_ => Some(value) // keep other values
}
}
}),
),
)
// Result: { "user": { "name": "Alice", "age": 60 }, "id": 246 }
}#alias(inspect, deprecated="Use `json_inspect` without package name instead.")
#callsite(autofill(args_loc, loc))
fn json_inspect(obj : &ToJson, content? : Json, loc~ : SourceLoc, args_loc~ : ArgsLoc) -> Unit raise InspectErrortest {
@debug.debug_inspect(
@json.to_json([1, 2, 3]),
content="Array([Number(1), Number(2), Number(3)])",
)
}Install
Installed by default