facet_deserialize/
error.rs

1#[cfg(feature = "rich-diagnostics")]
2use ariadne::{Color, Config, IndexType, Label, Report, ReportKind, Source};
3
4use alloc::string::String;
5
6use facet_core::{Shape, Type, UserType};
7use facet_reflect::{ReflectError, VariantError};
8use owo_colors::OwoColorize;
9
10use crate::{Outcome, Span};
11
12/// A JSON parse error, with context. Never would've guessed huh.
13pub struct DeserError<'input, 'shape> {
14    /// The input associated with the error.
15    pub input: alloc::borrow::Cow<'input, [u8]>,
16
17    /// Where the error occured
18    pub span: Span,
19
20    /// The specific error that occurred while parsing the JSON.
21    pub kind: DeserErrorKind<'shape>,
22
23    /// The source identifier for error reporting
24    pub source_id: &'static str,
25}
26
27impl<'input, 'shape> DeserError<'input, 'shape> {
28    /// Converts the error into an owned error.
29    pub fn into_owned(self) -> DeserError<'static, 'shape> {
30        DeserError {
31            input: self.input.into_owned().into(),
32            span: self.span,
33            kind: self.kind,
34            source_id: self.source_id,
35        }
36    }
37
38    /// Sets the span of this error
39    pub fn with_span(self, span: Span) -> DeserError<'input, 'shape> {
40        DeserError {
41            input: self.input,
42            span,
43            kind: self.kind,
44            source_id: self.source_id,
45        }
46    }
47}
48
49/// An error kind for JSON parsing.
50#[derive(Debug, Clone)]
51pub enum DeserErrorKind<'shape> {
52    /// An unexpected byte was encountered in the input.
53    UnexpectedByte {
54        /// The byte that was found.
55        got: u8,
56        /// The expected value as a string description.
57        wanted: &'static str,
58    },
59
60    /// An unexpected character was encountered in the input.
61    UnexpectedChar {
62        /// The character that was found.
63        got: char,
64        /// The expected value as a string description.
65        wanted: &'static str,
66    },
67
68    /// An unexpected outcome was encountered in the input.
69    UnexpectedOutcome {
70        /// The outcome that was found.
71        got: Outcome<'static>,
72        /// The expected value as a string description.
73        wanted: &'static str,
74    },
75
76    /// The input ended unexpectedly while parsing JSON.
77    UnexpectedEof {
78        /// The expected value as a string description.
79        wanted: &'static str,
80    },
81
82    /// Indicates a value was expected to follow an element in the input.
83    MissingValue {
84        /// Describes what type of value was expected.
85        expected: &'static str,
86        /// The element that requires the missing value.
87        field: String,
88    },
89
90    /// A required struct field was missing at the end of JSON input.
91    MissingField(&'static str),
92
93    /// A number is out of range.
94    NumberOutOfRange(f64),
95
96    /// An unexpected String was encountered in the input.
97    StringAsNumber(String),
98
99    /// An unexpected field name was encountered in the input.
100    UnknownField {
101        /// The name of the field that was not recognized
102        field_name: String,
103
104        /// The shape definition where the unknown field was encountered
105        shape: &'shape Shape<'shape>,
106    },
107
108    /// A string that could not be built into valid UTF-8 Unicode
109    InvalidUtf8(String),
110
111    /// An error occurred while reflecting a type.
112    ReflectError(ReflectError<'shape>),
113
114    /// Some feature is not yet implemented (under development).
115    Unimplemented(&'static str),
116
117    /// An unsupported type was encountered.
118    UnsupportedType {
119        /// The shape we got
120        got: &'shape Shape<'shape>,
121
122        /// The shape we wanted
123        wanted: &'static str,
124    },
125
126    /// An enum variant name that doesn't exist in the enum definition.
127    NoSuchVariant {
128        /// The name of the variant that was not found
129        name: String,
130
131        /// The enum shape definition where the variant was looked up
132        enum_shape: &'shape Shape<'shape>,
133    },
134
135    /// An error occurred when reflecting an enum variant (index) from a user type.
136    VariantError(VariantError),
137
138    /// Too many elements for an array.
139    ArrayOverflow {
140        /// The array shape
141        shape: &'shape Shape<'shape>,
142
143        /// Maximum allowed length
144        max_len: usize,
145    },
146
147    /// Failed to convert numeric type.
148    NumericConversion {
149        /// Source type name
150        from: &'static str,
151
152        /// Target type name
153        to: &'static str,
154    },
155}
156
157impl<'input, 'shape> DeserError<'input, 'shape> {
158    /// Creates a new deser error, preserving input and location context for accurate reporting.
159    pub fn new(
160        kind: DeserErrorKind<'shape>,
161        input: &'input [u8],
162        span: Span,
163        source_id: &'static str,
164    ) -> Self {
165        Self {
166            input: input.into(),
167            span,
168            kind,
169            source_id,
170        }
171    }
172
173    /// Constructs a reflection-related deser error, keeping contextual information intact.
174    pub(crate) fn new_reflect(
175        e: ReflectError<'shape>,
176        input: &'input [u8],
177        span: Span,
178        source_id: &'static str,
179    ) -> Self {
180        DeserError::new(DeserErrorKind::ReflectError(e), input, span, source_id)
181    }
182
183    /// Sets the source ID for this error
184    pub fn with_source_id(mut self, source_id: &'static str) -> Self {
185        self.source_id = source_id;
186        self
187    }
188
189    /// Provides a human-friendly message wrapper to improve error readability.
190    pub fn message(&self) -> DeserErrorMessage<'_, '_> {
191        DeserErrorMessage(self)
192    }
193}
194
195/// A wrapper type for displaying deser error messages
196pub struct DeserErrorMessage<'input, 'shape>(&'input DeserError<'input, 'shape>);
197
198impl core::fmt::Display for DeserErrorMessage<'_, '_> {
199    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
200        match &self.0.kind {
201            DeserErrorKind::UnexpectedByte { got, wanted } => write!(
202                f,
203                "Unexpected byte: got 0x{:02X}, wanted {}",
204                got.red(),
205                wanted.yellow()
206            ),
207            DeserErrorKind::UnexpectedChar { got, wanted } => write!(
208                f,
209                "Unexpected character: got '{}', wanted {}",
210                got.red(),
211                wanted.yellow()
212            ),
213            DeserErrorKind::UnexpectedOutcome { got, wanted } => {
214                write!(f, "Unexpected {}, wanted {}", got.red(), wanted.yellow())
215            }
216            DeserErrorKind::UnexpectedEof { wanted } => {
217                write!(f, "Unexpected end of file: wanted {}", wanted.red())
218            }
219            DeserErrorKind::MissingValue { expected, field } => {
220                write!(f, "Missing {} for {}", expected.red(), field.yellow())
221            }
222            DeserErrorKind::MissingField(fld) => write!(f, "Missing required field: {}", fld.red()),
223            DeserErrorKind::NumberOutOfRange(n) => {
224                write!(f, "Number out of range: {}", n.red())
225            }
226            DeserErrorKind::StringAsNumber(s) => {
227                write!(f, "Expected a string but got number: {}", s.red())
228            }
229            DeserErrorKind::UnknownField { field_name, shape } => {
230                write!(
231                    f,
232                    "Unknown field: {} for shape {}",
233                    field_name.red(),
234                    shape.yellow()
235                )
236            }
237            DeserErrorKind::InvalidUtf8(e) => write!(f, "Invalid UTF-8 encoding: {}", e.red()),
238            DeserErrorKind::ReflectError(e) => write!(f, "{e}"),
239            DeserErrorKind::Unimplemented(s) => {
240                write!(f, "Feature not yet implemented: {}", s.yellow())
241            }
242            DeserErrorKind::UnsupportedType { got, wanted } => {
243                write!(
244                    f,
245                    "Unsupported type: got {}, wanted {}",
246                    got.red(),
247                    wanted.green()
248                )
249            }
250            DeserErrorKind::NoSuchVariant { name, enum_shape } => {
251                if let Type::User(UserType::Enum(ed)) = enum_shape.ty {
252                    write!(
253                        f,
254                        "Enum variant not found: {} in enum {}. Available variants: [",
255                        name.red(),
256                        enum_shape.yellow()
257                    )?;
258
259                    let mut first = true;
260                    for variant in ed.variants.iter() {
261                        if !first {
262                            write!(f, ", ")?;
263                        }
264                        write!(f, "{}", variant.name.green())?;
265                        first = false;
266                    }
267
268                    write!(f, "]")?;
269                    Ok(())
270                } else {
271                    write!(
272                        f,
273                        "Enum variant not found: {} in non-enum type {}",
274                        name.red(),
275                        enum_shape.yellow()
276                    )?;
277                    Ok(())
278                }
279            }
280            DeserErrorKind::VariantError(e) => {
281                write!(f, "Variant error: {e}")
282            }
283            DeserErrorKind::ArrayOverflow { shape, max_len } => {
284                write!(
285                    f,
286                    "Too many elements for array {}: maximum {} elements allowed",
287                    shape.blue(),
288                    max_len.yellow()
289                )
290            }
291            DeserErrorKind::NumericConversion { from, to } => {
292                write!(
293                    f,
294                    "Cannot convert {} to {}: value out of range or precision loss",
295                    from.red(),
296                    to.green()
297                )
298            }
299        }
300    }
301}
302
303#[cfg(not(feature = "rich-diagnostics"))]
304impl core::fmt::Display for DeserError<'_, '_> {
305    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
306        write!(f, "{} at byte {}", self.message(), self.span.start(),)
307    }
308}
309
310#[cfg(feature = "rich-diagnostics")]
311impl core::fmt::Display for DeserError<'_, '_> {
312    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
313        // Try to convert input to utf8 for source display, otherwise fallback to error
314        let Ok(orig_input_str) = core::str::from_utf8(&self.input[..]) else {
315            return write!(f, "(JSON input was invalid UTF-8)");
316        };
317
318        let source_id = self.source_id;
319        let mut span_start = self.span.start();
320        let mut span_end = self.span.end();
321        use alloc::borrow::Cow;
322        let mut input_str: Cow<'_, str> = Cow::Borrowed(orig_input_str);
323
324        // --- Context-sensitive truncation logic ---
325        // When the error occurs very far into a huge (often one-line) input,
326        // such as minified JSON, it's annoying to display hundreds or thousands of
327        // preceding and trailing characters. Instead, we seek to trim the displayed
328        // "source" to just enough around the offending line/location, but only if
329        // we can do this cleanly.
330        //
331        // Our approach:
332        // - Find the full line that `span_start` is within, using memchr for newlines before and after.
333        // - Only proceed if both `span_start` and `span_end` are within this line (i.e., error doesn't span lines).
334        // - If there are more than 180 characters before/after the span on this line, truncate to show
335        //   "...<80 chars>SPANTEXT<80 chars>..." and adjust the display offsets to ensure ariadne points
336        //   to the correct span inside the trimmed display.
337        //
338        // Rationale: this avoids a sea of whitespace for extremely long lines (common in compact JSON).
339
340        let mut did_truncate = false;
341
342        {
343            // Find the line bounds containing span_start
344            let bytes = self.input.as_ref();
345            let line_start = bytes[..span_start]
346                .iter()
347                .rposition(|&b| b == b'\n')
348                .map(|pos| pos + 1)
349                .unwrap_or(0);
350            let line_end = bytes[span_start..]
351                .iter()
352                .position(|&b| b == b'\n')
353                .map(|pos| span_start + pos)
354                .unwrap_or(bytes.len());
355
356            // Check if span fits within one line
357            if span_end <= line_end {
358                // How much context do we have before and after the span in this line?
359                let before_chars = span_start - line_start;
360                let after_chars = line_end.saturating_sub(span_end);
361
362                // Only trim if context is long enough
363                if before_chars > 180 || after_chars > 180 {
364                    let trim_left = if before_chars > 180 {
365                        before_chars - 80
366                    } else {
367                        0
368                    };
369                    let trim_right = if after_chars > 180 {
370                        after_chars - 80
371                    } else {
372                        0
373                    };
374
375                    let new_start = line_start + trim_left;
376                    let new_end = line_end - trim_right;
377
378                    let truncated = &orig_input_str[new_start..new_end];
379
380                    let left_ellipsis = if trim_left > 0 { "…" } else { "" };
381                    let right_ellipsis = if trim_right > 0 { "…" } else { "" };
382
383                    let mut buf = String::with_capacity(
384                        left_ellipsis.len() + truncated.len() + right_ellipsis.len(),
385                    );
386                    buf.push_str(left_ellipsis);
387                    buf.push_str(truncated);
388                    buf.push_str(right_ellipsis);
389
390                    // Adjust span offsets to align with the trimmed string
391                    span_start = span_start - new_start + left_ellipsis.len();
392                    span_end = span_end - new_start + left_ellipsis.len();
393
394                    input_str = Cow::Owned(buf);
395
396                    did_truncate = true; // mark that truncation occurred
397                    // Done!
398                }
399            }
400            // If the span goes across lines or we cannot cleanly trim, display the full input as fallback
401        }
402
403        if did_truncate {
404            writeln!(
405                f,
406                "{}",
407                "WARNING: Input was truncated for display. Byte indexes in the error below do not match original input.".yellow().bold()
408            )?;
409        }
410
411        let mut report = Report::build(ReportKind::Error, (source_id, span_start..span_end))
412            .with_config(Config::new().with_index_type(IndexType::Byte));
413
414        let label = Label::new((source_id, span_start..span_end))
415            .with_message(self.message())
416            .with_color(Color::Red);
417
418        report = report.with_label(label);
419
420        let source = Source::from(input_str);
421
422        struct FmtWriter<'a, 'b: 'a> {
423            f: &'a mut core::fmt::Formatter<'b>,
424            error: Option<core::fmt::Error>,
425        }
426
427        impl core::fmt::Write for FmtWriter<'_, '_> {
428            fn write_str(&mut self, s: &str) -> core::fmt::Result {
429                if self.error.is_some() {
430                    // Already failed, do nothing
431                    return Err(core::fmt::Error);
432                }
433                if let Err(e) = self.f.write_str(s) {
434                    self.error = Some(e);
435                    Err(core::fmt::Error)
436                } else {
437                    Ok(())
438                }
439            }
440        }
441
442        struct IoWriter<'a, 'b: 'a> {
443            inner: FmtWriter<'a, 'b>,
444        }
445
446        impl std::io::Write for IoWriter<'_, '_> {
447            fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
448                match core::str::from_utf8(buf) {
449                    Ok(s) => match core::fmt::Write::write_str(&mut self.inner, s) {
450                        Ok(()) => Ok(buf.len()),
451                        Err(_) => Err(std::io::ErrorKind::Other.into()),
452                    },
453                    Err(_) => Err(std::io::ErrorKind::InvalidData.into()),
454                }
455            }
456            fn flush(&mut self) -> std::io::Result<()> {
457                Ok(())
458            }
459        }
460
461        let cache = (source_id, &source);
462
463        let fmt_writer = FmtWriter { f, error: None };
464        let mut io_writer = IoWriter { inner: fmt_writer };
465
466        if report.finish().write(cache, &mut io_writer).is_err() {
467            return write!(f, "Error formatting with ariadne");
468        }
469
470        // Check if our adapter ran into a formatting error
471        if io_writer.inner.error.is_some() {
472            return write!(f, "Error writing ariadne output to fmt::Formatter");
473        }
474
475        Ok(())
476    }
477}
478
479impl core::fmt::Debug for DeserError<'_, '_> {
480    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
481        core::fmt::Display::fmt(self, f)
482    }
483}
484
485impl core::error::Error for DeserError<'_, '_> {}