JSON

Nostos provides comprehensive JSON support for parsing, serialization, and typed deserialization. The fromJson[T] builtin makes it easy to convert JSON directly to your custom types with full type safety.

Parsing JSON

Use jsonParse to parse a JSON string into a Json value.

Tip: Use single-quoted strings for JSON to avoid escaping double quotes: '{"name": "Alice"}' instead of "{\"name\": \"Alice\"}"

# Import from stdlib
use stdlib.json.{jsonParse, jsonStringify, Json}

main() = {
    # Parse a JSON string (single quotes make it cleaner!)
    json: Json = jsonParse('{"name": "Alice", "age": 30}')

    # The Json type is a variant:
    # Null | Bool(Bool) | Number(Float) | String(String)
    # | Array(List[Json]) | Object(List[(String, Json)])

    println(json)
}

Note: In the REPL, stdlib functions are auto-imported so you can use jsonParse directly. In script files, use the use statement to import them.

Typed Deserialization with fromJson

The fromJson[T](json) builtin converts JSON to typed Nostos values. This is the recommended way to work with JSON data.

use stdlib.json.{jsonParse}

# Define your types
type Person = { name: String, age: Int }

main() = {
    # Parse JSON string
    json: Json = jsonParse('{"name": "Alice", "age": 30}')

    # Convert to typed value (fromJson is a builtin, no import needed)
    person: Person = fromJson[Person](json)

    println(person.name)  # "Alice"
    println(person.age)   # 30
}

Note: jsonToType[T] is an alias for fromJson[T] and works identically.

Supported Types

fromJson supports all Nostos types including numeric types, records, variants, tuples, and nested structures.

# All numeric types work
type Sizes = {
    i8: Int8, i16: Int16, i32: Int32, i64: Int,
    u8: UInt8, u16: UInt16, u32: UInt32, u64: UInt64,
    f32: Float32, f64: Float
}

# Records with any field types
type Config = { name: String, enabled: Bool, value: Float }

# Nested types
type Address = { city: String, zip: Int }
type Person = { name: String, address: Address }

# Tuples (represented as JSON arrays)
type Pair = { data: (Int, String) }

main() = {
    # Nested record example
    json: Json = jsonParse('{"name": "Bob", "address": {"city": "Oslo", "zip": 1234}}')
    person: Person = fromJson[Person](json)
    println(person.address.city)  # "Oslo"

    # Tuple example
    json2: Json = jsonParse('{"data": [42, "hello"]}')
    pair: Pair = fromJson[Pair](json2)
    println(pair.data)  # (42, hello)
}

Variants (Sum Types)

Variants use a JSON object with the constructor name as the key. Fields use _0, _1, etc. for positional values. Unit variants (no fields) use null or empty object.

type Result = Ok(Int) | Err(String)

getValue(Ok(n)) = n
getValue(Err(_)) = 0

main() = {
    # Ok(42) is represented as: {"Ok": {"_0": 42}}
    json: Json = jsonParse('{"Ok": {"_0": 42}}')
    result: Result = fromJson[Result](json)

    println(getValue(result))  # 42

    # Err("fail") is: {"Err": {"_0": "something went wrong"}}
    json2: Json = jsonParse('{"Err": {"_0": "something went wrong"}}')
    result2: Result = fromJson[Result](json2)
    println(tagOf(result2))  # "Err"
}

# Multi-field variants use _0, _1, _2...:
type Coord = Point(Int, Int)
# Point(10, 20) is: {"Point": {"_0": 10, "_1": 20}}

# Unit variants (no payload) use null or empty object:
type Status = Active | Pending | Done
# Active is: {"Active": null} or {"Active": {}}

Error Handling

fromJson throws catchable exceptions when the JSON doesn't match the expected type.

type Person = { name: String, age: Int }

main() = {
    # Missing required field
    json: Json = jsonParse('{"name": "Alice"}')

    try {
        person: Person = fromJson[Person](json)
        "success: " ++ person.name
    } catch { e -> "Error: " ++ e }
    # Returns: "Error: Missing field: age"
}

Common Errors

  • Missing field: <name> - Required field not in JSON
  • Unknown constructor: <name> - Variant constructor doesn't exist
  • Unknown type: <name> - Type not defined
  • Expected Json Object, found <type> - Wrong JSON structure

Round-Trip: Value to JSON and Back

Use reflect() to convert a typed value to JSON, and fromJson to parse it back.

type User = { id: Int, name: String, active: Bool }

main() = {
    # Create a user (positional construction)
    user = User(1, "Bob", true)

    # Convert to JSON string
    jsonStr = jsonStringify(user)
    println("JSON: " ++ jsonStr)

    # Parse back to typed value
    parsed: Json = jsonParse(jsonStr)
    user2: User = fromJson[User](parsed)

    println("Equal: " ++ show(user == user2))  # true
}

Practical Example: Parsing API Responses

Combine HTTP requests with fromJson for type-safe API consumption.

type IpResponse = { origin: String }

main() = {
    (status, resp): (String, HttpResponse) = Http.get("https://httpbin.org/ip")

    if status == "ok" then {
        json: Json = jsonParse(resp.body)
        ipResp: IpResponse = fromJson[IpResponse](json)
        println("Your IP: " ++ ipResp.origin)
    } else {
        println("Failed to fetch IP")
    }
}