Skip to main content

lc_core/output_parsers/
json_parser.rs

1use async_trait::async_trait;
2use futures_util::Stream;
3use std::pin::Pin;
4
5use super::base::{BaseOutputParser, OutputParserError, OutputParserResult};
6use crate::language_models::LLMResult;
7use crate::runnables::{Runnable, RunnableConfig};
8use crate::structured_output::parser::PartialJsonParser;
9
10/// JSON output parser
11///
12/// Parses the LLM's JSON string output into a `serde_json::Value`.
13/// Supports:
14/// - standard JSON parsing
15/// - extracting JSON from a Markdown code block
16/// - optional partial-JSON parsing (for streaming scenarios)
17///
18/// Equivalent to Python LangChain's `JsonOutputParser`.
19///
20/// # Example
21/// ```ignore
22/// use langchainrust::output_parsers::JsonOutputParser;
23/// use serde_json::json;
24///
25/// let parser = JsonOutputParser::new();
26/// let result = parser.parse(r#"{"name": "Rust", "year": 2015}"#).await?;
27/// assert_eq!(result["name"], "Rust");
28/// ```
29pub struct JsonOutputParser {
30    /// Whether partial-JSON parsing is allowed (for streaming scenarios)
31    partial: bool,
32}
33
34impl JsonOutputParser {
35    /// Creates a standard JSON output parser.
36    pub fn new() -> Self {
37        Self { partial: false }
38    }
39
40    /// Creates a parser that supports partial-JSON parsing
41    ///
42    /// In streaming scenarios the LLM may emit incomplete JSON;
43    /// with this option enabled it tries to parse as much data out as possible.
44    pub fn new_partial() -> Self {
45        Self { partial: true }
46    }
47
48    /// Extracts the JSON string from text
49    ///
50    /// Strips the Markdown code block ```json ... ``` and leading/trailing text, returning the actual JSON value.
51    /// More robust than the old `find("```")` matching: an unclosed fence (opening only, no closing) is stripped
52    /// correctly, and a complete fenced JSON is not misread as a parse failure.
53    fn extract_json_str<'a>(&self, text: &'a str) -> OutputParserResult<&'a str> {
54        let json = PartialJsonParser::strip_markdown_fence(text);
55        if json.is_empty() {
56            // no JSON structural chars: let the caller's serde report the error (avoid treating an empty string as a valid value)
57            Ok(text.trim())
58        } else {
59            Ok(json.trim())
60        }
61    }
62}
63
64impl Default for JsonOutputParser {
65    fn default() -> Self {
66        Self::new()
67    }
68}
69
70/// Takes the first `max_chars` chars of a string for an error preview.
71///
72/// Byte truncation cannot be used: a multi-byte UTF-8 char would be cut mid-char, panicking on slicing
73/// (the error path for invalid CJK JSON once crashed truncating at 200 bytes).
74fn preview_slice(s: &str, max_chars: usize) -> &str {
75    match s.char_indices().nth(max_chars) {
76        // the start byte of the max_chars-th char is a safe boundary; slicing to it keeps the first max_chars chars
77        Some((i, _)) => &s[..i],
78        None => s,
79    }
80}
81
82#[async_trait]
83impl BaseOutputParser<serde_json::Value> for JsonOutputParser {
84    async fn parse(&self, text: &str) -> OutputParserResult<serde_json::Value> {
85        let json_str = self.extract_json_str(text)?;
86
87        if self.partial {
88            self.parse_partial_json(json_str)
89        } else {
90            serde_json::from_str(json_str).map_err(|e| {
91                OutputParserError::JsonError(format!(
92                    "JSON parse failed (position {}:{}): {}, input: {}",
93                    e.line(),
94                    e.column(),
95                    e,
96                    preview_slice(json_str, 200)
97                ))
98            })
99        }
100    }
101
102    fn get_format_instructions(&self) -> String {
103        "请使用 JSON 格式输出,例如:{\"key\": \"value\"}。确保 JSON 是合法的。".to_string()
104    }
105}
106
107impl JsonOutputParser {
108    /// Tries to parse partial (incomplete) JSON
109    ///
110    /// In streaming LLM output the progressively accumulated JSON may be incomplete.
111    /// This method attempts to extract as much data from it as possible.
112    fn parse_partial_json(&self, text: &str) -> OutputParserResult<serde_json::Value> {
113        // first try a complete parse
114        if let Ok(value) = serde_json::from_str::<serde_json::Value>(text) {
115            return Ok(value);
116        }
117
118        // try to repair common incomplete JSON patterns
119        let repaired = self.repair_partial_json(text);
120        if let Ok(value) = serde_json::from_str::<serde_json::Value>(&repaired) {
121            return Ok(value);
122        }
123
124        Err(OutputParserError::JsonError(format!(
125            "partial JSON parse failed: {}",
126            preview_slice(text, 200)
127        )))
128    }
129
130    /// Repairs an incomplete JSON string
131    ///
132    /// Handles common incomplete JSON forms, such as:
133    /// - a trailing extra comma
134    /// - an incomplete string
135    /// - an incomplete object/array
136    fn repair_partial_json(&self, text: &str) -> String {
137        let mut repaired = text.trim().to_string();
138
139        // handle an unclosed string ending in `"` (drop the last incomplete token)
140        if let Some(stripped) = Self::strip_incomplete_token(&repaired) {
141            repaired = stripped;
142        }
143
144        // Scan tracking string state to correctly count braces/brackets
145        let mut in_string = false;
146        let mut escape_next = false;
147        let mut open_braces = 0usize;
148        let mut close_braces = 0usize;
149        let mut open_brackets = 0usize;
150        let mut close_brackets = 0usize;
151
152        for ch in repaired.chars() {
153            if escape_next {
154                escape_next = false;
155                continue;
156            }
157            if ch == '\\' && in_string {
158                escape_next = true;
159                continue;
160            }
161            if ch == '"' {
162                in_string = !in_string;
163                continue;
164            }
165            if !in_string {
166                match ch {
167                    '{' => open_braces += 1,
168                    '}' => close_braces += 1,
169                    '[' => open_brackets += 1,
170                    ']' => close_brackets += 1,
171                    _ => {}
172                }
173            }
174        }
175
176        // close the unclosed braces
177        for _ in close_braces..open_braces {
178            repaired.push('}');
179        }
180
181        for _ in close_brackets..open_brackets {
182            repaired.push(']');
183        }
184
185        // ensure the string ends with a quote (if a string was opened)
186        // Scan forward (not backward) to find unclosed strings (M31)
187        let mut in_string = false;
188        let mut escape_next = false;
189        let mut last_open_quote_pos: Option<usize> = None;
190
191        for (i, ch) in repaired.char_indices() {
192            if escape_next {
193                escape_next = false;
194                continue;
195            }
196            if ch == '\\' && in_string {
197                escape_next = true;
198                continue;
199            }
200            if ch == '"' {
201                if in_string {
202                    in_string = false;
203                    last_open_quote_pos = None;
204                } else {
205                    in_string = true;
206                    last_open_quote_pos = Some(i);
207                }
208                continue;
209            }
210        }
211
212        // If we're still in a string at the end, truncate at the opening quote
213        // and close it, or just close it if the string value is partially complete
214        if in_string {
215            // Check if the unclosed string contains a newline (invalid in JSON strings)
216            if let Some(open_pos) = last_open_quote_pos {
217                let after_quote = &repaired[open_pos + 1..];
218                if after_quote.contains('\n') {
219                    // Truncate at the newline and close the string
220                    if let Some(pos) = repaired[open_pos + 1..].find('\n') {
221                        let newline_pos = pos + open_pos + 1;
222                        repaired.truncate(newline_pos);
223                        repaired.push('"');
224                    }
225                }
226            }
227        }
228
229        repaired
230    }
231
232    /// Drops an incomplete trailing token
233    fn strip_incomplete_token(s: &str) -> Option<String> {
234        let trimmed = s.trim_end();
235
236        // If the string ends with an incomplete key or value token,
237        // try to find the last complete token boundary.
238        // Look for the last structural character (: , { [ }) and truncate after it.
239        let chars: Vec<char> = trimmed.chars().collect();
240        if chars.is_empty() {
241            return None;
242        }
243
244        // Scan backwards to find the last structural boundary
245        let mut i = chars.len();
246        while i > 0 {
247            i -= 1;
248            match chars[i] {
249                ',' | ':' | '{' | '[' | '}' | ']' => {
250                    // Found a structural character; truncate after it
251                    let truncate_at: usize = trimmed
252                        .char_indices()
253                        .nth(i + 1)
254                        .map(|(pos, _)| pos)
255                        .unwrap_or(trimmed.len());
256                    if truncate_at < s.len() {
257                        let result = trimmed[..truncate_at].to_string();
258                        if result != s.trim_end() {
259                            return Some(result);
260                        }
261                    }
262                    return None;
263                }
264                '"' => {
265                    // Check if this is a closing quote (even number of quotes before it)
266                    // If so, the JSON might be complete at this point
267                    return None;
268                }
269                _ => {}
270            }
271        }
272
273        None
274    }
275}
276
277#[async_trait]
278impl Runnable<LLMResult, serde_json::Value> for JsonOutputParser {
279    type Error = OutputParserError;
280
281    async fn invoke(
282        &self,
283        input: LLMResult,
284        _config: Option<RunnableConfig>,
285    ) -> Result<serde_json::Value, Self::Error> {
286        self.parse(&input.content).await
287    }
288
289    async fn stream(
290        &self,
291        input: LLMResult,
292        _config: Option<RunnableConfig>,
293    ) -> Result<
294        Pin<Box<dyn Stream<Item = Result<serde_json::Value, Self::Error>> + Send>>,
295        Self::Error,
296    > {
297        let result = self.parse(&input.content).await?;
298        let stream = futures_util::stream::once(async move { Ok(result) });
299        Ok(Box::pin(stream))
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[tokio::test]
308    async fn test_json_parser_standard_obj() {
309        let parser = JsonOutputParser::new();
310        let result = parser
311            .parse(r#"{"name": "Rust", "year": 2015}"#)
312            .await
313            .unwrap();
314        assert_eq!(result["name"], "Rust");
315        assert_eq!(result["year"], 2015);
316    }
317
318    #[tokio::test]
319    async fn test_json_parser_from_markdown_block() {
320        let parser = JsonOutputParser::new();
321        let input = "以下是结果:\n```json\n{\"status\": \"ok\"}\n```\n";
322        let result = parser.parse(input).await.unwrap();
323        assert_eq!(result["status"], "ok");
324    }
325
326    #[tokio::test]
327    async fn test_json_parser_array() {
328        let parser = JsonOutputParser::new();
329        let result = parser.parse("[1, 2, 3]").await.unwrap();
330        assert_eq!(result[0], 1);
331        assert_eq!(result[2], 3);
332    }
333
334    #[tokio::test]
335    async fn test_json_parser_from_markdown_block_unclosed_fence() {
336        // H4: only the opening ```json without the closing ``` (truncated model output) must also strip the fence and parse
337        let parser = JsonOutputParser::new();
338        let input = "以下是结果:\n```json\n{\"status\": \"ok\"}";
339        let result = parser.parse(input).await.unwrap();
340        assert_eq!(result["status"], "ok");
341    }
342
343    #[tokio::test]
344    async fn test_json_parser_from_prose_prefix() {
345        // H4: a prose prefix ("result:") before the JSON must also be stripped
346        let parser = JsonOutputParser::new();
347        let input = "结果是:\n{\"a\": 1}\n以上";
348        let result = parser.parse(input).await.unwrap();
349        assert_eq!(result["a"], 1);
350    }
351
352    #[tokio::test]
353    async fn test_json_parser_invalid_json() {
354        let parser = JsonOutputParser::new();
355        let result = parser.parse("{invalid}").await;
356        assert!(result.is_err());
357    }
358
359    #[tokio::test]
360    async fn test_json_parser_format_instructions() {
361        let parser = JsonOutputParser::new();
362        let instructions = parser.get_format_instructions();
363        assert!(!instructions.is_empty());
364    }
365
366    #[tokio::test]
367    async fn test_json_parser_invoke_runnable() {
368        // Runnable form takes an LLMResult and parses its content field
369        let parser = JsonOutputParser::new();
370        let result = parser
371            .invoke(
372                LLMResult {
373                    content: r#"{"key": "value"}"#.to_string(),
374                    ..Default::default()
375                },
376                None,
377            )
378            .await
379            .unwrap();
380        assert_eq!(result["key"], "value");
381    }
382
383    #[tokio::test]
384    async fn test_json_parser_partial_success() {
385        let parser = JsonOutputParser::new_partial();
386        // complete JSON: partial mode must also parse it
387        let result = parser.parse(r#"{"a": 1}"#).await.unwrap();
388        assert_eq!(result["a"], 1);
389    }
390
391    #[tokio::test]
392    async fn test_json_parser_invalid_cjk_over_200_bytes() {
393        // >200-byte invalid CJK JSON: if the error path truncated at 200 bytes it would panic mid multi-byte char;
394        // after the fix it returns Err instead of crashing
395        let parser = JsonOutputParser::new();
396        let long_cjk = "汉".repeat(200);
397        let bad = format!("{{\"名字\": {}", long_cjk);
398        let result = parser.parse(&bad).await;
399        assert!(result.is_err());
400    }
401
402    #[tokio::test]
403    async fn test_json_parser_partial_invalid_cjk_over_200_bytes() {
404        // partial mode uses the same error-preview truncation; must also not panic
405        let parser = JsonOutputParser::new_partial();
406        let long_cjk = "汉".repeat(200);
407        let bad = format!("{{\"名字\": {}", long_cjk);
408        let result = parser.parse(&bad).await;
409        assert!(result.is_err());
410    }
411}