#json

    The json package provides comprehensive JSON handling capabilities, including parsing, stringifying, and type-safe conversion between JSON and other MoonBit data types.

    Everything revolves around the builtin Json enum (Null · True · False · Number · String · Array · Object), which can also be written as a literal in MoonBit source:

    flowchart LR S["text : StringView"] -->|"@json.parse<br/>raise ParseError"| J["Json (builtin enum)"] L["Json literal in source<br/>{ &quot;name&quot;: &quot;Alice&quot;, &quot;tags&quot;: [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"]

    #Basic JSON Operations

    #Parsing and Validating JSON

    ///|
    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
    },
    )
    }

    #What may appear inside a string

    Every string in a parsed document is well-formed Unicode, so each \uXXXX escape must denote a Unicode scalar value on its own or be one half of a correctly ordered surrogate pair. An escaped leading surrogate must be followed immediately by an escaped trailing surrogate, and the pair decodes to the single character it stands for; an escape that cannot pair up is a parse error, reported at the backslash that opens it.

    ///|
    test "surrogate escapes" {
    // A surrogate pair decodes to the one character it denotes.
    assert_true(@json.parse("\"\\uD83D\\uDE00\"") == Json::string("😀"))
    // An escape that cannot pair up is rejected rather than producing a
    // string that is not well-formed Unicode.
    assert_false(@json.valid("\"\\uD800\""))
    }

    RFC 8259 §9 leaves what a string may contain to the implementation, and this is where MoonBit draws that line: a String is required to be well-formed, so the alternatives would be to hand one back that is not, or to substitute U+FFFD and lose the difference between two distinct keys. Note that JSON.stringify in JavaScript does emit lone surrogates this way, so a document JavaScript and Python accept can be rejected here — as it is by Rust's serde_json when parsing into String or Value; Go's encoding/json substitutes U+FFFD instead.

    #Object Navigation

    ///|
    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")
    }

    #Array Navigation

    ///|
    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)]
    ),
    )
    }

    #Type-Safe JSON Conversion

    #From JSON to Native Types

    ///|
    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 }
    ),
    )
    }

    #Error Handling with JSON Path

    ///|
    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")
    }
    }
    }

    #Stringify Options

    stringify accepts optional parameters: indent for pretty-printing, escape_slash to escape / characters, and replacer to transform or filter keys.

    ///|
    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"
    #|}
    ),
    )
    }

    #Replacer

    Replacer controls which keys appear in stringify output or transforms values during Json::transform.

    ///|
    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}
    ),
    )
    }

    Custom replacer with a function:

    ///|
    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}")
    }

    #Parse Errors

    parse() raises ParseError with specific variants indicating what went wrong:

    ///|
    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()
    }
    }

    #The FromJson Trait

    Types that implement FromJson can be decoded from JSON via @json.from_json(). Built-in support includes: Bool, Int, Int64, UInt, UInt64, Float, Double, String, Char, Array[T], Map[String, V], Option[T], Result[Ok, Err], tuples up to 16 elements, Bytes, and Json itself.

    ///|
    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)")
    }

    #JSON-based Snapshot Testing

    @json.json_inspect() can be used as an alternative to inspect() when a value's ToJson implementation is considered a better debugging representation than its Show implementation. This is particularly true for deeply-nested data structures.

    ///|
    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 })
    }

    FromJson

    pub(open) trait FromJson {
    fn from_json(Json, JsonPath) -> Self raise JsonDecodeError
    }

    Trait for types that can be converted from Json
    impl FromJson for Unit
    impl FromJson for Bool
    impl FromJson for Char
    impl FromJson for Int
    impl FromJson for Int64
    impl FromJson for UInt
    impl FromJson for UInt64
    impl FromJson for Float
    impl FromJson for Double
    impl FromJson for String
    impl FromJson for Option[T]
    impl FromJson for Result[Ok, Err]
    impl FromJson for FixedArray[X]
    impl FromJson for Bytes
    impl FromJson for Array[X]
    impl FromJson for ArrayView[X]
    impl FromJson for Json
    impl FromJson for Map[String, V]
    impl FromJson for Tuple2[A, B]
    impl FromJson for Tuple3[A, B, C]
    impl FromJson for Tuple4[A, B, C, D]
    impl FromJson for Tuple5[A, B, C, D, E]
    impl FromJson for Tuple6[A, B, C, D, E, F]
    impl FromJson for Tuple7[A, B, C, D, E, F, G]
    impl FromJson for Tuple8[T0, T1, T2, T3, T4, T5, T6, T7]
    impl FromJson for Tuple9[T0, T1, T2, T3, T4, T5, T6, T7, T8]
    impl FromJson for Tuple10[T0, T1, T2, T3, T4, T5, T6, T7, T8, T9]
    impl FromJson for Tuple11[T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10]
    impl FromJson for Tuple12[T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11]
    impl FromJson for Tuple13[T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12]
    impl FromJson for Tuple14[T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13]
    impl FromJson for Tuple15[T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14]
    impl FromJson for Tuple16[T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15]

    JsonDecodeError

    pub(all) suberror JsonDecodeError {
    JsonDecodeError((JsonPath, String))
    } derive(Eq, Show, ToJson,
    Debug
    )

    Error type JsonDecodeError.

    JsonDecodeError::equal

    JsonDecodeError::to_string

    fn JsonDecodeError::to_string(self : JsonDecodeError) -> String

    ParseError

    pub(all) suberror ParseError {
    InvalidChar(Position, Char)
    InvalidEof
    InvalidNumber(Position, String)
    InvalidIdentEscape(Position)
    DepthLimitExceeded
    } derive(Eq, ToJson,
    Debug
    )

    Error type ParseError.
    impl Show for ParseError

    ParseError::equal

    fn ParseError::equal(ParseError, ParseError) -> Bool

    ParseError::to_string

    fn ParseError::to_string(self : ParseError) -> String

    JsonPath

    type JsonPath derive(Eq,
    Debug
    )

    key and index are mutable so that a decoder can reuse one path node for every element of an array or object instead of allocating a node per element; on large arrays that is worth roughly 3.5x. A reused node must not outlive the decode call that created it.

    MoonBit only allows assigning to a constructor's mutable field through a binding that pattern matching has narrowed to that variant, so the decoders in from_json.mbt obtain one with a guard! whose test cannot fail.
    impl Show for JsonPath
    impl ToJson for JsonPath

    JsonPath::add_index

    fn JsonPath::add_index(self : JsonPath, index : Int) -> JsonPath

    Appends an array index to the JSON path.

    JsonPath::add_key

    fn JsonPath::add_key(self : JsonPath, key : String) -> JsonPath

    Appends an object key to the JSON path.

    JsonPath::equal

    fn JsonPath::equal(JsonPath, JsonPath) -> Bool

    JsonPath::to_string

    fn JsonPath::to_string(self : JsonPath) -> String

    Position

    pub(all) struct Position {
    line : Int
    column : Int
    } derive(Eq, ToJson,
    Debug
    )

    Type Position used by this package APIs.

    Position::equal

    fn Position::equal(Position, Position) -> Bool

    Replacer

    pub struct Replacer {
    // private fields
    } derive(
    Debug
    )

    A Replacer provides a way to filter and transform JSON object properties during stringification.

    Replacers contain a function that takes a property key and value, and returns:
    • Some(value) to include the property in the output (possibly transformed)
    • None to exclude the property from the output

    Only applies to object properties, not array elements.

    Replacer::Replacer

    #alias(new, deprecated="Use `Replacer()` instead")
    fn Replacer::Replacer(f : (String, Json) -> Json?) -> Replacer

    Create a new Replacer with a custom function.

    The function receives (key, value) pairs and should return:
    • Some(transformed_value) to include the property
    • None to exclude the property

    Example

    test {
    // 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
    }
    }
    }),
    )
    }

    Replacer::exclude

    fn Replacer::exclude(array : ArrayView[StringView]) -> Replacer

    Create a Replacer that excludes the specified property keys. All other properties will be included in the output.

    Example

    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"}
    }

    Replacer::keep

    fn Replacer::keep(array : ArrayView[StringView]) -> Replacer

    Create a Replacer that only keeps the specified property keys. All other properties will be excluded from the output.

    Example

    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"}
    }

    Json

    Json::as_array

    #deprecated("Suggestion: `if json is Array(array) { Some(array) } else { None }`")
    fn Json::as_array(self : Json) -> Array[Json]?

    Try to get this element as an Array

    Json::as_bool

    #deprecated("Suggestion: `if json is True { Some(true) } else if json is False { Some(false) } else { None }`")
    fn Json::as_bool(self : Json) -> Bool?

    Try to get this element as a Boolean

    Json::as_null

    #deprecated("Suggestion: `if json is Null { Some(()) } else { None }`")
    fn Json::as_null(self : Json) -> Unit?

    Try to get this element as a Null

    Json::as_number

    #deprecated("Suggestion: `if json is Number(n) { Some(n) } else { None }`")
    fn Json::as_number(self : Json) -> Double?

    Try to get this element as a Number

    Json::as_object

    #deprecated("This function is deprecated.")
    fn Json::as_object(self : Json) -> Map[String, Json]?

    Try to get this element as an Object

    Json::as_string

    #deprecated("Suggestion: `if json is String(s) { Some(s) } else { None }`")
    fn Json::as_string(self : Json) -> String?

    Try to get this element as a String

    Json::item

    #deprecated("Suggestion: `if json is Array(array) { array.get(index) } else { None }`")
    fn Json::item(self : Json, index : Int) -> Json?

    Try to get this element as a Json Array and get the element at the index as a Json Value

    Json::stringify

    fn Json::stringify(self : Json, escape_slash? : Bool, indent? : Int, replacer? : Replacer) -> String

    Convert this Json value to a String
    • escape_slash: Whether to escape '/' as '/' (default: false)
    • indent: Number of spaces to indent nested structures (default: 0 = non-indented)
    • replacer: An optional Replacer function to transform or filter values during stringification

    Replacer

    The replacer parameter allows you to control which object properties are included in the output and optionally transform values during stringification. Only applies to object properties, not array elements.

    Creating Replacers

    1. Replacer(f) - Create a custom replacer with a function (String, Json) -> Json?:
      • Return Some(value) to include the property (possibly transformed)
      • Return None to exclude the property

    2. Replacer::keep(keys) - Include only the specified property keys

    3. Replacer::exclude(keys) - Exclude the specified property keys

    Examples

    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}
    }

    Nested Objects

    Replacers work recursively on nested objects:

    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}
    }

    Json::transform

    fn Json::transform(self : Json, replacer : Replacer) -> Json

    Transform a JSON value by applying a replacer recursively to all object properties.

    Unlike stringify(replacer~) which only affects the string output, transform() returns a new JSON value with properties filtered and transformed according to the replacer.

    This is useful when you want to create a modified JSON structure that can be further processed, rather than just converting to a string.

    Example

    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 }
    }

    Behavior

    • Recursively applies the replacer to all nested objects
    • Arrays are rebuilt element by element, so objects nested inside an array are transformed as well (the replacer itself is only applied to object members)
    • All other values (strings, numbers, booleans, null) are returned unchanged
    • The original JSON value is not modified; a new value is returned

    Json::value

    #deprecated("Suggestion: `if json is Object(obj) { obj.get(key) } else { None }`")
    fn Json::value(self : Json, key : String) -> Json?

    Try to get this element as a Json Object and get the element with the key as a Json Value
    impl Show for Json
    impl ToJson for Json

    from_json

    fn[T : FromJson] from_json(json : Json, path? : JsonPath) -> T raise JsonDecodeError

    Create from json.

    json_inspect

    #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 InspectError

    Inspect JSON value with snapshot-friendly formatting.

    parse

    fn parse(input : StringView, max_nesting_depth? : Int) -> Json raise ParseError

    Parse a JSON input string into a Json value, with an optional maximum nesting depth (default is 1024)

    What strings may contain

    Every string in the result is well-formed Unicode: each \uXXXX escape must denote a Unicode scalar value on its own, or be one half of a correctly ordered surrogate pair. An escaped leading surrogate (\uD800\uDBFF) must therefore be followed immediately by an escaped trailing surrogate (\uDC00\uDFFF), and the pair is decoded as the one character it stands for.

    An escape that cannot pair up raises InvalidChar positioned at the backslash that opens it — that one position, whatever the scan actually stopped on. Input that simply runs out still raises InvalidEof, and a second escape that is itself malformed is reported as the hex-digit error it is, at the offending digit.

    This is a limit on what a string may contain, which RFC 8259 §9 leaves to the implementation — not a claim about which documents are grammatically well formed, since §8.2 admits unpaired surrogate escapes, nor a claim of I-JSON (RFC 7493) conformance, which restricts more than this. It is chosen because MoonBit's String is required to be well-formed, so the alternatives are to hand back a string that violates that invariant, or to substitute U+FFFD and silently lose the distinction between two different keys. A parse error is the only one of the three a caller can see and act on.

    The cost is real: JSON.stringify in JavaScript emits lone surrogates as \uXXXX, so some JSON that JavaScript and Python accept is rejected here. Rust's serde_json rejects it too when parsing into String or Value, though its byte-oriented mode admits WTF-8; Go's encoding/json substitutes U+FFFD, while its experimental v2 parser is stricter.

    to_json

    fn[T : ToJson] to_json(value : T) -> Json

    Convert value to Json.

    This is the free-function form of the ToJson trait method, symmetric with @json.from_json. Prefer it over the promoted value.to_json() method.

    test {
    @debug.debug_inspect(
    @json.to_json([1, 2, 3]),
    content="Array([Number(1), Number(2), Number(3)])",
    )
    }

    valid

    fn valid(input : StringView) -> Bool

    Returns whether input parses. "Valid" here means exactly what parse accepts, so it includes the restriction on string contents described there: a document whose only defect is an unpaired surrogate escape is reported as invalid.