Skip to main content

ferrin_schema/
partial_json.rs

1//! Repair and parse truncated JSON.
2//!
3//! Streaming models emit JSON text incrementally. [`repair`] closes open
4//! strings, arrays and objects, completes partial literals (`tru` → `true`)
5//! and drops trailing separators so that any prefix of a valid document
6//! becomes parseable. [`parse_partial`] tries a direct parse first and falls
7//! back to repair.
8//!
9//! Derived from the `fixJson` state machine of the Vercel AI SDK (Apache-2.0,
10//! Copyright 2023 Vercel, Inc.), translated from TypeScript to Rust and
11//! modified; see `NOTICE`.
12
13use std::borrow::Cow;
14
15use serde_json::Value;
16
17/// Outcome of [`parse_partial`].
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct PartialParse {
20    /// The parsed value, if parsing (possibly after repair) succeeded.
21    pub value: Option<Value>,
22    /// How the value was obtained.
23    pub state: PartialParseState,
24}
25
26/// How a partial parse succeeded or failed.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28#[non_exhaustive]
29pub enum PartialParseState {
30    /// No input was provided.
31    UndefinedInput,
32    /// The input parsed as-is.
33    SuccessfulParse,
34    /// The input parsed after repair.
35    RepairedParse,
36    /// The input could not be parsed even after repair.
37    FailedParse,
38}
39
40/// Parses optional JSON text, distinguishing absent input from invalid JSON.
41///
42/// # Examples
43///
44/// ```
45/// use ferrin_schema::partial_json::{parse_optional, PartialParseState};
46///
47/// assert_eq!(parse_optional(None).state, PartialParseState::UndefinedInput);
48/// assert_eq!(parse_optional(Some("null")).state, PartialParseState::SuccessfulParse);
49/// ```
50#[must_use]
51pub fn parse_optional(text: Option<&str>) -> PartialParse {
52    text.map_or(
53        PartialParse {
54            value: None,
55            state: PartialParseState::UndefinedInput,
56        },
57        parse_partial,
58    )
59}
60
61/// Parses `text`, repairing it first if a direct parse fails.
62#[must_use]
63pub fn parse_partial(text: &str) -> PartialParse {
64    if let Ok(value) = crate::json::parse(text) {
65        return PartialParse {
66            value: Some(value),
67            state: PartialParseState::SuccessfulParse,
68        };
69    }
70    let repaired = repair(text);
71    match crate::json::parse(&repaired) {
72        Ok(value) => PartialParse {
73            value: Some(value),
74            state: PartialParseState::RepairedParse,
75        },
76        Err(_) => PartialParse {
77            value: None,
78            state: PartialParseState::FailedParse,
79        },
80    }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84enum State {
85    Root,
86    Finish,
87    InsideString,
88    InsideStringEscape,
89    InsideStringUnicodeEscape,
90    InsideLiteral,
91    InsideNumber,
92    InsideObjectStart,
93    InsideObjectKey,
94    InsideObjectKeyEscape,
95    InsideObjectAfterKey,
96    InsideObjectBeforeValue,
97    InsideObjectAfterValue,
98    InsideObjectAfterComma,
99    InsideArrayStart,
100    InsideArrayAfterValue,
101    InsideArrayAfterComma,
102}
103
104struct Repairer<'a> {
105    input: &'a str,
106    stack: Vec<State>,
107    /// Byte index one past the last character that belongs to valid output.
108    last_valid_end: usize,
109    literal_start: usize,
110    unicode_escape_digits: u8,
111    unicode_escape_value: u16,
112    pending_high_surrogate: bool,
113}
114
115impl Repairer<'_> {
116    fn top(&self) -> State {
117        // The stack always holds at least `Root`/`Finish`.
118        self.stack.last().copied().unwrap_or(State::Finish)
119    }
120
121    fn replace_top(&mut self, state: State) {
122        self.stack.pop();
123        self.stack.push(state);
124    }
125
126    fn process_value_start(&mut self, ch: char, start: usize, end: usize, swap: State) {
127        match ch {
128            '"' => {
129                self.last_valid_end = end;
130                self.replace_top(swap);
131                self.stack.push(State::InsideString);
132            }
133            'f' | 't' | 'n' => {
134                self.last_valid_end = end;
135                self.literal_start = start;
136                self.replace_top(swap);
137                self.stack.push(State::InsideLiteral);
138            }
139            '-' => {
140                self.replace_top(swap);
141                self.stack.push(State::InsideNumber);
142            }
143            '0'..='9' => {
144                self.last_valid_end = end;
145                self.replace_top(swap);
146                self.stack.push(State::InsideNumber);
147            }
148            '{' => {
149                self.last_valid_end = end;
150                self.replace_top(swap);
151                self.stack.push(State::InsideObjectStart);
152            }
153            '[' => {
154                self.last_valid_end = end;
155                self.replace_top(swap);
156                self.stack.push(State::InsideArrayStart);
157            }
158            _ => {}
159        }
160    }
161
162    fn process_after_object_value(&mut self, ch: char, end: usize) {
163        match ch {
164            ',' => self.replace_top(State::InsideObjectAfterComma),
165            '}' => {
166                self.last_valid_end = end;
167                self.stack.pop();
168            }
169            _ => {}
170        }
171    }
172
173    fn process_after_array_value(&mut self, ch: char, end: usize) {
174        match ch {
175            ',' => self.replace_top(State::InsideArrayAfterComma),
176            ']' => {
177                self.last_valid_end = end;
178                self.stack.pop();
179            }
180            _ => {}
181        }
182    }
183
184    fn step(&mut self, ch: char, start: usize, end: usize) {
185        match self.top() {
186            State::Root => self.process_value_start(ch, start, end, State::Finish),
187            State::Finish => {}
188            State::InsideObjectStart => match ch {
189                '"' => self.replace_top(State::InsideObjectKey),
190                '}' => {
191                    self.last_valid_end = end;
192                    self.stack.pop();
193                }
194                _ => {}
195            },
196            State::InsideObjectAfterComma => {
197                if ch == '"' {
198                    self.replace_top(State::InsideObjectKey);
199                }
200            }
201            State::InsideObjectKey => match ch {
202                '"' => self.replace_top(State::InsideObjectAfterKey),
203                '\\' => self.stack.push(State::InsideObjectKeyEscape),
204                _ => {}
205            },
206            State::InsideObjectKeyEscape => {
207                self.stack.pop();
208            }
209            State::InsideObjectAfterKey => {
210                if ch == ':' {
211                    self.replace_top(State::InsideObjectBeforeValue);
212                }
213            }
214            State::InsideObjectBeforeValue => {
215                self.process_value_start(ch, start, end, State::InsideObjectAfterValue);
216            }
217            State::InsideObjectAfterValue => self.process_after_object_value(ch, end),
218            State::InsideString => match ch {
219                '"' => {
220                    self.stack.pop();
221                    self.last_valid_end = end;
222                }
223                '\\' => self.stack.push(State::InsideStringEscape),
224                _ => self.last_valid_end = end,
225            },
226            State::InsideArrayStart => {
227                if ch == ']' {
228                    self.last_valid_end = end;
229                    self.stack.pop();
230                } else {
231                    // Whitespace before the first element is valid output; a
232                    // lone `-` is not (it would leave `[-]`).
233                    if ch != '-' {
234                        self.last_valid_end = end;
235                    }
236                    self.process_value_start(ch, start, end, State::InsideArrayAfterValue);
237                }
238            }
239            State::InsideArrayAfterValue => match ch {
240                ',' => self.replace_top(State::InsideArrayAfterComma),
241                ']' => {
242                    self.last_valid_end = end;
243                    self.stack.pop();
244                }
245                _ => self.last_valid_end = end,
246            },
247            State::InsideArrayAfterComma => {
248                self.process_value_start(ch, start, end, State::InsideArrayAfterValue);
249            }
250            State::InsideStringEscape => {
251                self.stack.pop();
252                if ch == 'u' {
253                    self.unicode_escape_digits = 0;
254                    self.unicode_escape_value = 0;
255                    self.stack.push(State::InsideStringUnicodeEscape);
256                } else {
257                    self.last_valid_end = end;
258                }
259            }
260            State::InsideStringUnicodeEscape => {
261                if let Some(digit) = ch.to_digit(16) {
262                    self.unicode_escape_value = (self.unicode_escape_value << 4) | digit as u16;
263                    self.unicode_escape_digits += 1;
264                    if self.unicode_escape_digits == 4 {
265                        self.stack.pop();
266                        if (0xD800..=0xDBFF).contains(&self.unicode_escape_value) {
267                            // A high surrogate alone is not a Unicode scalar.
268                            self.pending_high_surrogate = true;
269                        } else if !self.pending_high_surrogate
270                            || (0xDC00..=0xDFFF).contains(&self.unicode_escape_value)
271                        {
272                            self.pending_high_surrogate = false;
273                            self.last_valid_end = end;
274                        }
275                    }
276                }
277            }
278            State::InsideNumber => match ch {
279                '0'..='9' => self.last_valid_end = end,
280                'e' | 'E' | '-' | '+' | '.' => {}
281                ',' => {
282                    self.stack.pop();
283                    if self.top() == State::InsideArrayAfterValue {
284                        self.process_after_array_value(ch, end);
285                    }
286                    if self.top() == State::InsideObjectAfterValue {
287                        self.process_after_object_value(ch, end);
288                    }
289                }
290                '}' => {
291                    self.stack.pop();
292                    if self.top() == State::InsideObjectAfterValue {
293                        self.process_after_object_value(ch, end);
294                    }
295                }
296                ']' => {
297                    self.stack.pop();
298                    if self.top() == State::InsideArrayAfterValue {
299                        self.process_after_array_value(ch, end);
300                    }
301                }
302                _ => {
303                    self.stack.pop();
304                }
305            },
306            State::InsideLiteral => {
307                let partial = &self.input[self.literal_start..end];
308                if !"false".starts_with(partial)
309                    && !"true".starts_with(partial)
310                    && !"null".starts_with(partial)
311                {
312                    self.stack.pop();
313                    if self.top() == State::InsideObjectAfterValue {
314                        self.process_after_object_value(ch, end);
315                    } else if self.top() == State::InsideArrayAfterValue {
316                        self.process_after_array_value(ch, end);
317                    }
318                } else {
319                    self.last_valid_end = end;
320                }
321            }
322        }
323    }
324
325    fn finish(self) -> String {
326        let mut result = String::with_capacity(self.last_valid_end + self.stack.len());
327        result.push_str(&self.input[..self.last_valid_end]);
328        for state in self.stack.iter().rev() {
329            match state {
330                State::InsideString => result.push('"'),
331                State::InsideObjectKey
332                | State::InsideObjectAfterKey
333                | State::InsideObjectAfterComma
334                | State::InsideObjectStart
335                | State::InsideObjectBeforeValue
336                | State::InsideObjectAfterValue => result.push('}'),
337                State::InsideArrayStart
338                | State::InsideArrayAfterComma
339                | State::InsideArrayAfterValue => result.push(']'),
340                State::InsideLiteral => {
341                    let partial = &self.input[self.literal_start..];
342                    for literal in ["true", "false", "null"] {
343                        if let Some(rest) = literal.strip_prefix(partial) {
344                            result.push_str(rest);
345                            break;
346                        }
347                    }
348                }
349                State::Root
350                | State::Finish
351                | State::InsideStringEscape
352                | State::InsideObjectKeyEscape
353                | State::InsideStringUnicodeEscape
354                | State::InsideNumber => {}
355            }
356        }
357        result
358    }
359}
360
361/// Repairs truncated JSON so that it parses.
362///
363/// Returns the input unchanged (borrowed) when no repair was needed.
364#[must_use]
365pub fn repair(input: &str) -> Cow<'_, str> {
366    let mut repairer = Repairer {
367        input,
368        stack: vec![State::Root],
369        last_valid_end: 0,
370        literal_start: 0,
371        unicode_escape_digits: 0,
372        unicode_escape_value: 0,
373        pending_high_surrogate: false,
374    };
375    for (start, ch) in input.char_indices() {
376        let end = start + ch.len_utf8();
377        repairer.step(ch, start, end);
378    }
379    let result = repairer.finish();
380    if result == input {
381        Cow::Borrowed(input)
382    } else {
383        Cow::Owned(result)
384    }
385}