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::JsonRootKind;
25use super::JsonSyntaxError;
26use crate::lexical::JsonLexicalFailure;
27
28/// Failure produced by either strict or normalizing JSON decoding.
29///
30/// Internal variants are private so callers branch through stable semantic
31/// accessors rather than depending on scanner, normalizer, or Serde details.
32///
33/// # Type Parameters
34///
35/// * `R` - Resource identity attached to budget failures.
36/// * `Q` - Quantity representation attached to budget failures.
37///
38/// # Examples
39///
40/// ```
41/// use qubit_json::decode::{JsonDecodeError, JsonDecodeErrorKind, JsonDecoder};
42/// use serde_json::Value;
43///
44/// let mut decoder = JsonDecoder::unlimited();
45/// let error: JsonDecodeError = decoder
46///     .decode_str::<Value>("")
47///     .expect_err("empty input must be rejected");
48/// assert_eq!(error.kind(), JsonDecodeErrorKind::InvalidJson);
49/// ```
50#[must_use]
51#[derive(Clone)]
52pub struct JsonDecodeError<R = JsonResource, Q = usize>
53where
54    Q: Copy + fmt::Debug,
55{
56    /// Policy controlling input-derived source retention.
57    diagnostic_policy: DiagnosticPolicy,
58    /// Mutually exclusive structured failure.
59    failure: JsonDecodeErrorSource<R, Q>,
60}
61
62impl<R, Q> JsonDecodeError<R, Q>
63where
64    Q: Copy + fmt::Debug,
65{
66    /// Creates a stream-read failure and conditionally retains its source.
67    ///
68    /// # Parameters
69    ///
70    /// * `source` - I/O error produced while reading the input.
71    /// * `raw_input_bytes` - Number of input bytes read before the failure.
72    /// * `diagnostic_policy` - Whether to retain the original error details.
73    #[must_use = "return or inspect the constructed decoding error"]
74    pub(crate) fn read(source: io::Error, raw_input_bytes: usize, diagnostic_policy: DiagnosticPolicy) -> Self {
75        let kind = source.kind();
76        let source = (diagnostic_policy == DiagnosticPolicy::Detailed).then(|| Arc::new(source));
77        Self {
78            diagnostic_policy,
79            failure: JsonDecodeErrorSource::Read {
80                raw_input_bytes,
81                kind,
82                source,
83            },
84        }
85    }
86
87    /// Creates a measured-budget failure at a semantic stage.
88    ///
89    /// # Parameters
90    ///
91    /// * `source` - Resource measurement that exceeded its budget.
92    /// * `stage` - Decode stage where the budget was exceeded.
93    /// * `raw_input_bytes` - Original input length in bytes.
94    /// * `normalized_input_bytes` - Normalized input length, when available.
95    /// * `diagnostic_policy` - Policy controlling retained input details.
96    #[must_use = "return or inspect the constructed decoding error"]
97    pub(crate) const fn budget(
98        source: MeasuredBudgetError<R, Q>,
99        stage: JsonDecodeStage,
100        raw_input_bytes: usize,
101        normalized_input_bytes: Option<usize>,
102        diagnostic_policy: DiagnosticPolicy,
103    ) -> Self {
104        Self {
105            diagnostic_policy,
106            failure: JsonDecodeErrorSource::Budget {
107                stage,
108                raw_input_bytes,
109                normalized_input_bytes,
110                source,
111            },
112        }
113    }
114
115    /// Creates an empty-input failure at a semantic stage.
116    ///
117    /// # Parameters
118    ///
119    /// * `stage` - Decode stage where the empty input was detected.
120    /// * `raw_input_bytes` - Original input length in bytes.
121    /// * `normalized_input_bytes` - Normalized input length, when available.
122    /// * `diagnostic_policy` - Policy controlling retained input details.
123    #[must_use = "return or inspect the constructed decoding error"]
124    pub(crate) const fn empty_input(
125        stage: JsonDecodeStage,
126        raw_input_bytes: usize,
127        normalized_input_bytes: Option<usize>,
128        diagnostic_policy: DiagnosticPolicy,
129    ) -> Self {
130        Self {
131            diagnostic_policy,
132            failure: JsonDecodeErrorSource::EmptyInput {
133                stage,
134                raw_input_bytes,
135                normalized_input_bytes,
136            },
137        }
138    }
139
140    /// Creates an invalid-UTF-8 failure and conditionally retains its source.
141    ///
142    /// # Parameters
143    ///
144    /// * `source` - UTF-8 validation failure for the input bytes.
145    /// * `raw_input_bytes` - Original input length in bytes.
146    /// * `diagnostic_policy` - Whether to retain the original error details.
147    #[must_use = "return or inspect the constructed decoding error"]
148    pub(crate) fn invalid_utf8(
149        source: std::str::Utf8Error,
150        raw_input_bytes: usize,
151        diagnostic_policy: DiagnosticPolicy,
152    ) -> Self {
153        let valid_up_to = source.valid_up_to();
154        let error_len = source.error_len();
155        let source = (diagnostic_policy == DiagnosticPolicy::Detailed).then_some(source);
156        Self {
157            diagnostic_policy,
158            failure: JsonDecodeErrorSource::InvalidUtf8 {
159                raw_input_bytes,
160                valid_up_to,
161                error_len,
162                source,
163            },
164        }
165    }
166
167    /// Creates a stable invalid-JSON failure from lexical admission.
168    ///
169    /// # Parameters
170    ///
171    /// * `syntax_source` - Lexical failure with stable syntax details.
172    /// * `detailed_source` - Optional parser error retained for detailed
173    ///   diagnostics.
174    /// * `raw_input_bytes` - Original input length in bytes.
175    /// * `normalized_input_bytes` - Normalized input length, when available.
176    /// * `diagnostic_policy` - Policy controlling retained input details.
177    #[must_use = "return or inspect the constructed decoding error"]
178    pub(crate) fn invalid_json(
179        syntax_source: JsonLexicalFailure,
180        detailed_source: Option<Arc<dyn Error + Send + Sync>>,
181        raw_input_bytes: usize,
182        normalized_input_bytes: Option<usize>,
183        diagnostic_policy: DiagnosticPolicy,
184    ) -> Self {
185        Self {
186            diagnostic_policy,
187            failure: JsonDecodeErrorSource::InvalidJson {
188                raw_input_bytes,
189                normalized_input_bytes,
190                syntax: JsonSyntaxError::from_lexical(syntax_source),
191                source: detailed_source,
192            },
193        }
194    }
195
196    /// Creates an unexpected-top-level failure.
197    ///
198    /// # Parameters
199    ///
200    /// * `expected` - Required top-level JSON kind.
201    /// * `actual` - Observed top-level JSON kind.
202    /// * `raw_input_bytes` - Original input length in bytes.
203    /// * `normalized_input_bytes` - Normalized input length, when available.
204    /// * `diagnostic_policy` - Policy controlling retained input details.
205    #[must_use = "return or inspect the constructed decoding error"]
206    pub(crate) const fn unexpected_top_level(
207        expected: JsonRootKind,
208        actual: JsonRootKind,
209        raw_input_bytes: usize,
210        normalized_input_bytes: Option<usize>,
211        diagnostic_policy: DiagnosticPolicy,
212    ) -> Self {
213        Self {
214            diagnostic_policy,
215            failure: JsonDecodeErrorSource::UnexpectedTopLevel {
216                raw_input_bytes,
217                normalized_input_bytes,
218                expected,
219                actual,
220            },
221        }
222    }
223
224    /// Creates a target-deserialization failure and conditionally retains its
225    /// input-derived source.
226    ///
227    /// # Parameters
228    ///
229    /// * `source` - Serde error produced while materializing the target value.
230    /// * `raw_input_bytes` - Original input length in bytes.
231    /// * `normalized_input_bytes` - Normalized input length, when available.
232    /// * `diagnostic_policy` - Whether to retain the original error details.
233    #[must_use = "return or inspect the constructed decoding error"]
234    pub(crate) fn deserialize(
235        source: JsonError,
236        raw_input_bytes: usize,
237        normalized_input_bytes: Option<usize>,
238        diagnostic_policy: DiagnosticPolicy,
239    ) -> Self {
240        let line = source.line();
241        let column = source.column();
242        let source =
243            (diagnostic_policy == DiagnosticPolicy::Detailed).then(|| Arc::new(source) as Arc<dyn Error + Send + Sync>);
244        Self {
245            diagnostic_policy,
246            failure: JsonDecodeErrorSource::Deserialize {
247                raw_input_bytes,
248                normalized_input_bytes,
249                line,
250                column,
251                source,
252            },
253        }
254    }
255
256    /// Returns the stable failure category.
257    ///
258    /// # Returns
259    ///
260    /// The category describing which kind of decode operation failed.
261    #[must_use]
262    #[inline(always)]
263    pub const fn kind(&self) -> JsonDecodeErrorKind {
264        match self.failure {
265            JsonDecodeErrorSource::Read { .. } => JsonDecodeErrorKind::Read,
266            JsonDecodeErrorSource::Budget { .. } => JsonDecodeErrorKind::Budget,
267            JsonDecodeErrorSource::EmptyInput { .. } => JsonDecodeErrorKind::EmptyInput,
268            JsonDecodeErrorSource::InvalidUtf8 { .. } => JsonDecodeErrorKind::InvalidUtf8,
269            JsonDecodeErrorSource::InvalidJson { .. } => JsonDecodeErrorKind::InvalidJson,
270            JsonDecodeErrorSource::UnexpectedTopLevel { .. } => JsonDecodeErrorKind::UnexpectedTopLevel,
271            JsonDecodeErrorSource::Deserialize { .. } => JsonDecodeErrorKind::Deserialize,
272        }
273    }
274
275    /// Returns the semantic stage that produced the failure.
276    ///
277    /// # Returns
278    ///
279    /// The pipeline stage at which the failure was recorded.
280    #[must_use]
281    #[inline(always)]
282    pub const fn stage(&self) -> JsonDecodeStage {
283        match self.failure {
284            JsonDecodeErrorSource::Budget { stage, .. } | JsonDecodeErrorSource::EmptyInput { stage, .. } => stage,
285            JsonDecodeErrorSource::Read { .. } => JsonDecodeStage::Input,
286            JsonDecodeErrorSource::InvalidUtf8 { .. } => JsonDecodeStage::DecodeText,
287            JsonDecodeErrorSource::InvalidJson { .. } => JsonDecodeStage::Parse,
288            JsonDecodeErrorSource::UnexpectedTopLevel { .. } => JsonDecodeStage::TopLevelCheck,
289            JsonDecodeErrorSource::Deserialize { .. } => JsonDecodeStage::Deserialize,
290        }
291    }
292
293    /// Returns the diagnostic policy applied while constructing this error.
294    ///
295    /// # Returns
296    ///
297    /// The policy that determines whether input-derived details are retained.
298    #[must_use]
299    #[inline(always)]
300    pub const fn diagnostic_policy(&self) -> DiagnosticPolicy {
301        self.diagnostic_policy
302    }
303
304    /// Returns the original input length in bytes.
305    ///
306    /// # Returns
307    ///
308    /// The number of bytes charged for the input that caused this error.
309    #[must_use]
310    #[inline(always)]
311    pub const fn raw_input_bytes(&self) -> usize {
312        match self.failure {
313            JsonDecodeErrorSource::Read { raw_input_bytes, .. }
314            | JsonDecodeErrorSource::Budget { raw_input_bytes, .. }
315            | JsonDecodeErrorSource::EmptyInput { raw_input_bytes, .. }
316            | JsonDecodeErrorSource::InvalidUtf8 { raw_input_bytes, .. }
317            | JsonDecodeErrorSource::InvalidJson { raw_input_bytes, .. }
318            | JsonDecodeErrorSource::UnexpectedTopLevel { raw_input_bytes, .. }
319            | JsonDecodeErrorSource::Deserialize { raw_input_bytes, .. } => raw_input_bytes,
320        }
321    }
322
323    /// Returns the normalized text length when normalization completed.
324    ///
325    /// # Returns
326    ///
327    /// `Some(length)` when normalization produced text, or `None` when the
328    /// failure occurred before a normalized document existed.
329    #[must_use]
330    #[inline(always)]
331    pub const fn normalized_input_bytes(&self) -> Option<usize> {
332        match self.failure {
333            JsonDecodeErrorSource::Budget {
334                normalized_input_bytes, ..
335            }
336            | JsonDecodeErrorSource::EmptyInput {
337                normalized_input_bytes, ..
338            }
339            | JsonDecodeErrorSource::InvalidJson {
340                normalized_input_bytes, ..
341            }
342            | JsonDecodeErrorSource::UnexpectedTopLevel {
343                normalized_input_bytes, ..
344            }
345            | JsonDecodeErrorSource::Deserialize {
346                normalized_input_bytes, ..
347            } => normalized_input_bytes,
348            JsonDecodeErrorSource::InvalidUtf8 { .. } | JsonDecodeErrorSource::Read { .. } => None,
349        }
350    }
351
352    /// Returns the standard I/O error category for a read failure.
353    #[must_use]
354    #[inline(always)]
355    pub const fn read_error_kind(&self) -> Option<io::ErrorKind> {
356        match self.failure {
357            JsonDecodeErrorSource::Read { kind, .. } => Some(kind),
358            _ => None,
359        }
360    }
361
362    /// Returns the retained original I/O error for detailed read diagnostics.
363    ///
364    /// # Returns
365    ///
366    /// The original read error when detailed diagnostics retained it, or `None`
367    /// when the error is not a read failure or its source was not retained.
368    #[must_use]
369    #[inline(always)]
370    pub fn read_error(&self) -> Option<&io::Error> {
371        match &self.failure {
372            JsonDecodeErrorSource::Read {
373                source: Some(source), ..
374            } => Some(source.as_ref()),
375            _ => None,
376        }
377    }
378
379    /// Returns the one-based error line when available.
380    ///
381    /// # Returns
382    ///
383    /// `Some(line)` for failures with source coordinates, otherwise `None`.
384    #[must_use]
385    #[inline(always)]
386    pub const fn line(&self) -> Option<usize> {
387        match &self.failure {
388            JsonDecodeErrorSource::InvalidJson { syntax, .. } => Some(syntax.line()),
389            JsonDecodeErrorSource::Deserialize { line, .. } if *line > 0 => Some(*line),
390            _ => None,
391        }
392    }
393
394    /// Returns the one-based error column when available.
395    ///
396    /// # Returns
397    ///
398    /// `Some(column)` for failures with source coordinates, otherwise `None`.
399    #[must_use]
400    #[inline(always)]
401    pub const fn column(&self) -> Option<usize> {
402        match &self.failure {
403            JsonDecodeErrorSource::InvalidJson { syntax, .. } => Some(syntax.column()),
404            JsonDecodeErrorSource::Deserialize { column, .. } if *column > 0 => Some(*column),
405            _ => None,
406        }
407    }
408
409    /// Returns the structured syntax failure for invalid JSON.
410    ///
411    /// # Returns
412    ///
413    /// A borrowed syntax error when parsing failed, otherwise `None`.
414    #[must_use]
415    #[inline(always)]
416    pub const fn syntax_error(&self) -> Option<&JsonSyntaxError> {
417        match &self.failure {
418            JsonDecodeErrorSource::InvalidJson { syntax, .. } => Some(syntax),
419            _ => None,
420        }
421    }
422
423    /// Returns the complete measured-budget failure when present.
424    ///
425    /// # Returns
426    ///
427    /// A borrowed budget error when resource accounting rejected the input,
428    /// otherwise `None`.
429    #[must_use]
430    #[inline(always)]
431    pub const fn budget_error(&self) -> Option<&MeasuredBudgetError<R, Q>> {
432        match &self.failure {
433            JsonDecodeErrorSource::Budget { source, .. } => Some(source),
434            _ => None,
435        }
436    }
437
438    /// Returns the valid UTF-8 prefix length for invalid byte input.
439    ///
440    /// # Returns
441    ///
442    /// `Some(bytes)` for an invalid UTF-8 failure, or `None` for other failure
443    /// kinds.
444    #[must_use]
445    #[inline(always)]
446    pub const fn utf8_valid_up_to(&self) -> Option<usize> {
447        match self.failure {
448            JsonDecodeErrorSource::InvalidUtf8 { valid_up_to, .. } => Some(valid_up_to),
449            _ => None,
450        }
451    }
452
453    /// Returns the invalid UTF-8 sequence length when known.
454    ///
455    /// # Returns
456    ///
457    /// The length of the invalid sequence when the decoder can determine it,
458    /// otherwise `None`.
459    #[must_use]
460    #[inline(always)]
461    pub const fn utf8_error_len(&self) -> Option<usize> {
462        match self.failure {
463            JsonDecodeErrorSource::InvalidUtf8 { error_len, .. } => error_len,
464            _ => None,
465        }
466    }
467
468    /// Returns the expected top-level kind for a constrained decode failure.
469    ///
470    /// # Returns
471    ///
472    /// The required root kind when a constrained operation failed, otherwise
473    /// `None`.
474    #[must_use]
475    #[inline(always)]
476    pub const fn expected_top_level(&self) -> Option<JsonRootKind> {
477        match self.failure {
478            JsonDecodeErrorSource::UnexpectedTopLevel { expected, .. } => Some(expected),
479            _ => None,
480        }
481    }
482
483    /// Returns the observed top-level kind for a constrained decode failure.
484    ///
485    /// # Returns
486    ///
487    /// The root kind observed in the valid document, otherwise `None`.
488    #[must_use]
489    #[inline(always)]
490    pub const fn actual_top_level(&self) -> Option<JsonRootKind> {
491        match self.failure {
492            JsonDecodeErrorSource::UnexpectedTopLevel { actual, .. } => Some(actual),
493            _ => None,
494        }
495    }
496
497    /// Consumes this error and returns its owned semantic source.
498    ///
499    /// Unlike the kind-specific accessors, this operation preserves the
500    /// complete mutually exclusive failure state. Input-derived third-party
501    /// sources remain present only when the decoder used
502    /// [`DiagnosticPolicy::Detailed`].
503    ///
504    /// # Returns
505    ///
506    /// The structured budget, input, syntax, top-level, or deserialization
507    /// source retained by this error.
508    ///
509    /// # Returns
510    ///
511    /// The complete owned source describing the decode failure.
512    #[inline(always)]
513    pub fn into_source(self) -> JsonDecodeErrorSource<R, Q> {
514        self.failure
515    }
516}
517
518impl<R, Q> fmt::Debug for JsonDecodeError<R, Q>
519where
520    R: fmt::Debug,
521    Q: Copy + fmt::Debug,
522{
523    /// Formats structured diagnostics retained under the active policy.
524    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
525        formatter
526            .debug_struct("JsonDecodeError")
527            .field("diagnostic_policy", &self.diagnostic_policy)
528            .field("failure", &self.failure)
529            .finish()
530    }
531}
532
533impl<R, Q> fmt::Display for JsonDecodeError<R, Q>
534where
535    R: fmt::Debug,
536    Q: ResourceQuantity,
537{
538    /// Formats a privacy-safe or detailed message according to the policy.
539    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
540        match &self.failure {
541            JsonDecodeErrorSource::Read { kind, .. } => write!(formatter, "JSON input read failed ({kind:?})"),
542            JsonDecodeErrorSource::Budget { source, .. } => {
543                write!(formatter, "JSON resource budget rejected input: {source}")
544            }
545            JsonDecodeErrorSource::EmptyInput { .. } => formatter.write_str("JSON input is empty after normalization"),
546            JsonDecodeErrorSource::InvalidUtf8 { source, .. } => match source {
547                Some(source) => write!(formatter, "Failed to decode JSON input as UTF-8: {source}"),
548                None => formatter.write_str("Failed to decode JSON input as UTF-8"),
549            },
550            JsonDecodeErrorSource::InvalidJson { syntax, source, .. } => match source {
551                Some(source) => {
552                    write!(formatter, "Failed to parse JSON: {source}")
553                }
554                None => write!(formatter, "Failed to parse JSON: {syntax}"),
555            },
556            JsonDecodeErrorSource::UnexpectedTopLevel { expected, actual, .. } => {
557                write!(
558                    formatter,
559                    "Unexpected JSON top-level type: expected {expected}, got {actual}"
560                )
561            }
562            JsonDecodeErrorSource::Deserialize {
563                normalized_input_bytes,
564                line,
565                column,
566                source,
567                ..
568            } => match source {
569                Some(source) => write!(formatter, "Failed to deserialize JSON value: {source}"),
570                None if normalized_input_bytes.is_some() => write!(
571                    formatter,
572                    "Failed to deserialize JSON value at normalized line {line} column {column}"
573                ),
574                None => write!(
575                    formatter,
576                    "Failed to deserialize JSON value at line {line} column {column}"
577                ),
578            },
579        }
580    }
581}
582
583impl<R, Q> Error for JsonDecodeError<R, Q>
584where
585    R: fmt::Debug + 'static,
586    Q: ResourceQuantity + 'static,
587{
588    /// Returns budget sources unconditionally and input-derived sources only
589    /// when detailed diagnostics retained them.
590    fn source(&self) -> Option<&(dyn Error + 'static)> {
591        match &self.failure {
592            JsonDecodeErrorSource::Read {
593                source: Some(source), ..
594            } => Some(source.as_ref()),
595            JsonDecodeErrorSource::Budget { source, .. } => Some(source),
596            JsonDecodeErrorSource::InvalidUtf8 {
597                source: Some(source), ..
598            } => Some(source),
599            JsonDecodeErrorSource::InvalidJson {
600                source: Some(source), ..
601            } => Some(source.as_ref()),
602            JsonDecodeErrorSource::Deserialize {
603                source: Some(source), ..
604            } => Some(source.as_ref()),
605            _ => None,
606        }
607    }
608}