Skip to main content

json_pluck/
lib.rs

1//! # json-pluck
2//!
3//! Pluck a single value out of a `serde_json::Value` by dotted path or
4//! a tiny subset of JSONPath. Lossy and forgiving — built for the kind
5//! of LLM-emitted JSON where the answer is buried under unpredictable
6//! wrappers.
7//!
8//! Supported path syntax:
9//!
10//! - `a.b.c` — nested object access
11//! - `a.b[0]` — array indexing
12//! - `a.b[-1]` — negative array indexing (-1 = last)
13//! - `**.answer` — first match anywhere under the root (BFS)
14//!
15//! ## Example
16//!
17//! ```
18//! use json_pluck::pluck;
19//! use serde_json::json;
20//!
21//! let v = json!({
22//!     "result": {
23//!         "items": [
24//!             { "value": 1 },
25//!             { "value": 42 }
26//!         ]
27//!     }
28//! });
29//! assert_eq!(pluck(&v, "result.items[1].value"), Some(&json!(42)));
30//! assert_eq!(pluck(&v, "result.items[-1].value"), Some(&json!(42)));
31//! assert_eq!(pluck(&v, "**.value"), Some(&json!(1)));
32//! ```
33
34#![deny(missing_docs)]
35
36use serde_json::Value;
37
38/// Pluck a single value by `path`.
39pub fn pluck<'a>(root: &'a Value, path: &str) -> Option<&'a Value> {
40    // BFS path: `**.field` — find the first field named `field` anywhere
41    // under `root`.
42    if let Some(name) = path.strip_prefix("**.") {
43        return bfs_first(root, name);
44    }
45    let mut cur = root;
46    for seg in split_path(path) {
47        cur = step(cur, &seg)?;
48    }
49    Some(cur)
50}
51
52/// Mutable variant.
53pub fn pluck_mut<'a>(root: &'a mut Value, path: &str) -> Option<&'a mut Value> {
54    if path.starts_with("**.") {
55        // BFS-mut is awkward in safe Rust; do an immutable lookup, then
56        // re-descend by the discovered path. For simplicity we only
57        // support deterministic paths in the mutable variant.
58        return None;
59    }
60    let mut cur = root;
61    for seg in split_path(path) {
62        cur = step_mut(cur, &seg)?;
63    }
64    Some(cur)
65}
66
67#[derive(Debug, Clone, PartialEq)]
68enum Seg {
69    Key(String),
70    Index(i64),
71}
72
73fn split_path(path: &str) -> Vec<Seg> {
74    // Tokenize on `.` and `[N]`. Robust enough for the dotted+bracket
75    // subset documented above.
76    let mut out = Vec::new();
77    let mut buf = String::new();
78    let mut chars = path.chars().peekable();
79    while let Some(c) = chars.next() {
80        match c {
81            '.' => {
82                if !buf.is_empty() {
83                    out.push(Seg::Key(std::mem::take(&mut buf)));
84                }
85            }
86            '[' => {
87                if !buf.is_empty() {
88                    out.push(Seg::Key(std::mem::take(&mut buf)));
89                }
90                let mut num = String::new();
91                while let Some(&next) = chars.peek() {
92                    if next == ']' {
93                        chars.next();
94                        break;
95                    }
96                    num.push(next);
97                    chars.next();
98                }
99                if let Ok(n) = num.parse::<i64>() {
100                    out.push(Seg::Index(n));
101                }
102            }
103            _ => buf.push(c),
104        }
105    }
106    if !buf.is_empty() {
107        out.push(Seg::Key(buf));
108    }
109    out
110}
111
112fn step<'a>(v: &'a Value, seg: &Seg) -> Option<&'a Value> {
113    match seg {
114        Seg::Key(k) => v.as_object()?.get(k),
115        Seg::Index(i) => {
116            let arr = v.as_array()?;
117            let idx = if *i < 0 {
118                (arr.len() as i64 + *i) as usize
119            } else {
120                *i as usize
121            };
122            arr.get(idx)
123        }
124    }
125}
126
127fn step_mut<'a>(v: &'a mut Value, seg: &Seg) -> Option<&'a mut Value> {
128    match seg {
129        Seg::Key(k) => v.as_object_mut()?.get_mut(k),
130        Seg::Index(i) => {
131            let arr = v.as_array_mut()?;
132            let idx = if *i < 0 {
133                (arr.len() as i64 + *i) as usize
134            } else {
135                *i as usize
136            };
137            arr.get_mut(idx)
138        }
139    }
140}
141
142fn bfs_first<'a>(root: &'a Value, name: &str) -> Option<&'a Value> {
143    let mut queue: std::collections::VecDeque<&Value> = std::collections::VecDeque::new();
144    queue.push_back(root);
145    while let Some(v) = queue.pop_front() {
146        match v {
147            Value::Object(map) => {
148                if let Some(hit) = map.get(name) {
149                    return Some(hit);
150                }
151                for (_, child) in map {
152                    queue.push_back(child);
153                }
154            }
155            Value::Array(arr) => {
156                for child in arr {
157                    queue.push_back(child);
158                }
159            }
160            _ => {}
161        }
162    }
163    None
164}