957 Json Query
Tutorial
The Problem
Implement path-based JSON querying: given a path like ["users", "0", "name"], traverse a JsonValue tree and return a borrowed reference to the value at that path. Array indices are specified as stringified integers. Implement typed extractors (get_string, get_number, get_bool) that further pattern-match the result.
🎯 Learning Outcomes
get<'a>(path: &[&str], json: &'a JsonValue) -> Option<&'a JsonValue> with lifetime annotations[] (empty) and [key, rest @ ..] (head and tail) for recursive path traversalObject lookup (pairs.iter().find(|(k, _)| k == key)) and Array index access (key.parse::<usize>().ok())get with a pattern match on the result variant'a ties the returned reference to the input jsonCode Example
#![allow(clippy::all)]
// 957: JSON Query by Path
// get(["users", "0", "name"], json) → Option<&JsonValue>
// Rust uses lifetime-annotated references; OCaml returns values directly
#[derive(Debug, Clone, PartialEq)]
pub enum JsonValue {
Null,
Bool(bool),
Number(f64),
Str(String),
Array(Vec<JsonValue>),
Object(Vec<(String, JsonValue)>),
}
// Approach 1: Path query returning Option<&JsonValue> (borrows from source)
pub fn get<'a>(path: &[&str], json: &'a JsonValue) -> Option<&'a JsonValue> {
match path {
[] => Some(json),
[key, rest @ ..] => match json {
JsonValue::Object(pairs) => {
let found = pairs.iter().find(|(k, _)| k == key);
found.and_then(|(_, v)| get(rest, v))
}
JsonValue::Array(items) => {
let idx: usize = key.parse().ok()?;
items.get(idx).and_then(|v| get(rest, v))
}
_ => None,
},
}
}
// Approach 2: Typed extractors (return borrowed inner values)
pub fn get_string<'a>(path: &[&str], json: &'a JsonValue) -> Option<&'a str> {
match get(path, json) {
Some(JsonValue::Str(s)) => Some(s.as_str()),
_ => None,
}
}
pub fn get_number(path: &[&str], json: &JsonValue) -> Option<f64> {
match get(path, json) {
Some(JsonValue::Number(n)) => Some(*n),
_ => None,
}
}
pub fn get_bool(path: &[&str], json: &JsonValue) -> Option<bool> {
match get(path, json) {
Some(JsonValue::Bool(b)) => Some(*b),
_ => None,
}
}
pub fn get_array<'a>(path: &[&str], json: &'a JsonValue) -> Option<&'a Vec<JsonValue>> {
match get(path, json) {
Some(JsonValue::Array(items)) => Some(items),
_ => None,
}
}
// Approach 3: Query with default (clones for ownership)
pub fn get_or(default: JsonValue, path: &[&str], json: &JsonValue) -> JsonValue {
match get(path, json) {
Some(v) => v.clone(),
None => default,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_json() -> JsonValue {
JsonValue::Object(vec![
(
"users".to_string(),
JsonValue::Array(vec![
JsonValue::Object(vec![
("name".to_string(), JsonValue::Str("Alice".to_string())),
("age".to_string(), JsonValue::Number(30.0)),
("active".to_string(), JsonValue::Bool(true)),
]),
JsonValue::Object(vec![
("name".to_string(), JsonValue::Str("Bob".to_string())),
("age".to_string(), JsonValue::Number(25.0)),
("active".to_string(), JsonValue::Bool(false)),
]),
]),
),
("count".to_string(), JsonValue::Number(2.0)),
(
"meta".to_string(),
JsonValue::Object(vec![
("version".to_string(), JsonValue::Str("1.0".to_string())),
("tag".to_string(), JsonValue::Null),
]),
),
])
}
#[test]
fn test_basic_queries() {
let json = make_json();
assert_eq!(get(&["count"], &json), Some(&JsonValue::Number(2.0)));
assert_eq!(
get(&["users", "0", "name"], &json),
Some(&JsonValue::Str("Alice".to_string()))
);
assert_eq!(
get(&["users", "1", "name"], &json),
Some(&JsonValue::Str("Bob".to_string()))
);
assert_eq!(get(&["meta", "tag"], &json), Some(&JsonValue::Null));
}
#[test]
fn test_missing_paths() {
let json = make_json();
assert_eq!(get(&["missing"], &json), None);
assert_eq!(get(&["users", "5", "name"], &json), None);
assert_eq!(get(&["users", "0", "missing"], &json), None);
}
#[test]
fn test_typed_extractors() {
let json = make_json();
assert_eq!(get_string(&["users", "0", "name"], &json), Some("Alice"));
assert_eq!(get_number(&["count"], &json), Some(2.0));
assert_eq!(get_bool(&["users", "0", "active"], &json), Some(true));
assert_eq!(get_bool(&["users", "1", "active"], &json), Some(false));
}
#[test]
fn test_empty_path_returns_root() {
let json = make_json();
assert_eq!(get(&[], &json), Some(&json));
}
#[test]
fn test_get_or_default() {
let json = make_json();
let result = get_or(JsonValue::Str("default".into()), &["missing"], &json);
assert_eq!(result, JsonValue::Str("default".to_string()));
let result2 = get_or(JsonValue::Null, &["count"], &json);
assert_eq!(result2, JsonValue::Number(2.0));
}
}Key Differences
| Aspect | Rust | OCaml |
|---|---|---|
| Return type | Option<&'a JsonValue> — borrowed reference with lifetime | json option — GC-managed value |
| Path destructuring | [key, rest @ ..] slice pattern | key :: rest list pattern |
| Object lookup | iter().find() | List.assoc_opt |
| Array index | key.parse::<usize>().ok()? | int_of_string_opt + bounds check |
| Lifetime annotation | Required to express borrow through recursion | Not needed |
The lifetime annotation is not extra complexity — it is the compiler making an implicit contract explicit. The returned reference is "borrowed from json for 'a", so the caller cannot mutate or drop json while holding the reference.
OCaml Approach
let rec get path json =
match path with
| [] -> Some json
| key :: rest ->
match json with
| Object pairs ->
(match List.assoc_opt key pairs with
| Some v -> get rest v
| None -> None)
| Array items ->
(match int_of_string_opt key with
| Some idx when idx >= 0 && idx < List.length items ->
get rest (List.nth items idx)
| _ -> None)
| _ -> None
let get_string path json =
match get path json with
| Some (Str s) -> Some s
| _ -> None
OCaml's List.assoc_opt key pairs looks up a key in an association list — directly replacing the find + and_then chain. OCaml does not need explicit lifetime annotations; the GC manages value lifetimes transparently.
Full Source
#![allow(clippy::all)]
// 957: JSON Query by Path
// get(["users", "0", "name"], json) → Option<&JsonValue>
// Rust uses lifetime-annotated references; OCaml returns values directly
#[derive(Debug, Clone, PartialEq)]
pub enum JsonValue {
Null,
Bool(bool),
Number(f64),
Str(String),
Array(Vec<JsonValue>),
Object(Vec<(String, JsonValue)>),
}
// Approach 1: Path query returning Option<&JsonValue> (borrows from source)
pub fn get<'a>(path: &[&str], json: &'a JsonValue) -> Option<&'a JsonValue> {
match path {
[] => Some(json),
[key, rest @ ..] => match json {
JsonValue::Object(pairs) => {
let found = pairs.iter().find(|(k, _)| k == key);
found.and_then(|(_, v)| get(rest, v))
}
JsonValue::Array(items) => {
let idx: usize = key.parse().ok()?;
items.get(idx).and_then(|v| get(rest, v))
}
_ => None,
},
}
}
// Approach 2: Typed extractors (return borrowed inner values)
pub fn get_string<'a>(path: &[&str], json: &'a JsonValue) -> Option<&'a str> {
match get(path, json) {
Some(JsonValue::Str(s)) => Some(s.as_str()),
_ => None,
}
}
pub fn get_number(path: &[&str], json: &JsonValue) -> Option<f64> {
match get(path, json) {
Some(JsonValue::Number(n)) => Some(*n),
_ => None,
}
}
pub fn get_bool(path: &[&str], json: &JsonValue) -> Option<bool> {
match get(path, json) {
Some(JsonValue::Bool(b)) => Some(*b),
_ => None,
}
}
pub fn get_array<'a>(path: &[&str], json: &'a JsonValue) -> Option<&'a Vec<JsonValue>> {
match get(path, json) {
Some(JsonValue::Array(items)) => Some(items),
_ => None,
}
}
// Approach 3: Query with default (clones for ownership)
pub fn get_or(default: JsonValue, path: &[&str], json: &JsonValue) -> JsonValue {
match get(path, json) {
Some(v) => v.clone(),
None => default,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_json() -> JsonValue {
JsonValue::Object(vec![
(
"users".to_string(),
JsonValue::Array(vec![
JsonValue::Object(vec![
("name".to_string(), JsonValue::Str("Alice".to_string())),
("age".to_string(), JsonValue::Number(30.0)),
("active".to_string(), JsonValue::Bool(true)),
]),
JsonValue::Object(vec![
("name".to_string(), JsonValue::Str("Bob".to_string())),
("age".to_string(), JsonValue::Number(25.0)),
("active".to_string(), JsonValue::Bool(false)),
]),
]),
),
("count".to_string(), JsonValue::Number(2.0)),
(
"meta".to_string(),
JsonValue::Object(vec![
("version".to_string(), JsonValue::Str("1.0".to_string())),
("tag".to_string(), JsonValue::Null),
]),
),
])
}
#[test]
fn test_basic_queries() {
let json = make_json();
assert_eq!(get(&["count"], &json), Some(&JsonValue::Number(2.0)));
assert_eq!(
get(&["users", "0", "name"], &json),
Some(&JsonValue::Str("Alice".to_string()))
);
assert_eq!(
get(&["users", "1", "name"], &json),
Some(&JsonValue::Str("Bob".to_string()))
);
assert_eq!(get(&["meta", "tag"], &json), Some(&JsonValue::Null));
}
#[test]
fn test_missing_paths() {
let json = make_json();
assert_eq!(get(&["missing"], &json), None);
assert_eq!(get(&["users", "5", "name"], &json), None);
assert_eq!(get(&["users", "0", "missing"], &json), None);
}
#[test]
fn test_typed_extractors() {
let json = make_json();
assert_eq!(get_string(&["users", "0", "name"], &json), Some("Alice"));
assert_eq!(get_number(&["count"], &json), Some(2.0));
assert_eq!(get_bool(&["users", "0", "active"], &json), Some(true));
assert_eq!(get_bool(&["users", "1", "active"], &json), Some(false));
}
#[test]
fn test_empty_path_returns_root() {
let json = make_json();
assert_eq!(get(&[], &json), Some(&json));
}
#[test]
fn test_get_or_default() {
let json = make_json();
let result = get_or(JsonValue::Str("default".into()), &["missing"], &json);
assert_eq!(result, JsonValue::Str("default".to_string()));
let result2 = get_or(JsonValue::Null, &["count"], &json);
assert_eq!(result2, JsonValue::Number(2.0));
}
}#[cfg(test)]
mod tests {
use super::*;
fn make_json() -> JsonValue {
JsonValue::Object(vec![
(
"users".to_string(),
JsonValue::Array(vec![
JsonValue::Object(vec![
("name".to_string(), JsonValue::Str("Alice".to_string())),
("age".to_string(), JsonValue::Number(30.0)),
("active".to_string(), JsonValue::Bool(true)),
]),
JsonValue::Object(vec![
("name".to_string(), JsonValue::Str("Bob".to_string())),
("age".to_string(), JsonValue::Number(25.0)),
("active".to_string(), JsonValue::Bool(false)),
]),
]),
),
("count".to_string(), JsonValue::Number(2.0)),
(
"meta".to_string(),
JsonValue::Object(vec![
("version".to_string(), JsonValue::Str("1.0".to_string())),
("tag".to_string(), JsonValue::Null),
]),
),
])
}
#[test]
fn test_basic_queries() {
let json = make_json();
assert_eq!(get(&["count"], &json), Some(&JsonValue::Number(2.0)));
assert_eq!(
get(&["users", "0", "name"], &json),
Some(&JsonValue::Str("Alice".to_string()))
);
assert_eq!(
get(&["users", "1", "name"], &json),
Some(&JsonValue::Str("Bob".to_string()))
);
assert_eq!(get(&["meta", "tag"], &json), Some(&JsonValue::Null));
}
#[test]
fn test_missing_paths() {
let json = make_json();
assert_eq!(get(&["missing"], &json), None);
assert_eq!(get(&["users", "5", "name"], &json), None);
assert_eq!(get(&["users", "0", "missing"], &json), None);
}
#[test]
fn test_typed_extractors() {
let json = make_json();
assert_eq!(get_string(&["users", "0", "name"], &json), Some("Alice"));
assert_eq!(get_number(&["count"], &json), Some(2.0));
assert_eq!(get_bool(&["users", "0", "active"], &json), Some(true));
assert_eq!(get_bool(&["users", "1", "active"], &json), Some(false));
}
#[test]
fn test_empty_path_returns_root() {
let json = make_json();
assert_eq!(get(&[], &json), Some(&json));
}
#[test]
fn test_get_or_default() {
let json = make_json();
let result = get_or(JsonValue::Str("default".into()), &["missing"], &json);
assert_eq!(result, JsonValue::Str("default".to_string()));
let result2 = get_or(JsonValue::Null, &["count"], &json);
assert_eq!(result2, JsonValue::Number(2.0));
}
}
Deep Comparison
JSON Query by Path — Comparison
Core Insight
Recursive path traversal is the same algorithm in both languages. The critical difference: OCaml's GC makes returning values trivial (Some v), while Rust must track where the returned data lives using lifetime annotations (Option<&'a JsonValue>). The borrow is more efficient (no copy) but requires explicit lifetime reasoning.
OCaml Approach
List.assoc_opt key pairs finds a key in association list, returning OptionList.nth items i indexes into a list (O(n) — fine for small arrays)int_of_string_opt safely parses array indicesmatch path, json with cleanly handles all combinationsSome j — a GC-managed copy (or shared immutable value)Rust Approach
pairs.iter().find(|(k, _)| k == key) searches Vec of pairsitems.get(idx) bounds-checked index returning Option<&T>key.parse::<usize>().ok() for index parsing[key, rest @ ..] for head/tail deconstructionOption<&'a JsonValue> — a borrowed reference, zero-copy'a lifetime links output reference to input referenceComparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Return type | json option | Option<&'a JsonValue> |
| Memory model | GC, shared immutable | Borrow, zero-copy, explicit lifetime |
| Assoc list lookup | List.assoc_opt key pairs | pairs.iter().find(\|(k,_)\| k==key) |
| Array index | List.nth items i | items.get(idx) |
| Index parsing | int_of_string_opt | key.parse::<usize>().ok() |
| Path deconstruction | key :: rest | [key, rest @ ..] slice pattern |
| Chaining options | match ... with \| Some v -> get rest v | .and_then(\|v\| get(rest, v)) |
Exercises
get_mut<'a>(path: &[&str], json: &'a mut JsonValue) -> Option<&'a mut JsonValue> to allow mutation at a path.set(path: &[&str], json: &mut JsonValue, value: JsonValue) that inserts/replaces a value at a path.delete(path: &[&str], json: &mut JsonValue) -> bool that removes a key or array element.query_all(key: &str, json: &JsonValue) -> Vec<&JsonValue> that finds all values with matching key at any depth."users[0].name" into a Vec<&str> slice and use it with get.