You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
json: an unquoted float value is rounded twice, so it can differ from the same value quoted #647
The decimal is just above 1 + 2⁻²⁴, the midpoint of two adjacent f32 values, so its nearest f32 is 1.0000001. The unquoted path receives the number from serde as an f64, which is the midpoint exactly, and v as f32 rounds the tie to even: 1.0 (json_helpers::float::deserialize, visit_f64, buffa/src/json_helpers.rs:1280-1292). The quoted path parses the text with str::parse::<f32> and rounds once, as the text format does since #520. The reflective parser calls the same helper (buffa-descriptor/src/reflect/json.rs:1038).
protobuf-go rounds once (strconv.ParseFloat(raw, 32), internal/encoding/json/decode_token.go:133). C++ parses a double and narrows (src/google/protobuf/json/internal/parser.cc:271-311), as buffa does.
A serde Visitor sees only the f64, so rounding once needs the token text: serde_json's arbitrary_precision (see #482) or a raw-value capture. If that cost is not wanted, the rustdoc on json_helpers::float should say that an unquoted number is rounded through f64.
As the issue notes, the visitor only sees the f64, and when that value is exactly an f32 midpoint the original decimal could have been on either side of it, so there's nothing to correct after the fact. Rounding once needs the token text, and both routes to it have costs: arbitrary_precision is blocked on #482, and a raw-value capture would tie the helper to serde_json while json_helpers stays generic over Deserializer.
Plan, unless you'd prefer a different route: document the behaviour on json_helpers::float (an unquoted number is parsed as f64 and then narrowed, so a decimal within half an f64 ulp of an f32 midpoint can round differently from the same value quoted; quote the value to get single rounding). I'll add the same note at the reflective parser's call site (buffa-descriptor/src/reflect/json.rs:1038), add a test that pins both paths using the example from this issue, and add a changelog fragment.
If #482 ends up supporting arbitrary_precision, the number-as-text branch could parse straight to f32 and fix this properly. I'm happy to follow up on that if it's wanted.
Activity
farmer-data commentedon Oct 12, 2026
I'd like to take this one.
As the issue notes, the visitor only sees the
f64, and when that value is exactly anf32midpoint the original decimal could have been on either side of it, so there's nothing to correct after the fact. Rounding once needs the token text, and both routes to it have costs:arbitrary_precisionis blocked on #482, and a raw-value capture would tie the helper toserde_jsonwhilejson_helpersstays generic overDeserializer.Plan, unless you'd prefer a different route: document the behaviour on
json_helpers::float(an unquoted number is parsed asf64and then narrowed, so a decimal within half anf64ulp of anf32midpoint can round differently from the same value quoted; quote the value to get single rounding). I'll add the same note at the reflective parser's call site (buffa-descriptor/src/reflect/json.rs:1038), add a test that pins both paths using the example from this issue, and add a changelog fragment.If #482 ends up supporting
arbitrary_precision, the number-as-text branch could parse straight tof32and fix this properly. I'm happy to follow up on that if it's wanted.json: document that an unquoted float can be rounded twice (anthropic…