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