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}