Skip to main content

qubit_json/decode/
json_decode_error.rs

1// =============================================================================
2//    Copyright (c) 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Defines the shared error returned by all JSON decoding facades.
9
10use std::error::Error;
11use std::fmt;
12use std::io;
13use std::sync::Arc;
14
15use qubit_budget::MeasuredBudgetError;
16use qubit_budget::ResourceQuantity;
17use qubit_budget::json::JsonResource;
18use serde_json::Error as JsonError;
19
20use super::DiagnosticPolicy;
21use super::JsonDecodeErrorKind;
22use super::JsonDecodeErrorSource;
23use super::JsonDecodeStage;
24use super::JsonMaterializationErrorReason;
25use super::JsonRootKind;
26use super::JsonSyntaxError;
27use crate::lexical::JsonLexicalFailure;
28
29/// Failure produced by either strict or normalizing JSON decoding.
30///
31/// Internal variants are private so callers branch through stable semantic
32/// accessors rather than depending on scanner, normalizer, or Serde details.
33///
34/// # Type Parameters
35///
36/// * `R` - Resource identity attached to budget failures.
37/// * `Q` - Quantity representation attached to budget failures.
38///
39/// # Examples
40///
41/// ```
42/// use qubit_json::decode::{JsonDecodeError, JsonDecodeErrorKind, JsonDecoder};
43/// use serde_json::Value;
44///
45/// let mut decoder = JsonDecoder::unlimited();
46/// let error: JsonDecodeError = decoder
47///     .decode_str::<Value>("")
48///     .expect_err("empty input must be rejected");
49/// assert_eq!(error.kind(), JsonDecodeErrorKind::InvalidJson);
50/// ```
51#[must_use]
52#[derive(Clone)]
53pub struct JsonDecodeError<R = JsonResource, Q = usize>
54where
55    Q: Copy + fmt::Debug,
56{
57    /// Policy controlling input-derived source retention.
58    diagnostic_policy: DiagnosticPolicy,
59    /// Safe reason for a unique-key intermediate-tree construction failure.
60    materialization_reason: Option<JsonMaterializationErrorReason>,
61    /// Mutually exclusive structured failure.
62    failure: JsonDecodeErrorSource<R, Q>,
63}
64
65impl<R, Q> JsonDecodeError<R, Q>
66where
67    Q: Copy + fmt::Debug,
68{
69    /// Creates a stream-read failure and conditionally retains its source.
70    ///
71    /// # Parameters
72    ///
73    /// * `source` - I/O error produced while reading the input.
74    /// * `raw_input_bytes` - Number of input bytes read before the failure.
75    /// * `diagnostic_policy` - Whether to retain the original error details.
76    ///
77    /// # Returns
78    ///
79    /// A read failure containing the I/O category and, under detailed
80    /// diagnostics, the original error.
81    pub(crate) fn read(source: io::Error, raw_input_bytes: usize, diagnostic_policy: DiagnosticPolicy) -> Self {
82        let kind = source.kind();
83        let source = (diagnostic_policy == DiagnosticPolicy::Detailed).then(|| Arc::new(source));
84        Self {
85            diagnostic_policy,
86            materialization_reason: None,
87            failure: JsonDecodeErrorSource::Read {
88                raw_input_bytes,
89                kind,
90                source,
91            },
92        }
93    }
94
95    /// Creates a measured-budget failure at a semantic stage.
96    ///
97    /// # Parameters
98    ///
99    /// * `source` - Resource measurement that exceeded its budget.
100    /// * `stage` - Decode stage where the budget was exceeded.
101    /// * `raw_input_bytes` - Original input length in bytes.
102    /// * `normalized_input_bytes` - Normalized input length, when available.
103    /// * `diagnostic_policy` - Policy controlling retained input details.
104    ///
105    /// # Returns
106    ///
107    /// A budget failure retaining the measured resource error and decode
108    /// stage.
109    pub(crate) const fn budget(
110        source: MeasuredBudgetError<R, Q>,
111        stage: JsonDecodeStage,
112        raw_input_bytes: usize,
113        normalized_input_bytes: Option<usize>,
114        diagnostic_policy: DiagnosticPolicy,
115    ) -> Self {
116        Self {
117            diagnostic_policy,
118            materialization_reason: None,
119            failure: JsonDecodeErrorSource::Budget {
120                stage,
121                raw_input_bytes,
122                normalized_input_bytes,
123                source,
124            },
125        }
126    }
127
128    /// Creates an empty-input failure at a semantic stage.
129    ///
130    /// # Parameters
131    ///
132    /// * `stage` - Decode stage where the empty input was detected.
133    /// * `raw_input_bytes` - Original input length in bytes.
134    /// * `normalized_input_bytes` - Normalized input length, when available.
135    /// * `diagnostic_policy` - Policy controlling retained input details.
136    ///
137    /// # Returns
138    ///
139    /// An empty-input failure retaining the stage and input lengths.
140    pub(crate) const fn empty_input(
141        stage: JsonDecodeStage,
142        raw_input_bytes: usize,
143        normalized_input_bytes: Option<usize>,
144        diagnostic_policy: DiagnosticPolicy,
145    ) -> Self {
146        Self {
147            diagnostic_policy,
148            materialization_reason: None,
149            failure: JsonDecodeErrorSource::EmptyInput {
150                stage,
151                raw_input_bytes,
152                normalized_input_bytes,
153            },
154        }
155    }
156
157    /// Creates an invalid-UTF-8 failure and conditionally retains its source.
158    ///
159    /// # Parameters
160    ///
161    /// * `source` - UTF-8 validation failure for the input bytes.
162    /// * `raw_input_bytes` - Original input length in bytes.
163    /// * `diagnostic_policy` - Whether to retain the original error details.
164    ///
165    /// # Returns
166    ///
167    /// An invalid-UTF-8 failure retaining the invalid byte position and, under
168    /// detailed diagnostics, the original validation error.
169    pub(crate) fn invalid_utf8(
170        source: std::str::Utf8Error,
171        raw_input_bytes: usize,
172        diagnostic_policy: DiagnosticPolicy,
173    ) -> Self {
174        let valid_up_to = source.valid_up_to();
175        let error_len = source.error_len();
176        let source = (diagnostic_policy == DiagnosticPolicy::Detailed).then_some(source);
177        Self {
178            diagnostic_policy,
179            materialization_reason: None,
180            failure: JsonDecodeErrorSource::InvalidUtf8 {
181                raw_input_bytes,
182                valid_up_to,
183                error_len,
184                source,
185            },
186        }
187    }
188
189    /// Creates a stable invalid-JSON failure from lexical admission.
190    ///
191    /// # Parameters
192    ///
193    /// * `syntax_source` - Lexical failure with stable syntax details.
194    /// * `detailed_source` - Optional parser error retained for detailed
195    ///   diagnostics.
196    /// * `raw_input_bytes` - Original input length in bytes.
197    /// * `normalized_input_bytes` - Normalized input length, when available.
198    /// * `diagnostic_policy` - Policy controlling retained input details.
199    ///
200    /// # Returns
201    ///
202    /// An invalid-JSON failure containing stable syntax details and the
203    /// optional detailed parser source.
204    pub(crate) fn invalid_json(
205        syntax_source: JsonLexicalFailure,
206        detailed_source: Option<Arc<dyn Error + Send + Sync>>,
207        raw_input_bytes: usize,
208        normalized_input_bytes: Option<usize>,
209        diagnostic_policy: DiagnosticPolicy,
210    ) -> Self {
211        Self {
212            diagnostic_policy,
213            materialization_reason: None,
214            failure: JsonDecodeErrorSource::InvalidJson {
215                raw_input_bytes,
216                normalized_input_bytes,
217                syntax: JsonSyntaxError::from_lexical(syntax_source),
218                source: detailed_source,
219            },
220        }
221    }
222
223    /// Creates an unexpected-top-level failure.
224    ///
225    /// # Parameters
226    ///
227    /// * `expected` - Required top-level JSON kind.
228    /// * `actual` - Observed top-level JSON kind.
229    /// * `raw_input_bytes` - Original input length in bytes.
230    /// * `normalized_input_bytes` - Normalized input length, when available.
231    /// * `diagnostic_policy` - Policy controlling retained input details.
232    ///
233    /// # Returns
234    ///
235    /// A top-level-kind failure retaining the expected and observed kinds.
236    pub(crate) const fn unexpected_top_level(
237        expected: JsonRootKind,
238        actual: JsonRootKind,
239        raw_input_bytes: usize,
240        normalized_input_bytes: Option<usize>,
241        diagnostic_policy: DiagnosticPolicy,
242    ) -> Self {
243        Self {
244            diagnostic_policy,
245            materialization_reason: None,
246            failure: JsonDecodeErrorSource::UnexpectedTopLevel {
247                raw_input_bytes,
248                normalized_input_bytes,
249                expected,
250                actual,
251            },
252        }
253    }
254
255    /// Creates a target-deserialization failure and conditionally retains its
256    /// input-derived source.
257    ///
258    /// # Parameters
259    ///
260    /// * `source` - Serde error produced while materializing the target value.
261    /// * `raw_input_bytes` - Original input length in bytes.
262    /// * `normalized_input_bytes` - Normalized input length, when available.
263    /// * `diagnostic_policy` - Whether to retain the original error details.
264    ///
265    /// # Returns
266    ///
267    /// A deserialization failure retaining the source location and, under
268    /// detailed diagnostics, the original Serde error.
269    pub(crate) fn deserialize(
270        source: JsonError,
271        raw_input_bytes: usize,
272        normalized_input_bytes: Option<usize>,
273        diagnostic_policy: DiagnosticPolicy,
274    ) -> Self {
275        let line = source.line();
276        let column = source.column();
277        let source =
278            (diagnostic_policy == DiagnosticPolicy::Detailed).then(|| Arc::new(source) as Arc<dyn Error + Send + Sync>);
279        Self {
280            diagnostic_policy,
281            materialization_reason: None,
282            failure: JsonDecodeErrorSource::Deserialize {
283                raw_input_bytes,
284                normalized_input_bytes,
285                line,
286                column,
287                source,
288            },
289        }
290    }
291
292    /// Creates a unique-key materialization failure with a safe subreason.
293    pub(crate) fn materialization(
294        source: JsonError,
295        reason: JsonMaterializationErrorReason,
296        raw_input_bytes: usize,
297        diagnostic_policy: DiagnosticPolicy,
298    ) -> Self {
299        let mut error = Self::deserialize(source, raw_input_bytes, None, diagnostic_policy);
300        error.materialization_reason = Some(reason);
301        error
302    }
303
304    /// Returns the stable failure category.
305    ///
306    /// # Returns
307    ///
308    /// The category describing which kind of decode operation failed.
309    #[must_use = "inspect or propagate the underlying decode error source"]
310    #[inline(always)]
311    pub const fn kind(&self) -> JsonDecodeErrorKind {
312        match self.failure {
313            JsonDecodeErrorSource::Read { .. } => JsonDecodeErrorKind::Read,
314            JsonDecodeErrorSource::Budget { .. } => JsonDecodeErrorKind::Budget,
315            JsonDecodeErrorSource::EmptyInput { .. } => JsonDecodeErrorKind::EmptyInput,
316            JsonDecodeErrorSource::InvalidUtf8 { .. } => JsonDecodeErrorKind::InvalidUtf8,
317            JsonDecodeErrorSource::InvalidJson { .. } => JsonDecodeErrorKind::InvalidJson,
318            JsonDecodeErrorSource::UnexpectedTopLevel { .. } => JsonDecodeErrorKind::UnexpectedTopLevel,
319            JsonDecodeErrorSource::Deserialize { .. } => JsonDecodeErrorKind::Deserialize,
320        }
321    }
322
323    /// Returns the safe reason for a unique-key intermediate-tree failure.
324    ///
325    /// Returns `Some(reason)` when unique-key tree construction failed because
326    /// of a duplicate key or a defensive unrepresentable admitted number.
327    /// The legacy depth-limit reason remains for source compatibility, but the
328    /// shared builder has no fixed container cap and configured depth failures
329    /// are reported as budget errors. Returns `None` for lexical, input,
330    /// budget, and target-deserialization
331    /// failures. The reason remains available under redacted diagnostics and
332    /// contains no input-derived text. Read it before consuming the error with
333    /// [`Self::into_source`].
334    ///
335    /// # Examples
336    ///
337    /// ```
338    /// use qubit_json::decode::{JsonDecoder, JsonMaterializationErrorReason};
339    /// use serde_json::Value;
340    ///
341    /// let mut decoder = JsonDecoder::unlimited();
342    /// let error = decoder
343    ///     .decode_unique_str::<Value>(r#"{"id":1,"id":2}"#)
344    ///     .expect_err("duplicate keys must be rejected");
345    /// assert_eq!(
346    ///     error.materialization_error_reason(),
347    ///     Some(JsonMaterializationErrorReason::DuplicateKey),
348    /// );
349    /// ```
350    #[inline(always)]
351    pub const fn materialization_error_reason(&self) -> Option<JsonMaterializationErrorReason> {
352        self.materialization_reason
353    }
354
355    /// Returns the semantic stage that produced the failure.
356    ///
357    /// # Returns
358    ///
359    /// The pipeline stage at which the failure was recorded.
360    #[must_use]
361    #[inline(always)]
362    pub const fn stage(&self) -> JsonDecodeStage {
363        match self.failure {
364            JsonDecodeErrorSource::Budget { stage, .. } | JsonDecodeErrorSource::EmptyInput { stage, .. } => stage,
365            JsonDecodeErrorSource::Read { .. } => JsonDecodeStage::Input,
366            JsonDecodeErrorSource::InvalidUtf8 { .. } => JsonDecodeStage::DecodeText,
367            JsonDecodeErrorSource::InvalidJson { .. } => JsonDecodeStage::Parse,
368            JsonDecodeErrorSource::UnexpectedTopLevel { .. } => JsonDecodeStage::TopLevelCheck,
369            JsonDecodeErrorSource::Deserialize { .. } => JsonDecodeStage::Deserialize,
370        }
371    }
372
373    /// Returns the diagnostic policy applied while constructing this error.
374    ///
375    /// # Returns
376    ///
377    /// The policy that determines whether input-derived details are retained.
378    #[must_use]
379    #[inline(always)]
380    pub const fn diagnostic_policy(&self) -> DiagnosticPolicy {
381        self.diagnostic_policy
382    }
383
384    /// Returns the raw input length observed at the decoding boundary.
385    ///
386    /// For slice-based entry points, this is the complete input slice length,
387    /// including when input admission rejects the slice before processing it.
388    /// For `decode_input`, this is the length of the prefix read and retained
389    /// before the error; a byte consumed only to probe past the input budget is
390    /// excluded. The value therefore does not universally represent bytes
391    /// charged to the budget or the session's cumulative input usage.
392    ///
393    /// # Returns
394    ///
395    /// The raw input length observed for the input that caused this error.
396    #[must_use]
397    #[inline(always)]
398    pub const fn raw_input_bytes(&self) -> usize {
399        match self.failure {
400            JsonDecodeErrorSource::Read { raw_input_bytes, .. }
401            | JsonDecodeErrorSource::Budget { raw_input_bytes, .. }
402            | JsonDecodeErrorSource::EmptyInput { raw_input_bytes, .. }
403            | JsonDecodeErrorSource::InvalidUtf8 { raw_input_bytes, .. }
404            | JsonDecodeErrorSource::InvalidJson { raw_input_bytes, .. }
405            | JsonDecodeErrorSource::UnexpectedTopLevel { raw_input_bytes, .. }
406            | JsonDecodeErrorSource::Deserialize { raw_input_bytes, .. } => raw_input_bytes,
407        }
408    }
409
410    /// Returns the predicted normalized text length when it is available.
411    ///
412    /// A value may be present even when a budget rejected the input before
413    /// normalized text was allocated or rewritten. This reports a target
414    /// length, not proof that a normalized document was produced or charged.
415    ///
416    /// # Returns
417    ///
418    /// `Some(length)` when normalization determined a target length, or
419    /// `None` when the failure occurred before that length was available.
420    #[inline(always)]
421    pub const fn normalized_input_bytes(&self) -> Option<usize> {
422        match self.failure {
423            JsonDecodeErrorSource::Budget {
424                normalized_input_bytes, ..
425            }
426            | JsonDecodeErrorSource::EmptyInput {
427                normalized_input_bytes, ..
428            }
429            | JsonDecodeErrorSource::InvalidJson {
430                normalized_input_bytes, ..
431            }
432            | JsonDecodeErrorSource::UnexpectedTopLevel {
433                normalized_input_bytes, ..
434            }
435            | JsonDecodeErrorSource::Deserialize {
436                normalized_input_bytes, ..
437            } => normalized_input_bytes,
438            JsonDecodeErrorSource::InvalidUtf8 { .. } | JsonDecodeErrorSource::Read { .. } => None,
439        }
440    }
441
442    /// Returns the standard I/O error category for a read failure.
443    ///
444    /// # Returns
445    ///
446    /// `Some(kind)` for a read failure, or `None` for other failure kinds.
447    #[inline(always)]
448    pub const fn read_error_kind(&self) -> Option<io::ErrorKind> {
449        match self.failure {
450            JsonDecodeErrorSource::Read { kind, .. } => Some(kind),
451            _ => None,
452        }
453    }
454
455    /// Returns the retained original I/O error for detailed read diagnostics.
456    ///
457    /// # Returns
458    ///
459    /// The original read error when detailed diagnostics retained it, or `None`
460    /// when the error is not a read failure or its source was not retained.
461    #[inline(always)]
462    pub fn read_error(&self) -> Option<&io::Error> {
463        match &self.failure {
464            JsonDecodeErrorSource::Read {
465                source: Some(source), ..
466            } => Some(source.as_ref()),
467            _ => None,
468        }
469    }
470
471    /// Returns the one-based error line when available.
472    ///
473    /// Invalid-JSON coordinates come from the lexical scanner. Deserialize
474    /// coordinates preserve `serde_json`'s one-based line values, including
475    /// unique-key materialization errors. For a deserialize error, a backend
476    /// line of zero is reported as `None`.
477    ///
478    /// # Returns
479    ///
480    /// `Some(line)` for failures with source coordinates, otherwise `None`.
481    #[inline(always)]
482    pub const fn line(&self) -> Option<usize> {
483        match &self.failure {
484            JsonDecodeErrorSource::InvalidJson { syntax, .. } => Some(syntax.line()),
485            JsonDecodeErrorSource::Deserialize { line, .. } if *line > 0 => Some(*line),
486            _ => None,
487        }
488    }
489
490    /// Returns the one-based error column when available.
491    ///
492    /// Invalid-JSON columns count UTF-8 characters, while deserialize columns
493    /// preserve `serde_json`'s byte-based column. Unique-key materialization
494    /// columns count bytes before the offending token, with only LF starting a
495    /// new line. For a deserialize error, a backend column of zero is reported
496    /// as `None`.
497    ///
498    /// # Returns
499    ///
500    /// `Some(column)` for failures with source coordinates, otherwise `None`.
501    #[inline(always)]
502    pub const fn column(&self) -> Option<usize> {
503        match &self.failure {
504            JsonDecodeErrorSource::InvalidJson { syntax, .. } => Some(syntax.column()),
505            JsonDecodeErrorSource::Deserialize { column, .. } if *column > 0 => Some(*column),
506            _ => None,
507        }
508    }
509
510    /// Returns the structured syntax failure for invalid JSON.
511    ///
512    /// # Returns
513    ///
514    /// A borrowed syntax error when parsing failed, otherwise `None`.
515    #[inline(always)]
516    pub const fn syntax_error(&self) -> Option<&JsonSyntaxError> {
517        match &self.failure {
518            JsonDecodeErrorSource::InvalidJson { syntax, .. } => Some(syntax),
519            _ => None,
520        }
521    }
522
523    /// Returns the complete measured-budget failure when present.
524    ///
525    /// # Returns
526    ///
527    /// A borrowed budget error when resource accounting rejected the input,
528    /// otherwise `None`.
529    #[inline(always)]
530    pub const fn budget_error(&self) -> Option<&MeasuredBudgetError<R, Q>> {
531        match &self.failure {
532            JsonDecodeErrorSource::Budget { source, .. } => Some(source),
533            _ => None,
534        }
535    }
536
537    /// Returns the valid UTF-8 prefix length for invalid byte input.
538    ///
539    /// # Returns
540    ///
541    /// `Some(bytes)` for an invalid UTF-8 failure, or `None` for other failure
542    /// kinds.
543    #[inline(always)]
544    pub const fn utf8_valid_up_to(&self) -> Option<usize> {
545        match self.failure {
546            JsonDecodeErrorSource::InvalidUtf8 { valid_up_to, .. } => Some(valid_up_to),
547            _ => None,
548        }
549    }
550
551    /// Returns the invalid UTF-8 sequence length when known.
552    ///
553    /// # Returns
554    ///
555    /// The length of the invalid sequence when the decoder can determine it,
556    /// otherwise `None`.
557    #[inline(always)]
558    pub const fn utf8_error_len(&self) -> Option<usize> {
559        match self.failure {
560            JsonDecodeErrorSource::InvalidUtf8 { error_len, .. } => error_len,
561            _ => None,
562        }
563    }
564
565    /// Returns the expected top-level kind for a constrained decode failure.
566    ///
567    /// # Returns
568    ///
569    /// The required root kind when a constrained operation failed, otherwise
570    /// `None`.
571    #[inline(always)]
572    pub const fn expected_top_level(&self) -> Option<JsonRootKind> {
573        match self.failure {
574            JsonDecodeErrorSource::UnexpectedTopLevel { expected, .. } => Some(expected),
575            _ => None,
576        }
577    }
578
579    /// Returns the observed top-level kind for a constrained decode failure.
580    ///
581    /// # Returns
582    ///
583    /// The root kind observed in the valid document, otherwise `None`.
584    #[inline(always)]
585    pub const fn actual_top_level(&self) -> Option<JsonRootKind> {
586        match self.failure {
587            JsonDecodeErrorSource::UnexpectedTopLevel { actual, .. } => Some(actual),
588            _ => None,
589        }
590    }
591
592    /// Consumes this error and returns its owned operation-level source.
593    ///
594    /// The materialization reason returned by
595    /// [`Self::materialization_error_reason`] is stored separately and is not
596    /// included in the returned source. Read it before consuming this error.
597    /// Input-derived third-party sources remain present only when the decoder
598    /// used [`DiagnosticPolicy::Detailed`].
599    ///
600    /// # Returns
601    ///
602    /// The owned budget, input, syntax, top-level, or deserialization source
603    /// retained by this error. Any materialization reason is not returned.
604    ///
605    /// # Examples
606    ///
607    /// ```
608    /// use qubit_json::decode::{
609    ///     JsonDecodeErrorSource, JsonDecoder, JsonMaterializationErrorReason,
610    /// };
611    ///
612    /// let error = JsonDecoder::unlimited()
613    ///     .decode_value_str(r#"{"id":1,"id":2}"#)
614    ///     .expect_err("duplicate keys must be rejected");
615    /// let reason = error.materialization_error_reason();
616    /// let source = error.into_source();
617    ///
618    /// assert_eq!(reason, Some(JsonMaterializationErrorReason::DuplicateKey));
619    /// assert!(matches!(source, JsonDecodeErrorSource::Deserialize { .. }));
620    /// ```
621    #[inline(always)]
622    pub fn into_source(self) -> JsonDecodeErrorSource<R, Q> {
623        self.failure
624    }
625}
626
627impl<R, Q> fmt::Debug for JsonDecodeError<R, Q>
628where
629    R: fmt::Debug,
630    Q: Copy + fmt::Debug,
631{
632    /// Formats structured diagnostics retained under the active policy.
633    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
634        formatter
635            .debug_struct("JsonDecodeError")
636            .field("diagnostic_policy", &self.diagnostic_policy)
637            .field("materialization_reason", &self.materialization_reason)
638            .field("failure", &self.failure)
639            .finish()
640    }
641}
642
643impl<R, Q> fmt::Display for JsonDecodeError<R, Q>
644where
645    R: fmt::Debug,
646    Q: ResourceQuantity,
647{
648    /// Formats a privacy-safe or detailed message according to the policy.
649    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
650        match &self.failure {
651            JsonDecodeErrorSource::Read { kind, .. } => {
652                write!(formatter, "JSON input read failed ({kind:?})")
653            }
654            JsonDecodeErrorSource::Budget { source, .. } => {
655                write!(formatter, "JSON resource budget rejected input: {source}")
656            }
657            JsonDecodeErrorSource::EmptyInput { .. } => formatter.write_str("JSON input is empty after normalization"),
658            JsonDecodeErrorSource::InvalidUtf8 { source, .. } => match source {
659                Some(source) => write!(formatter, "Failed to decode JSON input as UTF-8: {source}"),
660                None => formatter.write_str("Failed to decode JSON input as UTF-8"),
661            },
662            JsonDecodeErrorSource::InvalidJson { syntax, source, .. } => match source {
663                Some(source) => {
664                    write!(formatter, "Failed to parse JSON: {source}")
665                }
666                None => write!(formatter, "Failed to parse JSON: {syntax}"),
667            },
668            JsonDecodeErrorSource::UnexpectedTopLevel { expected, actual, .. } => {
669                write!(
670                    formatter,
671                    "Unexpected JSON top-level type: expected {expected}, got {actual}"
672                )
673            }
674            JsonDecodeErrorSource::Deserialize {
675                normalized_input_bytes,
676                line,
677                column,
678                source,
679                ..
680            } => match source {
681                Some(source) => write!(formatter, "Failed to deserialize JSON value: {source}"),
682                None if *line == 0 || *column == 0 => formatter.write_str("Failed to deserialize JSON value"),
683                None if normalized_input_bytes.is_some() => write!(
684                    formatter,
685                    "Failed to deserialize JSON value at normalized line {line} column {column}"
686                ),
687                None => write!(
688                    formatter,
689                    "Failed to deserialize JSON value at line {line} column {column}"
690                ),
691            },
692        }
693    }
694}
695
696impl<R, Q> Error for JsonDecodeError<R, Q>
697where
698    R: fmt::Debug + 'static,
699    Q: ResourceQuantity + 'static,
700{
701    /// Returns budget sources unconditionally and input-derived sources only
702    /// when detailed diagnostics retained them.
703    fn source(&self) -> Option<&(dyn Error + 'static)> {
704        match &self.failure {
705            JsonDecodeErrorSource::Read {
706                source: Some(source), ..
707            } => Some(source.as_ref()),
708            JsonDecodeErrorSource::Budget { source, .. } => Some(source),
709            JsonDecodeErrorSource::InvalidUtf8 {
710                source: Some(source), ..
711            } => Some(source),
712            JsonDecodeErrorSource::InvalidJson {
713                source: Some(source), ..
714            } => Some(source.as_ref()),
715            JsonDecodeErrorSource::Deserialize {
716                source: Some(source), ..
717            } => Some(source.as_ref()),
718            _ => None,
719        }
720    }
721}