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 {
82 if short_plain_decimal(text) {
83 return true;
84 }
85 if text.parse::<i64>().is_ok() || text.parse::<u64>().is_ok() {
86 return true;
87 }
88 let Ok(Value::Number(n)) = serde_json::from_str::<Value>(text) else {
89 return false;
90 };
91 let exact = normalize(text);
92 exact.is_some() && exact == normalize(&n.to_string())
93}
94
95fn short_plain_decimal(text: &str) -> bool {
98 let body = text.strip_prefix('-').unwrap_or(text);
99 let (int, frac) = body.split_once('.').unwrap_or((body, "1"));
100 !int.is_empty()
101 && !frac.is_empty()
102 && int.len() + frac.len() <= 15
103 && int.bytes().chain(frac.bytes()).all(|b| b.is_ascii_digit())
104}
105
106fn number_tokens(text: &str) -> Vec<std::ops::Range<usize>> {
108 let bytes = text.as_bytes();
109 let mut out = Vec::new();
110 let mut i = 0;
111 while i < bytes.len() {
112 match bytes[i] {
113 b'"' => {
114 i += 1;
115 while i < bytes.len() && bytes[i] != b'"' {
116 i += if bytes[i] == b'\\' { 2 } else { 1 };
117 }
118 i += 1;
119 }
120 b'-' | b'0'..=b'9' => {
121 let start = i;
122 while i < bytes.len()
123 && matches!(bytes[i], b'-' | b'+' | b'.' | b'e' | b'E' | b'0'..=b'9')
124 {
125 i += 1;
126 }
127 out.push(start..i);
128 }
129 _ => i += 1,
130 }
131 }
132 out
133}
134
135pub fn parse_json_exact(
139 text: &str,
140 mode: JsonBigNumbers,
141) -> Result<(Value, Vec<InexactNumber>), JsonNumberError> {
142 let inexact: Vec<_> = number_tokens(text)
143 .into_iter()
144 .filter(|r| !is_exact(&text[r.clone()]))
145 .collect();
146 if inexact.is_empty() {
147 return serde_json::from_str(text)
148 .map(|v| (v, Vec::new()))
149 .map_err(|e| JsonNumberError::Invalid(e.to_string()));
150 }
151 let found: Vec<InexactNumber> = inexact
152 .iter()
153 .map(|r| InexactNumber {
154 text: text[r.clone()].to_string(),
155 })
156 .collect();
157 if mode == JsonBigNumbers::Fail {
158 return Err(JsonNumberError::Inexact(found));
159 }
160 let mut rewritten = String::with_capacity(text.len() + 2 * inexact.len());
161 let mut last = 0;
162 for r in &inexact {
163 rewritten.push_str(&text[last..r.start]);
164 rewritten.push('"');
165 rewritten.push_str(&text[r.clone()]);
166 rewritten.push('"');
167 last = r.end;
168 }
169 rewritten.push_str(&text[last..]);
170 serde_json::from_str(&rewritten)
171 .map(|v| (v, found))
172 .map_err(|e| JsonNumberError::Invalid(e.to_string()))
173}
174
175#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
177pub enum JsonNumberError {
178 #[error(
180 "{} number(s) cannot be held exactly, first {}",
181 .0.len(),
182 .0.first().map(InexactNumber::preview).unwrap_or_default()
183 )]
184 Inexact(Vec<InexactNumber>),
185 #[error("invalid JSON: {0}")]
187 Invalid(String),
188}
189
190impl std::fmt::Display for InexactNumber {
191 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192 write!(f, "{} cannot be held exactly", self.preview())
193 }
194}
195
196impl std::error::Error for InexactNumber {}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use serde_json::json;
202
203 #[test]
204 fn errors_render_a_message() {
205 let n = InexactNumber {
206 text: "123456789012345678901234567890".into(),
207 };
208 assert_eq!(
209 n.to_string(),
210 "123456789012345678901234… cannot be held exactly"
211 );
212 let e = JsonNumberError::Inexact(vec![n.clone(), n]);
213 assert!(
214 e.to_string()
215 .starts_with("2 number(s) cannot be held exactly, first 1234"),
216 "{e}"
217 );
218 assert_eq!(
219 JsonNumberError::Invalid("eof".into()).to_string(),
220 "invalid JSON: eof"
221 );
222 let _: &dyn std::error::Error = &e;
223 }
224
225 #[test]
226 fn exactness_follows_the_float_serde_json_produces() {
227 for text in [
228 "1.0715660391465826e-75",
229 "-1.81996730402717e-179",
230 "-1.603964615428183e143",
231 "-9.643915712060552e-234",
232 "1.5860846119992697e-265",
233 "2.2250738585072011e-308",
234 ] {
235 let parsed = parse_json_exact(text, JsonBigNumbers::String).unwrap().0;
236 match &parsed {
237 Value::Number(n) => {
238 assert!(is_exact(text), "{text}");
239 assert_eq!(normalize(&n.to_string()), normalize(text), "{text}");
240 }
241 Value::String(s) => {
242 assert!(!is_exact(text), "{text}");
243 assert_eq!(s, text);
244 }
245 other => panic!("{text} parsed to {other}"),
246 }
247 }
248 }
249
250 #[test]
251 fn exactness_compares_decimal_values() {
252 for exact in [
253 "0",
254 "-0",
255 "1",
256 "1.50",
257 "0.1",
258 "1e2",
259 "1E-2",
260 "9007199254740993",
261 "18446744073709551615",
262 "-9223372036854775808",
263 "123.456",
264 "1e300",
265 ] {
266 assert!(is_exact(exact), "{exact}");
267 }
268 for inexact in [
269 "12345678901234567890.123",
270 "18446744073709551616",
271 "0.12345678901234567891",
272 "1e400",
273 "-",
274 ] {
275 assert!(!is_exact(inexact), "{inexact}");
276 }
277 assert!(short_plain_decimal("-123456789.12345"));
278 assert!(!short_plain_decimal("1234567890.123456"));
279 assert!(!short_plain_decimal("1."));
280 assert!(!short_plain_decimal("-"));
281 assert!(!short_plain_decimal("1e5"));
282 assert_eq!(normalize("1.2e"), None);
283 assert_eq!(normalize("."), None);
284 assert_eq!(normalize("0.000"), Some((false, String::new(), 0)));
285 assert_eq!(normalize("-1.50"), Some((true, "15".to_string(), -1)));
286 }
287
288 #[test]
289 fn documents_fail_or_keep_big_numbers_as_strings() {
290 let doc = r#"{"a": 12345678901234567890.123, "s": "9.99999999999999999999 in a string", "b": [1, 2.5], "e": "\"q\\" }"#;
291 let failed = parse_json_exact(doc, JsonBigNumbers::Fail);
292 assert!(matches!(
293 &failed,
294 Err(JsonNumberError::Inexact(found))
295 if found.len() == 1 && found[0].preview() == "12345678901234567890.123"
296 ));
297 let (v, found) = parse_json_exact(doc, JsonBigNumbers::String).unwrap();
298 assert_eq!(found.len(), 1);
299 assert_eq!(v["a"], json!("12345678901234567890.123"));
300 assert_eq!(v["b"], json!([1, 2.5]));
301 let (v, found) = parse_json_exact("[1.5]", JsonBigNumbers::Fail).unwrap();
302 assert!(found.is_empty());
303 assert_eq!(v, json!([1.5]));
304 assert!(matches!(
305 parse_json_exact("{", JsonBigNumbers::Fail),
306 Err(JsonNumberError::Invalid(_))
307 ));
308 assert!(matches!(
309 parse_json_exact("[1e400,", JsonBigNumbers::String),
310 Err(JsonNumberError::Invalid(_))
311 ));
312 let long = InexactNumber {
313 text: "1".repeat(30),
314 };
315 assert!(long.preview().ends_with('…'));
316 }
317}