1use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13#[derive(
16 Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
17)]
18#[serde(rename_all = "snake_case")]
19pub enum JsonBigNumbers {
20 #[default]
23 Fail,
24 String,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct InexactNumber {
31 pub text: String,
33}
34
35impl InexactNumber {
36 pub fn preview(&self) -> String {
38 let mut p: String = self.text.chars().take(24).collect();
39 if self.text.chars().count() > 24 {
40 p.push('…');
41 }
42 p
43 }
44}
45
46fn normalize(text: &str) -> Option<(bool, String, i64)> {
49 let (neg, rest) = match text.strip_prefix('-') {
50 Some(r) => (true, r),
51 None => (false, text),
52 };
53 let (mantissa, exp) = match rest.find(['e', 'E']) {
54 Some(i) => (&rest[..i], rest[i + 1..].parse::<i64>().ok()?),
55 None => (rest, 0),
56 };
57 let (int, frac) = mantissa.split_once('.').unwrap_or((mantissa, ""));
58 if int.is_empty() || !int.bytes().chain(frac.bytes()).all(|b| b.is_ascii_digit()) {
59 return None;
60 }
61 let mut digits = format!("{int}{frac}");
62 let mut exp = exp.checked_sub(frac.len() as i64)?;
63 let trimmed = digits.trim_start_matches('0').len();
64 digits.drain(..digits.len() - trimmed);
65 while digits.ends_with('0') {
66 digits.pop();
67 exp += 1;
68 }
69 if digits.is_empty() {
70 return Some((false, String::new(), 0));
71 }
72 Some((neg, digits, exp))
73}
74
75pub fn is_exact(text: &str) -> bool {
78 if text.parse::<i64>().is_ok() || text.parse::<u64>().is_ok() {
79 return true;
80 }
81 let Ok(f) = text.parse::<f64>() else {
82 return false;
83 };
84 let Some(n) = serde_json::Number::from_f64(f) else {
85 return false;
86 };
87 normalize(text).is_some() && normalize(text) == normalize(&n.to_string())
88}
89
90fn number_tokens(text: &str) -> Vec<std::ops::Range<usize>> {
92 let bytes = text.as_bytes();
93 let mut out = Vec::new();
94 let mut i = 0;
95 while i < bytes.len() {
96 match bytes[i] {
97 b'"' => {
98 i += 1;
99 while i < bytes.len() && bytes[i] != b'"' {
100 i += if bytes[i] == b'\\' { 2 } else { 1 };
101 }
102 i += 1;
103 }
104 b'-' | b'0'..=b'9' => {
105 let start = i;
106 while i < bytes.len()
107 && matches!(bytes[i], b'-' | b'+' | b'.' | b'e' | b'E' | b'0'..=b'9')
108 {
109 i += 1;
110 }
111 out.push(start..i);
112 }
113 _ => i += 1,
114 }
115 }
116 out
117}
118
119pub fn parse_json_exact(
123 text: &str,
124 mode: JsonBigNumbers,
125) -> Result<(Value, Vec<InexactNumber>), JsonNumberError> {
126 let inexact: Vec<_> = number_tokens(text)
127 .into_iter()
128 .filter(|r| !is_exact(&text[r.clone()]))
129 .collect();
130 if inexact.is_empty() {
131 return serde_json::from_str(text)
132 .map(|v| (v, Vec::new()))
133 .map_err(|e| JsonNumberError::Invalid(e.to_string()));
134 }
135 let found: Vec<InexactNumber> = inexact
136 .iter()
137 .map(|r| InexactNumber {
138 text: text[r.clone()].to_string(),
139 })
140 .collect();
141 if mode == JsonBigNumbers::Fail {
142 return Err(JsonNumberError::Inexact(found));
143 }
144 let mut rewritten = String::with_capacity(text.len() + 2 * inexact.len());
145 let mut last = 0;
146 for r in &inexact {
147 rewritten.push_str(&text[last..r.start]);
148 rewritten.push('"');
149 rewritten.push_str(&text[r.clone()]);
150 rewritten.push('"');
151 last = r.end;
152 }
153 rewritten.push_str(&text[last..]);
154 serde_json::from_str(&rewritten)
155 .map(|v| (v, found))
156 .map_err(|e| JsonNumberError::Invalid(e.to_string()))
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
161pub enum JsonNumberError {
162 Inexact(Vec<InexactNumber>),
164 Invalid(String),
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use serde_json::json;
172
173 #[test]
174 fn exactness_compares_decimal_values() {
175 for exact in [
176 "0",
177 "-0",
178 "1",
179 "1.50",
180 "0.1",
181 "1e2",
182 "1E-2",
183 "9007199254740993",
184 "18446744073709551615",
185 "-9223372036854775808",
186 "123.456",
187 "1e300",
188 ] {
189 assert!(is_exact(exact), "{exact}");
190 }
191 for inexact in [
192 "12345678901234567890.123",
193 "18446744073709551616",
194 "0.12345678901234567891",
195 "1e400",
196 "-",
197 ] {
198 assert!(!is_exact(inexact), "{inexact}");
199 }
200 assert_eq!(normalize("1.2e"), None);
201 assert_eq!(normalize("."), None);
202 assert_eq!(normalize("0.000"), Some((false, String::new(), 0)));
203 assert_eq!(normalize("-1.50"), Some((true, "15".to_string(), -1)));
204 }
205
206 #[test]
207 fn documents_fail_or_keep_big_numbers_as_strings() {
208 let doc = r#"{"a": 12345678901234567890.123, "s": "9.99999999999999999999 in a string", "b": [1, 2.5], "e": "\"q\\" }"#;
209 let failed = parse_json_exact(doc, JsonBigNumbers::Fail);
210 assert!(matches!(
211 &failed,
212 Err(JsonNumberError::Inexact(found))
213 if found.len() == 1 && found[0].preview() == "12345678901234567890.123"
214 ));
215 let (v, found) = parse_json_exact(doc, JsonBigNumbers::String).unwrap();
216 assert_eq!(found.len(), 1);
217 assert_eq!(v["a"], json!("12345678901234567890.123"));
218 assert_eq!(v["b"], json!([1, 2.5]));
219 let (v, found) = parse_json_exact("[1.5]", JsonBigNumbers::Fail).unwrap();
220 assert!(found.is_empty());
221 assert_eq!(v, json!([1.5]));
222 assert!(matches!(
223 parse_json_exact("{", JsonBigNumbers::Fail),
224 Err(JsonNumberError::Invalid(_))
225 ));
226 assert!(matches!(
227 parse_json_exact("[1e400,", JsonBigNumbers::String),
228 Err(JsonNumberError::Invalid(_))
229 ));
230 let long = InexactNumber {
231 text: "1".repeat(30),
232 };
233 assert!(long.preview().ends_with('…'));
234 }
235}