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