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}