Skip to main content

qubit_json/error/
json_decode_error.rs

1// =============================================================================
2//    Copyright (c) 2025 - 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 [`JsonDecodeError`] type used by the public decoder API.
9
10use std::{
11    error::Error,
12    fmt,
13    sync::Arc,
14};
15
16use crate::{
17    ErrorPrivacyPolicy,
18    JsonDecodeErrorKind,
19    JsonDecodeStage,
20    JsonTopLevelKind,
21};
22
23use super::internal::JsonInputSizeLimit;
24
25/// Error returned when lenient JSON decoding fails.
26///
27/// This type exposes immutable diagnostics so its error category, stage, and
28/// associated input metadata always remain consistent with one another.
29#[non_exhaustive]
30#[derive(Debug, Clone)]
31pub struct JsonDecodeError {
32    /// Stores the stable category of the decoding failure.
33    kind: JsonDecodeErrorKind,
34    /// Stores the pipeline stage that produced the failure.
35    stage: JsonDecodeStage,
36    /// Stores the privacy policy applied while constructing diagnostics.
37    privacy_policy: ErrorPrivacyPolicy,
38    /// Stores the human-readable diagnostic message.
39    message: String,
40    /// Stores the expected constrained top-level kind, when applicable.
41    expected_top_level: Option<JsonTopLevelKind>,
42    /// Stores the actual constrained top-level kind, when applicable.
43    actual_top_level: Option<JsonTopLevelKind>,
44    /// Stores the input byte length before normalization.
45    raw_input_bytes: usize,
46    /// Stores the normalized byte length when normalization completed.
47    normalized_input_bytes: Option<usize>,
48    /// Stores the configured size limit that rejected the input.
49    size_limit: Option<JsonInputSizeLimit>,
50    /// Stores the one-based parser line in normalized text when available.
51    normalized_line: Option<usize>,
52    /// Stores the one-based parser column in normalized text when available.
53    normalized_column: Option<usize>,
54    /// Stores internal error details exposed only when diagnostics permit it.
55    source: Option<Arc<dyn Error + Send + Sync>>,
56}
57
58impl JsonDecodeError {
59    /// Creates an error for raw bytes that are not valid UTF-8.
60    ///
61    /// # Parameters
62    ///
63    /// * `error` - UTF-8 validation error.
64    /// * `raw_input_bytes` - Raw input length in bytes.
65    /// * `privacy_policy` - Policy controlling retained diagnostics.
66    ///
67    /// # Returns
68    ///
69    /// An invalid-UTF-8 error that exposes the source only in detailed mode.
70    #[must_use]
71    pub(crate) fn invalid_utf8(
72        error: std::str::Utf8Error,
73        raw_input_bytes: usize,
74        privacy_policy: ErrorPrivacyPolicy,
75    ) -> Self {
76        let utf8_error = Arc::new(error);
77        let message = match privacy_policy {
78            ErrorPrivacyPolicy::Redacted => {
79                "Failed to decode JSON input as UTF-8".to_string()
80            }
81            ErrorPrivacyPolicy::Detailed => {
82                format!("Failed to decode JSON input as UTF-8: {utf8_error}")
83            }
84        };
85        let source = Some(utf8_error as Arc<dyn Error + Send + Sync>);
86        Self {
87            kind: JsonDecodeErrorKind::InvalidUtf8,
88            stage: JsonDecodeStage::DecodeText,
89            privacy_policy,
90            message,
91            expected_top_level: None,
92            actual_top_level: None,
93            raw_input_bytes,
94            normalized_input_bytes: None,
95            size_limit: None,
96            normalized_line: None,
97            normalized_column: None,
98            source,
99        }
100    }
101
102    /// Creates an error for raw input that exceeds the configured size limit.
103    ///
104    /// # Parameters
105    ///
106    /// * `raw_input_bytes` - Raw input length in bytes.
107    /// * `max_input_bytes` - Configured maximum raw input length.
108    /// * `privacy_policy` - Privacy policy active during normalization.
109    ///
110    /// # Returns
111    ///
112    /// An input-too-large error containing the supplied size diagnostics.
113    #[must_use]
114    pub(crate) fn input_too_large(
115        raw_input_bytes: usize,
116        max_input_bytes: usize,
117        privacy_policy: ErrorPrivacyPolicy,
118    ) -> Self {
119        Self {
120            kind: JsonDecodeErrorKind::InputTooLarge,
121            stage: JsonDecodeStage::Normalize,
122            privacy_policy,
123            message: format!(
124                "JSON input is too large: {raw_input_bytes} bytes exceed configured limit {max_input_bytes} bytes"
125            ),
126            expected_top_level: None,
127            actual_top_level: None,
128            raw_input_bytes,
129            normalized_input_bytes: None,
130            size_limit: Some(JsonInputSizeLimit::Raw(max_input_bytes)),
131            normalized_line: None,
132            normalized_column: None,
133            source: None,
134        }
135    }
136
137    /// Creates an error for normalized input that exceeds its configured limit.
138    ///
139    /// # Parameters
140    ///
141    /// * `raw_input_bytes` - Raw input length in bytes.
142    /// * `normalized_input_bytes` - Calculated normalized input length in
143    ///   bytes.
144    /// * `max_normalized_bytes` - Configured maximum normalized input length.
145    /// * `privacy_policy` - Privacy policy active during normalization.
146    ///
147    /// # Returns
148    ///
149    /// An input-too-large error containing the supplied normalized size
150    /// diagnostics.
151    #[must_use]
152    pub(crate) fn normalized_input_too_large(
153        raw_input_bytes: usize,
154        normalized_input_bytes: usize,
155        max_normalized_bytes: usize,
156        privacy_policy: ErrorPrivacyPolicy,
157    ) -> Self {
158        Self {
159            kind: JsonDecodeErrorKind::InputTooLarge,
160            stage: JsonDecodeStage::Normalize,
161            privacy_policy,
162            message: format!(
163                "Normalized JSON input is too large: {normalized_input_bytes} bytes exceed configured limit {max_normalized_bytes} bytes"
164            ),
165            expected_top_level: None,
166            actual_top_level: None,
167            raw_input_bytes,
168            normalized_input_bytes: Some(normalized_input_bytes),
169            size_limit: Some(JsonInputSizeLimit::Normalized(
170                max_normalized_bytes,
171            )),
172            normalized_line: None,
173            normalized_column: None,
174            source: None,
175        }
176    }
177
178    /// Creates an error for input that is empty at a normalization boundary.
179    ///
180    /// # Parameters
181    ///
182    /// * `raw_input_bytes` - Raw input length in bytes.
183    /// * `normalized_input_bytes` - Normalized length when normalization
184    ///   completed, or `None` when the input was rejected earlier.
185    /// * `privacy_policy` - Privacy policy active during normalization.
186    ///
187    /// # Returns
188    ///
189    /// An empty-input error containing the available size diagnostics.
190    #[must_use]
191    pub(crate) fn empty_input(
192        raw_input_bytes: usize,
193        normalized_input_bytes: Option<usize>,
194        privacy_policy: ErrorPrivacyPolicy,
195    ) -> Self {
196        Self {
197            kind: JsonDecodeErrorKind::EmptyInput,
198            stage: JsonDecodeStage::Normalize,
199            privacy_policy,
200            message: "JSON input is empty after normalization".to_string(),
201            expected_top_level: None,
202            actual_top_level: None,
203            raw_input_bytes,
204            normalized_input_bytes,
205            size_limit: None,
206            normalized_line: None,
207            normalized_column: None,
208            source: None,
209        }
210    }
211
212    /// Creates an error for invalid normalized JSON syntax.
213    ///
214    /// # Parameters
215    ///
216    /// * `error` - Serde parser error.
217    /// * `raw_input_bytes` - Raw input length in bytes.
218    /// * `normalized_input_bytes` - Normalized input length in bytes.
219    /// * `privacy_policy` - Policy controlling retained serde diagnostics.
220    ///
221    /// # Returns
222    ///
223    /// An invalid-JSON error with stable location and size metadata.
224    #[inline(always)]
225    #[must_use]
226    pub(crate) fn invalid_json(
227        error: serde_json::Error,
228        raw_input_bytes: usize,
229        normalized_input_bytes: usize,
230        privacy_policy: ErrorPrivacyPolicy,
231    ) -> Self {
232        Self::from_serde_error(
233            JsonDecodeErrorKind::InvalidJson,
234            JsonDecodeStage::Parse,
235            "Failed to parse JSON",
236            error,
237            raw_input_bytes,
238            normalized_input_bytes,
239            privacy_policy,
240        )
241    }
242
243    /// Creates an error for a valid JSON value with an unexpected top-level
244    /// kind.
245    ///
246    /// # Parameters
247    ///
248    /// * `expected` - Required top-level kind.
249    /// * `actual` - Observed top-level kind.
250    /// * `raw_input_bytes` - Raw input length in bytes.
251    /// * `normalized_input_bytes` - Normalized input length in bytes.
252    /// * `privacy_policy` - Privacy policy active during decoding.
253    ///
254    /// # Returns
255    ///
256    /// A top-level-check error containing the expected and actual kinds.
257    #[must_use]
258    pub(crate) fn unexpected_top_level(
259        expected: JsonTopLevelKind,
260        actual: JsonTopLevelKind,
261        raw_input_bytes: usize,
262        normalized_input_bytes: usize,
263        privacy_policy: ErrorPrivacyPolicy,
264    ) -> Self {
265        Self {
266            kind: JsonDecodeErrorKind::UnexpectedTopLevel,
267            stage: JsonDecodeStage::TopLevelCheck,
268            privacy_policy,
269            message: format!(
270                "Unexpected JSON top-level type: expected {expected}, got {actual}"
271            ),
272            expected_top_level: Some(expected),
273            actual_top_level: Some(actual),
274            raw_input_bytes,
275            normalized_input_bytes: Some(normalized_input_bytes),
276            size_limit: None,
277            normalized_line: None,
278            normalized_column: None,
279            source: None,
280        }
281    }
282
283    /// Creates an error for valid JSON that cannot deserialize into the target.
284    ///
285    /// # Parameters
286    ///
287    /// * `error` - Serde deserialization error.
288    /// * `raw_input_bytes` - Raw input length in bytes.
289    /// * `normalized_input_bytes` - Normalized input length in bytes.
290    /// * `privacy_policy` - Policy controlling retained serde diagnostics.
291    ///
292    /// # Returns
293    ///
294    /// A deserialization error with stable location and size metadata.
295    #[inline(always)]
296    #[must_use]
297    pub(crate) fn deserialize(
298        error: serde_json::Error,
299        raw_input_bytes: usize,
300        normalized_input_bytes: usize,
301        privacy_policy: ErrorPrivacyPolicy,
302    ) -> Self {
303        Self::from_serde_error(
304            JsonDecodeErrorKind::Deserialize,
305            JsonDecodeStage::Deserialize,
306            "Failed to deserialize JSON value",
307            error,
308            raw_input_bytes,
309            normalized_input_bytes,
310            privacy_policy,
311        )
312    }
313
314    /// Creates a decoder error from a serde error and privacy policy.
315    ///
316    /// # Parameters
317    ///
318    /// * `kind` - Stable public error category.
319    /// * `stage` - Decoder stage that produced the error.
320    /// * `prefix` - Stable message prefix.
321    /// * `error` - Serde error carrying location and optional details.
322    /// * `raw_input_bytes` - Raw input length in bytes.
323    /// * `normalized_input_bytes` - Normalized input length in bytes.
324    /// * `privacy_policy` - Policy controlling retained serde diagnostics.
325    ///
326    /// # Returns
327    ///
328    /// A decoder error that always retains stable metadata and retains the
329    /// serde source only under the detailed privacy policy.
330    #[must_use]
331    fn from_serde_error(
332        kind: JsonDecodeErrorKind,
333        stage: JsonDecodeStage,
334        prefix: &str,
335        error: serde_json::Error,
336        raw_input_bytes: usize,
337        normalized_input_bytes: usize,
338        privacy_policy: ErrorPrivacyPolicy,
339    ) -> Self {
340        let line = error.line();
341        let column = error.column();
342        let (message, source) = match privacy_policy {
343            ErrorPrivacyPolicy::Redacted => {
344                (Self::redacted_message(prefix, line, column), None)
345            }
346            ErrorPrivacyPolicy::Detailed => (
347                format!("{prefix}: {error}"),
348                Some(Arc::new(error) as Arc<dyn Error + Send + Sync>),
349            ),
350        };
351        Self {
352            kind,
353            stage,
354            privacy_policy,
355            message,
356            expected_top_level: None,
357            actual_top_level: None,
358            raw_input_bytes,
359            normalized_input_bytes: Some(normalized_input_bytes),
360            size_limit: None,
361            normalized_line: (line > 0).then_some(line),
362            normalized_column: (column > 0).then_some(column),
363            source,
364        }
365    }
366
367    /// Returns the stable category of this decoding failure.
368    ///
369    /// # Returns
370    ///
371    /// The stable error category.
372    #[inline(always)]
373    pub const fn kind(&self) -> JsonDecodeErrorKind {
374        self.kind
375    }
376
377    /// Returns the decoding stage that produced this error.
378    ///
379    /// # Returns
380    ///
381    /// The decoder stage where the failure occurred.
382    #[inline(always)]
383    pub const fn stage(&self) -> JsonDecodeStage {
384        self.stage
385    }
386
387    /// Returns the privacy policy applied when this error was constructed.
388    ///
389    /// # Returns
390    ///
391    /// The effective error privacy policy.
392    #[inline(always)]
393    pub const fn privacy_policy(&self) -> ErrorPrivacyPolicy {
394        self.privacy_policy
395    }
396
397    /// Returns the human-readable diagnostic message.
398    ///
399    /// # Returns
400    ///
401    /// The stable redacted message or explicitly requested detailed message.
402    #[inline(always)]
403    #[must_use]
404    pub fn message(&self) -> &str {
405        &self.message
406    }
407
408    /// Returns the required top-level JSON kind for a constrained decode.
409    ///
410    /// # Returns
411    ///
412    /// `Some(kind)` when constrained decoding rejected a valid top-level
413    /// value; otherwise, `None`.
414    #[inline(always)]
415    pub const fn expected_top_level(&self) -> Option<JsonTopLevelKind> {
416        self.expected_top_level
417    }
418
419    /// Returns the observed top-level JSON kind for a constrained decode.
420    ///
421    /// # Returns
422    ///
423    /// `Some(kind)` when constrained decoding rejected a valid top-level
424    /// value; otherwise, `None`.
425    #[inline(always)]
426    pub const fn actual_top_level(&self) -> Option<JsonTopLevelKind> {
427        self.actual_top_level
428    }
429
430    /// Returns the byte length of the input before normalization.
431    ///
432    /// # Returns
433    ///
434    /// The raw input length in bytes.
435    #[inline(always)]
436    #[must_use]
437    pub const fn raw_input_bytes(&self) -> usize {
438        self.raw_input_bytes
439    }
440
441    /// Returns the valid UTF-8 prefix length for invalid byte input.
442    ///
443    /// # Returns
444    ///
445    /// `Some(length)` for [`JsonDecodeErrorKind::InvalidUtf8`], including zero
446    /// when the first byte is invalid; otherwise, `None`.
447    #[inline(always)]
448    pub fn utf8_valid_up_to(&self) -> Option<usize> {
449        self.source
450            .as_deref()
451            .and_then(|error| error.downcast_ref::<std::str::Utf8Error>())
452            .map(std::str::Utf8Error::valid_up_to)
453    }
454
455    /// Returns the known length of the invalid UTF-8 sequence.
456    ///
457    /// # Returns
458    ///
459    /// `Some(length)` when the invalid sequence length is known, or `None` for
460    /// an incomplete trailing sequence and for non-UTF-8 errors.
461    #[inline(always)]
462    pub fn utf8_error_len(&self) -> Option<usize> {
463        self.source
464            .as_deref()
465            .and_then(|error| error.downcast_ref::<std::str::Utf8Error>())
466            .and_then(std::str::Utf8Error::error_len)
467    }
468
469    /// Returns the byte length of normalized JSON text.
470    ///
471    /// # Returns
472    ///
473    /// `Some(length)` when normalization completed before the failure, or
474    /// `None` when the input was rejected before a normalized length existed.
475    #[inline(always)]
476    pub const fn normalized_input_bytes(&self) -> Option<usize> {
477        self.normalized_input_bytes
478    }
479
480    /// Returns the configured raw-input limit for a size failure.
481    ///
482    /// # Returns
483    ///
484    /// `Some(limit)` when raw input exceeded its configured limit, or `None`
485    /// for normalized-size and non-size failures.
486    #[inline(always)]
487    pub const fn max_input_bytes(&self) -> Option<usize> {
488        match self.size_limit {
489            Some(JsonInputSizeLimit::Raw(limit)) => Some(limit),
490            Some(JsonInputSizeLimit::Normalized(_)) | None => None,
491        }
492    }
493
494    /// Returns the configured normalized-input limit for a size failure.
495    ///
496    /// # Returns
497    ///
498    /// `Some(limit)` when normalized JSON exceeded its configured limit, or
499    /// `None` for raw-size and non-size failures.
500    #[inline(always)]
501    pub const fn max_normalized_bytes(&self) -> Option<usize> {
502        match self.size_limit {
503            Some(JsonInputSizeLimit::Normalized(limit)) => Some(limit),
504            Some(JsonInputSizeLimit::Raw(_)) | None => None,
505        }
506    }
507
508    /// Returns the parser line in normalized JSON text.
509    ///
510    /// # Returns
511    ///
512    /// `Some(line)` with a one-based line number when serde reported one, or
513    /// `None` when no parser location is available.
514    #[inline(always)]
515    pub const fn normalized_line(&self) -> Option<usize> {
516        self.normalized_line
517    }
518
519    /// Returns the parser column in normalized JSON text.
520    ///
521    /// # Returns
522    ///
523    /// `Some(column)` with a one-based column number when serde reported one,
524    /// or `None` when no parser location is available.
525    #[inline(always)]
526    pub const fn normalized_column(&self) -> Option<usize> {
527        self.normalized_column
528    }
529
530    /// Builds a diagnostic that contains only stable text and parser location.
531    ///
532    /// # Parameters
533    ///
534    /// * `prefix` - Stable error-message prefix.
535    /// * `line` - One-based parser line, or zero when unavailable.
536    /// * `column` - One-based parser column, or zero when unavailable.
537    ///
538    /// # Returns
539    ///
540    /// A message containing the prefix and each available normalized location.
541    #[must_use]
542    fn redacted_message(prefix: &str, line: usize, column: usize) -> String {
543        match (line > 0, column > 0) {
544            (true, true) => {
545                format!("{prefix} at normalized line {line} column {column}")
546            }
547            (true, false) => {
548                format!("{prefix} at normalized line {line}")
549            }
550            (false, true) => {
551                format!("{prefix} at normalized column {column}")
552            }
553            (false, false) => prefix.to_string(),
554        }
555    }
556}
557
558impl PartialEq for JsonDecodeError {
559    /// Compares all stable error fields while ignoring the retained source.
560    ///
561    /// # Parameters
562    ///
563    /// * `other` - Error to compare with this error.
564    ///
565    /// # Returns
566    ///
567    /// `true` when every stable diagnostic field is equal; otherwise, `false`.
568    fn eq(&self, other: &Self) -> bool {
569        self.kind == other.kind
570            && self.stage == other.stage
571            && self.privacy_policy == other.privacy_policy
572            && self.message == other.message
573            && self.expected_top_level == other.expected_top_level
574            && self.actual_top_level == other.actual_top_level
575            && self.raw_input_bytes == other.raw_input_bytes
576            && self.utf8_valid_up_to() == other.utf8_valid_up_to()
577            && self.utf8_error_len() == other.utf8_error_len()
578            && self.normalized_input_bytes == other.normalized_input_bytes
579            && self.size_limit == other.size_limit
580            && self.normalized_line == other.normalized_line
581            && self.normalized_column == other.normalized_column
582    }
583}
584
585impl Eq for JsonDecodeError {}
586
587impl fmt::Display for JsonDecodeError {
588    /// Writes the configured human-readable diagnostic message.
589    ///
590    /// # Parameters
591    ///
592    /// * `f` - Destination formatter.
593    ///
594    /// # Returns
595    ///
596    /// `Ok(())` when the message is written successfully.
597    ///
598    /// # Errors
599    ///
600    /// Returns a formatting error when the destination formatter rejects the
601    /// write.
602    #[inline(always)]
603    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
604        f.write_str(&self.message)
605    }
606}
607
608impl Error for JsonDecodeError {
609    /// Returns the decoder source under detailed privacy mode.
610    ///
611    /// # Returns
612    ///
613    /// `Some(source)` when detailed diagnostics expose a UTF-8 or serde
614    /// error, or `None` for redacted and source-free failures.
615    #[inline(always)]
616    fn source(&self) -> Option<&(dyn Error + 'static)> {
617        match self.privacy_policy {
618            ErrorPrivacyPolicy::Redacted => None,
619            ErrorPrivacyPolicy::Detailed => self
620                .source
621                .as_deref()
622                .map(|error| error as &(dyn Error + 'static)),
623        }
624    }
625}