Skip to content

Serialize negative zero floats instead of dropping them#226

Merged
AdrienVannson merged 2 commits into
betterproto:mainfrom
gaoflow:fix-serialize-negative-zero
Jun 14, 2026
Merged

Serialize negative zero floats instead of dropping them#226
AdrienVannson merged 2 commits into
betterproto:mainfrom
gaoflow:fix-serialize-negative-zero

Conversation

@gaoflow

@gaoflow gaoflow commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Problem

A float or double field set to -0.0 is silently dropped during serialization. -0.0 compares equal to the 0.0 field default and is falsy, so the "is this the default value?" checks treat it as unset and skip it.

The reference protobuf implementation keeps -0.0: its sign bit is set, which distinguishes it from the default +0.0.

# google.protobuf (reference), double field v = -0.0
wire: 090000000000000080      # 9 bytes, field present
json: {"v": -0.0}

# betterproto2 (before this PR), same field v = -0.0
wire: (empty)                 # field dropped, sign lost
json: {}

So a betterproto2 producer loses the sign for any consumer (including another betterproto2 process), while +0.0 is correctly omitted. Decoding already worked: parsing the reference -0.0 wire yields -0.0, so only the encode side was affected.

Fix

Add a small _is_negative_zero helper and treat -0.0 as a non-default value in the three serialization paths that relied on value == default / not bool(value):

  • binary __bytes__
  • to_dict / to_json PROTO_JSON output
  • to_dict PYTHON output

The real +0.0 default keeps being omitted everywhere.

Tests

tests/test_negative_zero.py checks that -0.0 survives a binary round trip and a dict/json round trip for both float and double, and that +0.0 is still omitted. The fixed binary output now matches the google reference byte for byte (090000000000000080).

Notes

I found this with a differential test of betterproto2 against the google protobuf package. AI assistance was used under my direction; I verified the behavior against the reference implementation and reviewed the change.

A float or double field set to -0.0 was dropped from the binary wire
format and from to_dict/to_json output: -0.0 compares equal to the 0.0
default and is falsy, so the default checks treated it as unset.

The reference protobuf implementation serializes -0.0 (its sign bit is
set, distinguishing it from the +0.0 default), so betterproto2 producers
were silently losing the sign for any consumer. Add an _is_negative_zero
helper and keep the value in all three serialization paths, while still
omitting the real +0.0 default.

@AdrienVannson AdrienVannson left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

I see one line to change to match generated code (I don't think there is any reason to have something different here), other than that it looks great



def _make_cls(proto_type: str):
@dataclass(eq=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@dataclass(eq=True)
@dataclass(eq=False, repr=False)

To match with standard generated messages

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 59614e5 — switched to @dataclass(eq=False, repr=False) to match the generated-message options. Tests still green.

Use @DataClass(eq=False, repr=False) on the test Message, matching the
options betterproto2 emits for generated messages, per review.
@AdrienVannson AdrienVannson merged commit 28886bb into betterproto:main Jun 14, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants