Skip to content

Decoding external data

The ard/decode module provides a flexible, type-safe way to decode external data from multiple sources (JSON, SQLite query results, etc.) without requiring predefined struct definitions. It uses composable decoder functions inspired by Gleam’s approach.

Unlike ard/json which requires defining structs upfront, the decode library lets you work with arbitrary JSON structures and extract only the data you need.

use ard/decode
// Parse any JSON string into Dynamic data
let data = decode::any("{\"name\": \"Alice\", \"age\": 30}")
// Extract specific values with type safety
let name = decode::run(data, decode::field("name", decode::string)).expect("")
let age = decode::run(data, decode::field("age", decode::int)).expect("")

The Dynamic type represents external, untyped data (JSON, database rows, etc.):

let data = decode::any("{\"users\": [\"Alice\", \"Bob\"]}") // Dynamic

Decoders are functions that transform Dynamic data into typed Ard values:

type Decoder($T) = fn(Dynamic) $T![Error]
use ard/decode
let data = decode::any("\"hello\"")
let text = decode::run(data, decode::string).expect("") // "hello"
let data = decode::any("42")
let number = decode::run(data, decode::int).expect("") // 42
let data = decode::any("3.14")
let pi = decode::run(data, decode::float).expect("") // 3.14
let data = decode::any("true")
let active = decode::run(data, decode::bool).expect("") // true

The decode::from_json() function parses JSON strings into Dynamic data:

use ard/decode
// Parse a JSON string
let data = decode::from_json("{\"name\": \"Alice\", \"age\": 30}").expect("Invalid JSON")

The decode::run() function applies a decoder to dynamic data:

fn run(data: Dynamic, decoder: Decoder($T)) $T![Error]

The power of the decode library comes from composing simple decoders into complex ones.

Handle optional or null values using nullable():

let data = decode::any("null")
let maybe_text = decode::run(data, decode::nullable(decode::string)).expect("")
let text = maybe_text.or("default") // "default"
let data = decode::any("\"hello\"")
let maybe_text = decode::run(data, decode::nullable(decode::string)).expect("")
let text = maybe_text.or("default") // "hello"

Decode entire arrays using list():

let data = decode::from_json("[1, 2, 3, 4, 5]").expect("Invalid JSON")
let numbers = decode::run(data, decode::list(decode::int)).expect("")
numbers.size() // 5

Extract a specific element from an array using at():

let data = decode::from_json("[\"first\", \"second\", \"third\"]").expect("Invalid JSON")
// Decode the element at index 0
let first = decode::run(data, decode::at(0, decode::string)).expect("")
first // "first"

Decode objects with flexible key and value types using map():

let data = decode::any("\{\"name\": \"Alice\", \"city\": \"Boston\"\}")
// String keys to string values
let info = decode::run(data, decode::map(decode::string, decode::string)).expect("")
info.get("name").or("unknown") // "Alice"

Extract specific fields from objects using field():

let json = "\{\"user\": \{\"name\": \"Alice\", \"age\": 30\}\}"
let data = decode::from_json(json).expect("Invalid JSON")
// Extract nested field
let name = decode::run(data,
decode::field("user",
decode::field("name", decode::string)
)
).expect("") // "Alice"

For deeply nested fields, use path() to specify a list of keys:

let json = "\{\"user\": \{\"profile\": \{\"name\": \"Alice\"\}\}\}"
let data = decode::from_json(json).expect("Invalid JSON")
// Navigate deeply nested paths with a single list of keys
let name = decode::run(data, decode::path(["user", "profile", "name"], decode::string)).expect("")

The one_of() function allows trying multiple decoders in sequence until one succeeds. This is useful when external data may be in different formats but you need a single output type:

use ard/decode
fn int_to_str(d: Dynamic) Str![decode::Error] {
let num = try decode::int(d)
Result::ok(num.to_str())
}
// Try string first, then try an int as a backup
let decode_as_string = decode::one_of(decode::string, [int_to_str])
let data1 = decode::from_json("\"hello\"").expect("Invalid JSON")
let result1 = decode_as_string(data1).expect("") // "hello"
let data2 = decode::from_json("42").expect("Invalid JSON")
let result2 = decode_as_string(data2).expect("") // "42"

Combine decoders for complex data structures:

use ard/decode
let json = "\{\"users\": [\{\"name\": \"Alice\", \"active\": true\}, \{\"name\": \"Bob\", \"active\": null\}]\}"
let data = decode::any(json)
// Decode array of user objects
let user_decoder = decode::map(decode::string, decode::nullable(decode::bool))
let users = decode::run(data, decode::field("users", decode::list(user_decoder))).expect("")
// Access user data
let first_user = users.at(0)
let is_active = first_user.get("active").or(maybe::some(false))

The decode library provides detailed error information:

let data = decode::any("\{\"age\": \"not_a_number\"\}")
let result = decode::run(data, decode::field("age", decode::int))
match result {
ok(age) => io::print("Age: {age}"),
err(errors) => {
let first_error = errors.at(0)
io::print("Expected: {first_error.expected}") // "Int"
io::print("Found: {first_error.found}") // "Dynamic"
io::print("Path: {first_error.path}") // ["age"]
}
}

Here’s how to process API responses without predefined structs:

use ard/http
use ard/decode
use ard/io
fn fetch_pokemon() {
let response = http::get("https://pokeapi.co/api/v2/pokemon").expect("Request failed")
if response.is_ok() {
// Extract count
let count = decode::run(response.body, decode::field("count", decode::int))
match count {
ok(n) => io::print("Total Pokemon: {n}"),
err(_) => io::print("Could not extract count")
}
// Extract results array
let results_decoder = decode::list(decode::map(decode::string, decode::string))
let results = decode::run(response.body, decode::field("results", results_decoder))
match results {
ok(pokemon_list) => io::print("Found {pokemon_list.size()} Pokemon"),
err(_) => io::print("Could not extract results")
}
}
}
  • No struct definitions required - work with arbitrary JSON
  • Type safety - decoders ensure correct types
  • Composable - build complex decoders from simple ones
  • Flexible - extract only what you need
  • Error-friendly - detailed error messages with path information
  • Extensible - works with JSON, database rows, and other external data

The decode library is perfect for working with APIs, configuration files, and any situation where you need flexible JSON processing without rigid type definitions.