Skip to content

json: an unquoted float value is rounded twice, so it can differ from the same value quoted #647

Open

Description

@iainmcgin
use buffa_types::google::protobuf::FloatValue;
let a: FloatValue = serde_json::from_str("1.0000000596046448")?;     // 1.0        (0x3f800000)
let b: FloatValue = serde_json::from_str("\"1.0000000596046448\"")?; // 1.0000001  (0x3f800001)

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.

Activity

  1. farmer-data commented on Oct 12, 2026

    @farmer-data
    Contributor

    I'd like to take this one.

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions