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