use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum JsonBigNumbers {
#[default]
Fail,
String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InexactNumber {
pub text: String,
}
impl InexactNumber {
pub fn preview(&self) -> String {
let mut p: String = self.text.chars().take(24).collect();
if self.text.chars().count() > 24 {
p.push('…');
}
p
}
}
fn normalize(text: &str) -> Option<(bool, String, i64)> {
let (neg, rest) = match text.strip_prefix('-') {
Some(r) => (true, r),
None => (false, text),
};
let (mantissa, exp) = match rest.find(['e', 'E']) {
Some(i) => (&rest[..i], rest[i + 1..].parse::<i64>().ok()?),
None => (rest, 0),
};
let (int, frac) = mantissa.split_once('.').unwrap_or((mantissa, ""));
if int.is_empty() || !int.bytes().chain(frac.bytes()).all(|b| b.is_ascii_digit()) {
return None;
}
let mut digits = format!("{int}{frac}");
let mut exp = exp.checked_sub(frac.len() as i64)?;
let trimmed = digits.trim_start_matches('0').len();
digits.drain(..digits.len() - trimmed);
while digits.ends_with('0') {
digits.pop();
exp += 1;
}
if digits.is_empty() {
return Some((false, String::new(), 0));
}
Some((neg, digits, exp))
}
pub fn is_exact(text: &str) -> bool {
if short_plain_decimal(text) {
return true;
}
if text.parse::<i64>().is_ok() || text.parse::<u64>().is_ok() {
return true;
}
let Ok(Value::Number(n)) = serde_json::from_str::<Value>(text) else {
return false;
};
let exact = normalize(text);
exact.is_some() && exact == normalize(&n.to_string())
}
fn short_plain_decimal(text: &str) -> bool {
let body = text.strip_prefix('-').unwrap_or(text);
let (int, frac) = body.split_once('.').unwrap_or((body, "1"));
!int.is_empty()
&& !frac.is_empty()
&& int.len() + frac.len() <= 15
&& int.bytes().chain(frac.bytes()).all(|b| b.is_ascii_digit())
}
fn number_tokens(text: &str) -> Vec<std::ops::Range<usize>> {
let bytes = text.as_bytes();
let mut out = Vec::new();
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'"' => {
i += 1;
while i < bytes.len() && bytes[i] != b'"' {
i += if bytes[i] == b'\\' { 2 } else { 1 };
}
i += 1;
}
b'-' | b'0'..=b'9' => {
let start = i;
while i < bytes.len()
&& matches!(bytes[i], b'-' | b'+' | b'.' | b'e' | b'E' | b'0'..=b'9')
{
i += 1;
}
out.push(start..i);
}
_ => i += 1,
}
}
out
}
pub fn parse_json_exact(
text: &str,
mode: JsonBigNumbers,
) -> Result<(Value, Vec<InexactNumber>), JsonNumberError> {
let inexact: Vec<_> = number_tokens(text)
.into_iter()
.filter(|r| !is_exact(&text[r.clone()]))
.collect();
if inexact.is_empty() {
return serde_json::from_str(text)
.map(|v| (v, Vec::new()))
.map_err(|e| JsonNumberError::Invalid(e.to_string()));
}
let found: Vec<InexactNumber> = inexact
.iter()
.map(|r| InexactNumber {
text: text[r.clone()].to_string(),
})
.collect();
if mode == JsonBigNumbers::Fail {
return Err(JsonNumberError::Inexact(found));
}
let mut rewritten = String::with_capacity(text.len() + 2 * inexact.len());
let mut last = 0;
for r in &inexact {
rewritten.push_str(&text[last..r.start]);
rewritten.push('"');
rewritten.push_str(&text[r.clone()]);
rewritten.push('"');
last = r.end;
}
rewritten.push_str(&text[last..]);
serde_json::from_str(&rewritten)
.map(|v| (v, found))
.map_err(|e| JsonNumberError::Invalid(e.to_string()))
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum JsonNumberError {
#[error(
"{} number(s) cannot be held exactly, first {}",
.0.len(),
.0.first().map(InexactNumber::preview).unwrap_or_default()
)]
Inexact(Vec<InexactNumber>),
#[error("invalid JSON: {0}")]
Invalid(String),
}
impl std::fmt::Display for InexactNumber {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} cannot be held exactly", self.preview())
}
}
impl std::error::Error for InexactNumber {}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn errors_render_a_message() {
let n = InexactNumber {
text: "123456789012345678901234567890".into(),
};
assert_eq!(
n.to_string(),
"123456789012345678901234… cannot be held exactly"
);
let e = JsonNumberError::Inexact(vec![n.clone(), n]);
assert!(
e.to_string()
.starts_with("2 number(s) cannot be held exactly, first 1234"),
"{e}"
);
assert_eq!(
JsonNumberError::Invalid("eof".into()).to_string(),
"invalid JSON: eof"
);
let _: &dyn std::error::Error = &e;
}
#[test]
fn exactness_follows_the_float_serde_json_produces() {
for text in [
"1.0715660391465826e-75",
"-1.81996730402717e-179",
"-1.603964615428183e143",
"-9.643915712060552e-234",
"1.5860846119992697e-265",
"2.2250738585072011e-308",
] {
let parsed = parse_json_exact(text, JsonBigNumbers::String).unwrap().0;
match &parsed {
Value::Number(n) => {
assert!(is_exact(text), "{text}");
assert_eq!(normalize(&n.to_string()), normalize(text), "{text}");
}
Value::String(s) => {
assert!(!is_exact(text), "{text}");
assert_eq!(s, text);
}
other => panic!("{text} parsed to {other}"),
}
}
}
#[test]
fn exactness_compares_decimal_values() {
for exact in [
"0",
"-0",
"1",
"1.50",
"0.1",
"1e2",
"1E-2",
"9007199254740993",
"18446744073709551615",
"-9223372036854775808",
"123.456",
"1e300",
] {
assert!(is_exact(exact), "{exact}");
}
for inexact in [
"12345678901234567890.123",
"18446744073709551616",
"0.12345678901234567891",
"1e400",
"-",
] {
assert!(!is_exact(inexact), "{inexact}");
}
assert!(short_plain_decimal("-123456789.12345"));
assert!(!short_plain_decimal("1234567890.123456"));
assert!(!short_plain_decimal("1."));
assert!(!short_plain_decimal("-"));
assert!(!short_plain_decimal("1e5"));
assert_eq!(normalize("1.2e"), None);
assert_eq!(normalize("."), None);
assert_eq!(normalize("0.000"), Some((false, String::new(), 0)));
assert_eq!(normalize("-1.50"), Some((true, "15".to_string(), -1)));
}
#[test]
fn documents_fail_or_keep_big_numbers_as_strings() {
let doc = r#"{"a": 12345678901234567890.123, "s": "9.99999999999999999999 in a string", "b": [1, 2.5], "e": "\"q\\" }"#;
let failed = parse_json_exact(doc, JsonBigNumbers::Fail);
assert!(matches!(
&failed,
Err(JsonNumberError::Inexact(found))
if found.len() == 1 && found[0].preview() == "12345678901234567890.123"
));
let (v, found) = parse_json_exact(doc, JsonBigNumbers::String).unwrap();
assert_eq!(found.len(), 1);
assert_eq!(v["a"], json!("12345678901234567890.123"));
assert_eq!(v["b"], json!([1, 2.5]));
let (v, found) = parse_json_exact("[1.5]", JsonBigNumbers::Fail).unwrap();
assert!(found.is_empty());
assert_eq!(v, json!([1.5]));
assert!(matches!(
parse_json_exact("{", JsonBigNumbers::Fail),
Err(JsonNumberError::Invalid(_))
));
assert!(matches!(
parse_json_exact("[1e400,", JsonBigNumbers::String),
Err(JsonNumberError::Invalid(_))
));
let long = InexactNumber {
text: "1".repeat(30),
};
assert!(long.preview().ends_with('…'));
}
}