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    ///
294    /// # Parameters
295    ///
296    /// * `source` - Serde error produced while constructing the unique-key
297    ///   tree.
298    /// * `reason` - Safe materialization failure category.
299    /// * `raw_input_bytes` - Original input length in bytes.
300    /// * `diagnostic_policy` - Policy controlling retained input details.
301    ///
302    /// # Returns
303    ///
304    /// A deserialization failure annotated with the safe materialization
305    /// reason.
306    pub(crate) fn materialization(
307        source: JsonError,
308        reason: JsonMaterializationErrorReason,
309        raw_input_bytes: usize,
310        diagnostic_policy: DiagnosticPolicy,
311    ) -> Self {
312        let mut error = Self::deserialize(source, raw_input_bytes, None, diagnostic_policy);
313        error.materialization_reason = Some(reason);
314        error
315    }
316
317    /// Returns the stable failure category.
318    ///
319    /// # Returns
320    ///
321    /// The category describing which kind of decode operation failed.
322    #[must_use = "inspect or propagate the underlying decode error source"]
323    #[inline(always)]
324    pub const fn kind(&self) -> JsonDecodeErrorKind {
325        match self.failure {
326            JsonDecodeErrorSource::Read { .. } => JsonDecodeErrorKind::Read,
327            JsonDecodeErrorSource::Budget { .. } => JsonDecodeErrorKind::Budget,
328            JsonDecodeErrorSource::EmptyInput { .. } => JsonDecodeErrorKind::EmptyInput,
329            JsonDecodeErrorSource::InvalidUtf8 { .. } => JsonDecodeErrorKind::InvalidUtf8,
330            JsonDecodeErrorSource::InvalidJson { .. } => JsonDecodeErrorKind::InvalidJson,
331            JsonDecodeErrorSource::UnexpectedTopLevel { .. } => JsonDecodeErrorKind::UnexpectedTopLevel,
332            JsonDecodeErrorSource::Deserialize { .. } => JsonDecodeErrorKind::Deserialize,
333        }
334    }
335
336    /// Returns the safe reason for a unique-key intermediate-tree failure.
337    ///
338    /// Returns `Some(reason)` when unique-key tree construction failed because
339    /// of a duplicate key or a defensive unrepresentable admitted number.
340    /// The legacy depth-limit reason remains for source compatibility, but the
341    /// shared builder has no fixed container cap and configured depth failures
342    /// are reported as budget errors. Returns `None` for lexical, input,
343    /// budget, and target-deserialization
344    /// failures. The reason remains available under redacted diagnostics and
345    /// contains no input-derived text. Read it before consuming the error with
346    /// [`Self::into_source`].
347    ///
348    /// # Examples
349    ///
350    /// ```
351    /// use qubit_json::decode::{JsonDecoder, JsonMaterializationErrorReason};
352    /// use serde_json::Value;
353    ///
354    /// let mut decoder = JsonDecoder::unlimited();
355    /// let error = decoder
356    ///     .decode_unique_str::<Value>(r#"{"id":1,"id":2}"#)
357    ///     .expect_err("duplicate keys must be rejected");
358    /// assert_eq!(
359    ///     error.materialization_error_reason(),
360    ///     Some(JsonMaterializationErrorReason::DuplicateKey),
361    /// );
362    /// ```
363    #[inline(always)]
364    pub const fn materialization_error_reason(&self) -> Option<JsonMaterializationErrorReason> {
365        self.materialization_reason
366    }
367
368    /// Returns the semantic stage that produced the failure.
369    ///
370    /// # Returns
371    ///
372    /// The pipeline stage at which the failure was recorded.
373    #[must_use]
374    #[inline(always)]
375    pub const fn stage(&self) -> JsonDecodeStage {
376        match self.failure {
377            JsonDecodeErrorSource::Budget { stage, .. } | JsonDecodeErrorSource::EmptyInput { stage, .. } => stage,
378            JsonDecodeErrorSource::Read { .. } => JsonDecodeStage::Input,
379            JsonDecodeErrorSource::InvalidUtf8 { .. } => JsonDecodeStage::DecodeText,
380            JsonDecodeErrorSource::InvalidJson { .. } => JsonDecodeStage::Parse,
381            JsonDecodeErrorSource::UnexpectedTopLevel { .. } => JsonDecodeStage::TopLevelCheck,
382            JsonDecodeErrorSource::Deserialize { .. } => JsonDecodeStage::Deserialize,
383        }
384    }
385
386    /// Returns the diagnostic policy applied while constructing this error.
387    ///
388    /// # Returns
389    ///
390    /// The policy that determines whether input-derived details are retained.
391    #[must_use]
392    #[inline(always)]
393    pub const fn diagnostic_policy(&self) -> DiagnosticPolicy {
394        self.diagnostic_policy
395    }
396
397    /// Returns the raw input length observed at the decoding boundary.
398    ///
399    /// For slice-based entry points, this is the complete input slice length,
400    /// including when input admission rejects the slice before processing it.
401    /// For `decode_input`, this is the length of the prefix read and retained
402    /// before the error; a byte consumed only to probe past the input budget is
403    /// excluded. The value therefore does not universally represent bytes
404    /// charged to the budget or the session's cumulative input usage.
405    ///
406    /// # Returns
407    ///
408    /// The raw input length observed for the input that caused this error.
409    #[must_use]
410    #[inline(always)]
411    pub const fn raw_input_bytes(&self) -> usize {
412        match self.failure {
413            JsonDecodeErrorSource::Read { raw_input_bytes, .. }
414            | JsonDecodeErrorSource::Budget { raw_input_bytes, .. }
415            | JsonDecodeErrorSource::EmptyInput { raw_input_bytes, .. }
416            | JsonDecodeErrorSource::InvalidUtf8 { raw_input_bytes, .. }
417            | JsonDecodeErrorSource::InvalidJson { raw_input_bytes, .. }
418            | JsonDecodeErrorSource::UnexpectedTopLevel { raw_input_bytes, .. }
419            | JsonDecodeErrorSource::Deserialize { raw_input_bytes, .. } => raw_input_bytes,
420        }
421    }
422
423    /// Returns the predicted normalized text length when it is available.
424    ///
425    /// A value may be present even when a budget rejected the input before
426    /// normalized text was allocated or rewritten. This reports a target
427    /// length, not proof that a normalized document was produced or charged.
428    ///
429    /// # Returns
430    ///
431    /// `Some(length)` when normalization determined a target length, or
432    /// `None` when the failure occurred before that length was available.
433    #[inline(always)]
434    pub const fn normalized_input_bytes(&self) -> Option<usize> {
435        match self.failure {
436            JsonDecodeErrorSource::Budget {
437                normalized_input_bytes, ..
438            }
439            | JsonDecodeErrorSource::EmptyInput {
440                normalized_input_bytes, ..
441            }
442            | JsonDecodeErrorSource::InvalidJson {
443                normalized_input_bytes, ..
444            }
445            | JsonDecodeErrorSource::UnexpectedTopLevel {
446                normalized_input_bytes, ..
447            }
448            | JsonDecodeErrorSource::Deserialize {
449                normalized_input_bytes, ..
450            } => normalized_input_bytes,
451            JsonDecodeErrorSource::InvalidUtf8 { .. } | JsonDecodeErrorSource::Read { .. } => None,
452        }
453    }
454
455    /// Returns the standard I/O error category for a read failure.
456    ///
457    /// # Returns
458    ///
459    /// `Some(kind)` for a read failure, or `None` for other failure kinds.
460    #[inline(always)]
461    pub const fn read_error_kind(&self) -> Option<io::ErrorKind> {
462        match self.failure {
463            JsonDecodeErrorSource::Read { kind, .. } => Some(kind),
464            _ => None,
465        }
466    }
467
468    /// Returns the retained original I/O error for detailed read diagnostics.
469    ///
470    /// # Returns
471    ///
472    /// The original read error when detailed diagnostics retained it, or `None`
473    /// when the error is not a read failure or its source was not retained.
474    #[inline(always)]
475    pub fn read_error(&self) -> Option<&io::Error> {
476        match &self.failure {
477            JsonDecodeErrorSource::Read {
478                source: Some(source), ..
479            } => Some(source.as_ref()),
480            _ => None,
481        }
482    }
483
484    /// Returns the one-based error line when available.
485    ///
486    /// Invalid-JSON coordinates come from the lexical scanner. Deserialize
487    /// coordinates preserve `serde_json`'s one-based line values, including
488    /// unique-key materialization errors. For a deserialize error, a backend
489    /// line of zero is reported as `None`.
490    ///
491    /// # Returns
492    ///
493    /// `Some(line)` for failures with source coordinates, otherwise `None`.
494    #[inline(always)]
495    pub const fn line(&self) -> Option<usize> {
496        match &self.failure {
497            JsonDecodeErrorSource::InvalidJson { syntax, .. } => Some(syntax.line()),
498            JsonDecodeErrorSource::Deserialize { line, .. } if *line > 0 => Some(*line),
499            _ => None,
500        }
501    }
502
503    /// Returns the one-based error column when available.
504    ///
505    /// Invalid-JSON columns count UTF-8 characters, while deserialize columns
506    /// preserve `serde_json`'s byte-based column. Unique-key materialization
507    /// columns count bytes before the offending token, with only LF starting a
508    /// new line. For a deserialize error, a backend column of zero is reported
509    /// as `None`.
510    ///
511    /// # Returns
512    ///
513    /// `Some(column)` for failures with source coordinates, otherwise `None`.
514    #[inline(always)]
515    pub const fn column(&self) -> Option<usize> {
516        match &self.failure {
517            JsonDecodeErrorSource::InvalidJson { syntax, .. } => Some(syntax.column()),
518            JsonDecodeErrorSource::Deserialize { column, .. } if *column > 0 => Some(*column),
519            _ => None,
520        }
521    }
522
523    /// Returns the structured syntax failure for invalid JSON.
524    ///
525    /// # Returns
526    ///
527    /// A borrowed syntax error when parsing failed, otherwise `None`.
528    #[inline(always)]
529    pub const fn syntax_error(&self) -> Option<&JsonSyntaxError> {
530        match &self.failure {
531            JsonDecodeErrorSource::InvalidJson { syntax, .. } => Some(syntax),
532            _ => None,
533        }
534    }
535
536    /// Returns the complete measured-budget failure when present.
537    ///
538    /// # Returns
539    ///
540    /// A borrowed budget error when resource accounting rejected the input,
541    /// otherwise `None`.
542    #[inline(always)]
543    pub const fn budget_error(&self) -> Option<&MeasuredBudgetError<R, Q>> {
544        match &self.failure {
545            JsonDecodeErrorSource::Budget { source, .. } => Some(source),
546            _ => None,
547        }
548    }
549
550    /// Returns the valid UTF-8 prefix length for invalid byte input.
551    ///
552    /// # Returns
553    ///
554    /// `Some(bytes)` for an invalid UTF-8 failure, or `None` for other failure
555    /// kinds.
556    #[inline(always)]
557    pub const fn utf8_valid_up_to(&self) -> Option<usize> {
558        match self.failure {
559            JsonDecodeErrorSource::InvalidUtf8 { valid_up_to, .. } => Some(valid_up_to),
560            _ => None,
561        }
562    }
563
564    /// Returns the invalid UTF-8 sequence length when known.
565    ///
566    /// # Returns
567    ///
568    /// The length of the invalid sequence when the decoder can determine it,
569    /// otherwise `None`.
570    #[inline(always)]
571    pub const fn utf8_error_len(&self) -> Option<usize> {
572        match self.failure {
573            JsonDecodeErrorSource::InvalidUtf8 { error_len, .. } => error_len,
574            _ => None,
575        }
576    }
577
578    /// Returns the expected top-level kind for a constrained decode failure.
579    ///
580    /// # Returns
581    ///
582    /// The required root kind when a constrained operation failed, otherwise
583    /// `None`.
584    #[inline(always)]
585    pub const fn expected_top_level(&self) -> Option<JsonRootKind> {
586        match self.failure {
587            JsonDecodeErrorSource::UnexpectedTopLevel { expected, .. } => Some(expected),
588            _ => None,
589        }
590    }
591
592    /// Returns the observed top-level kind for a constrained decode failure.
593    ///
594    /// # Returns
595    ///
596    /// The root kind observed in the valid document, otherwise `None`.
597    #[inline(always)]
598    pub const fn actual_top_level(&self) -> Option<JsonRootKind> {
599        match self.failure {
600            JsonDecodeErrorSource::UnexpectedTopLevel { actual, .. } => Some(actual),
601            _ => None,
602        }
603    }
604
605    /// Consumes this error and returns its owned operation-level source.
606    ///
607    /// The materialization reason returned by
608    /// [`Self::materialization_error_reason`] is stored separately and is not
609    /// included in the returned source. Read it before consuming this error.
610    /// Input-derived third-party sources remain present only when the decoder
611    /// used [`DiagnosticPolicy::Detailed`].
612    ///
613    /// # Returns
614    ///
615    /// The owned budget, input, syntax, top-level, or deserialization source
616    /// retained by this error. Any materialization reason is not returned.
617    ///
618    /// # Examples
619    ///
620    /// ```
621    /// use qubit_json::decode::{
622    ///     JsonDecodeErrorSource, JsonDecoder, JsonMaterializationErrorReason,
623    /// };
624    ///
625    /// let error = JsonDecoder::unlimited()
626    ///     .decode_value_str(r#"{"id":1,"id":2}"#)
627    ///     .expect_err("duplicate keys must be rejected");
628    /// let reason = error.materialization_error_reason();
629    /// let source = error.into_source();
630    ///
631    /// assert_eq!(reason, Some(JsonMaterializationErrorReason::DuplicateKey));
632    /// assert!(matches!(source, JsonDecodeErrorSource::Deserialize { .. }));
633    /// ```
634    #[inline(always)]
635    pub fn into_source(self) -> JsonDecodeErrorSource<R, Q> {
636        self.failure
637    }
638}
639
640impl<R, Q> fmt::Debug for JsonDecodeError<R, Q>
641where
642    R: fmt::Debug,
643    Q: Copy + fmt::Debug,
644{
645    /// Formats structured diagnostics retained under the active policy.
646    ///
647    /// # Parameters
648    ///
649    /// * `formatter` - Destination that receives the debug representation.
650    ///
651    /// # Returns
652    ///
653    /// `Ok(())` after the structured diagnostic has been written.
654    ///
655    /// # Errors
656    ///
657    /// Returns the formatter error if writing the diagnostic fails.
658    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
659        formatter
660            .debug_struct("JsonDecodeError")
661            .field("diagnostic_policy", &self.diagnostic_policy)
662            .field("materialization_reason", &self.materialization_reason)
663            .field("failure", &self.failure)
664            .finish()
665    }
666}
667
668impl<R, Q> fmt::Display for JsonDecodeError<R, Q>
669where
670    R: fmt::Debug,
671    Q: ResourceQuantity,
672{
673    /// Formats a privacy-safe or detailed message according to the policy.
674    ///
675    /// # Parameters
676    ///
677    /// * `formatter` - Destination that receives the display message.
678    ///
679    /// # Returns
680    ///
681    /// `Ok(())` after the policy-appropriate message has been written.
682    ///
683    /// # Errors
684    ///
685    /// Returns the formatter error if writing the message fails.
686    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
687        match &self.failure {
688            JsonDecodeErrorSource::Read { kind, .. } => {
689                write!(formatter, "JSON input read failed ({kind:?})")
690            }
691            JsonDecodeErrorSource::Budget { source, .. } => {
692                write!(formatter, "JSON resource budget rejected input: {source}")
693            }
694            JsonDecodeErrorSource::EmptyInput { .. } => formatter.write_str("JSON input is empty after normalization"),
695            JsonDecodeErrorSource::InvalidUtf8 { source, .. } => match source {
696                Some(source) => write!(formatter, "Failed to decode JSON input as UTF-8: {source}"),
697                None => formatter.write_str("Failed to decode JSON input as UTF-8"),
698            },
699            JsonDecodeErrorSource::InvalidJson { syntax, source, .. } => match source {
700                Some(source) => {
701                    write!(formatter, "Failed to parse JSON: {source}")
702                }
703                None => write!(formatter, "Failed to parse JSON: {syntax}"),
704            },
705            JsonDecodeErrorSource::UnexpectedTopLevel { expected, actual, .. } => {
706                write!(
707                    formatter,
708                    "Unexpected JSON top-level type: expected {expected}, got {actual}"
709                )
710            }
711            JsonDecodeErrorSource::Deserialize {
712                normalized_input_bytes,
713                line,
714                column,
715                source,
716                ..
717            } => match source {
718                Some(source) => write!(formatter, "Failed to deserialize JSON value: {source}"),
719                None if *line == 0 || *column == 0 => formatter.write_str("Failed to deserialize JSON value"),
720                None if normalized_input_bytes.is_some() => write!(
721                    formatter,
722                    "Failed to deserialize JSON value at normalized line {line} column {column}"
723                ),
724                None => write!(
725                    formatter,
726                    "Failed to deserialize JSON value at line {line} column {column}"
727                ),
728            },
729        }
730    }
731}
732
733impl<R, Q> Error for JsonDecodeError<R, Q>
734where
735    R: fmt::Debug + 'static,
736    Q: ResourceQuantity + 'static,
737{
738    /// Returns budget sources unconditionally and input-derived sources only
739    /// when detailed diagnostics retained them.
740    ///
741    /// # Returns
742    ///
743    /// The retained source error, or `None` when this failure has no source or
744    /// input-derived diagnostics were redacted.
745    fn source(&self) -> Option<&(dyn Error + 'static)> {
746        match &self.failure {
747            JsonDecodeErrorSource::Read {
748                source: Some(source), ..
749            } => Some(source.as_ref()),
750            JsonDecodeErrorSource::Budget { source, .. } => Some(source),
751            JsonDecodeErrorSource::InvalidUtf8 {
752                source: Some(source), ..
753            } => Some(source),
754            JsonDecodeErrorSource::InvalidJson {
755                source: Some(source), ..
756            } => Some(source.as_ref()),
757            JsonDecodeErrorSource::Deserialize {
758                source: Some(source), ..
759            } => Some(source.as_ref()),
760            _ => None,
761        }
762    }
763}