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}