ExamplesBy LevelBy TopicLearning Paths
957 Fundamental

957 Json Query

Functional Programming

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

  • • Implement get<'a>(path: &[&str], json: &'a JsonValue) -> Option<&'a JsonValue> with lifetime annotations
  • • Use slice patterns [] (empty) and [key, rest @ ..] (head and tail) for recursive path traversal
  • • Handle both Object lookup (pairs.iter().find(|(k, _)| k == key)) and Array index access (key.parse::<usize>().ok())
  • • Implement typed extractors that combine get with a pattern match on the result variant
  • • Understand why Rust's lifetime 'a ties the returned reference to the input json
  • Code 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

    AspectRustOCaml
    Return typeOption<&'a JsonValue> — borrowed reference with lifetimejson option — GC-managed value
    Path destructuring[key, rest @ ..] slice patternkey :: rest list pattern
    Object lookupiter().find()List.assoc_opt
    Array indexkey.parse::<usize>().ok()?int_of_string_opt + bounds check
    Lifetime annotationRequired to express borrow through recursionNot 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));
        }
    }
    ✓ Tests Rust test suite
    #[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 Option
  • List.nth items i indexes into a list (O(n) — fine for small arrays)
  • int_of_string_opt safely parses array indices
  • • Recursive match path, json with cleanly handles all combinations
  • • Returns Some j — a GC-managed copy (or shared immutable value)
  • Rust Approach

  • pairs.iter().find(|(k, _)| k == key) searches Vec of pairs
  • items.get(idx) bounds-checked index returning Option<&T>
  • key.parse::<usize>().ok() for index parsing
  • • Slice pattern [key, rest @ ..] for head/tail deconstruction
  • • Returns Option<&'a JsonValue> — a borrowed reference, zero-copy
  • 'a lifetime links output reference to input reference
  • Comparison Table

    AspectOCamlRust
    Return typejson optionOption<&'a JsonValue>
    Memory modelGC, shared immutableBorrow, zero-copy, explicit lifetime
    Assoc list lookupList.assoc_opt key pairspairs.iter().find(\|(k,_)\| k==key)
    Array indexList.nth items iitems.get(idx)
    Index parsingint_of_string_optkey.parse::<usize>().ok()
    Path deconstructionkey :: rest[key, rest @ ..] slice pattern
    Chaining optionsmatch ... with \| Some v -> get rest v.and_then(\|v\| get(rest, v))

    Exercises

  • Implement get_mut<'a>(path: &[&str], json: &'a mut JsonValue) -> Option<&'a mut JsonValue> to allow mutation at a path.
  • Implement set(path: &[&str], json: &mut JsonValue, value: JsonValue) that inserts/replaces a value at a path.
  • Implement delete(path: &[&str], json: &mut JsonValue) -> bool that removes a key or array element.
  • Implement query_all(key: &str, json: &JsonValue) -> Vec<&JsonValue> that finds all values with matching key at any depth.
  • Parse a JSON path string like "users[0].name" into a Vec<&str> slice and use it with get.
  • Open Source Repos