Skip to content

fix(serde): integer map keys must be exactly a JSON integer - #249

Open
yuxi-liu-wired wants to merge 2 commits into
cloudwego:mainfrom
yuxi-liu-wired:pr/numeric-map-keys
Open

yuxi-liu-wired wants to merge 2 commits into
cloudwego:mainfrom
yuxi-liu-wired:pr/numeric-map-keys

Conversation

@yuxi-liu-wired

Copy link
Copy Markdown
Contributor

Related fixes in one area, 2 commits (each with its own test).

fix: reject whitespace before a numeric map key

Map keys deserialized as i8..i64, u8..u64 or f64 went through
deserialize_number, which skips leading whitespace, so whitespace
inside the key's quotes was accepted and dropped:

from_str::<BTreeMap<u32, u8>>(r#"{" 1":2}"#)     Ok({1: 2})
from_str::<BTreeMap<i64, u8>>("{\"\t1\":2}")     Ok({1: 2})

although "\t1" is not even a valid JSON string (raw control
character), and " 1" is not the text of a number. serde_json rejects
both ("expected key to be a number in quotes"), and sonic-rs's own
i128/u128 key path already did.

The single-argument arm of deserialize_numeric_key! now makes the same
check as the two-argument arm: the first byte inside the quotes must be
a digit or -.

Test: test_numeric_key_rejects_leading_whitespace (fails before this
change).

fix: numeric map keys from a Value must be the whole key

MapKeyDeserializer (used by from_value and Value as a
Deserializer) parsed an integer key as a prefix of the key text and
ignored the rest:

let v: Value = from_str(r#"{"12abc":1,"1,2":2,"08":3}"#)?;
from_value::<BTreeMap<i64, u8>>(&v)    Ok({12: 1, 1: 2, 0: 3})

Deserializing the same JSON text into the same type is an error, and so
is serde_json's from_value. After the number, the key must be at its
end; otherwise ExpectedNumericKey.

Test: test_numeric_map_key_from_value_must_be_whole_number (fails
before this change).

Testing

Each commit adds a regression test that fails before the change. The full test suite passes and
cargo fmt --check is clean on the changed files.

Found with a differential fuzzer comparing serde_json, simd-json, sonic-rs and jiter.

@CLAassistant

CLAassistant commented Oct 1, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Map keys deserialized as i8..i64, u8..u64 or f64 went through
`deserialize_number`, which skips leading whitespace, so whitespace
inside the key's quotes was accepted and dropped:

    from_str::<BTreeMap<u32, u8>>(r#"{" 1":2}"#)     Ok({1: 2})
    from_str::<BTreeMap<i64, u8>>("{\"\t1\":2}")     Ok({1: 2})

although `"\t1"` is not even a valid JSON string (raw control
character), and `" 1"` is not the text of a number. serde_json rejects
both ("expected key to be a number in quotes"), and sonic-rs's own
i128/u128 key path already did.

The single-argument arm of `deserialize_numeric_key!` now makes the same
check as the two-argument arm: the first byte inside the quotes must be
a digit or `-`.

Test: `test_numeric_key_rejects_leading_whitespace` (fails before this
change).

Found by a three-way differential fuzzer (serde_json, simd-json,
sonic-rs).
`MapKeyDeserializer` (used by `from_value` and `Value` as a
`Deserializer`) parsed an integer key as a prefix of the key text and
ignored the rest:

    let v: Value = from_str(r#"{"12abc":1,"1,2":2,"08":3}"#)?;
    from_value::<BTreeMap<i64, u8>>(&v)    Ok({12: 1, 1: 2, 0: 3})

Deserializing the same JSON text into the same type is an error, and so
is serde_json's `from_value`. After the number, the key must be at its
end; otherwise `ExpectedNumericKey`.

Test: `test_numeric_map_key_from_value_must_be_whole_number` (fails
before this change).

Found by a differential fuzzer (serde_json, simd-json, sonic-rs, jiter).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants