Skip to content

Fix primitive default-value round-trip and preserve -0.0 in the codec#112

Closed
ghostdogpr wants to merge 1 commit into
mainfrom
fix/codec-defaults-negzero
Closed

Fix primitive default-value round-trip and preserve -0.0 in the codec#112
ghostdogpr wants to merge 1 commit into
mainfrom
fix/codec-defaults-negzero

Conversation

@ghostdogpr

Copy link
Copy Markdown
Owner

Two encode/decode consistency bugs in the primitive codec, both surfaced by a fresh bug-hunt pass.

1. Non-zero primitive defaults corrupt the round-trip

The deriver populates a SimpleField's decode default from the schema default, which zio-blocks derives from the Scala case-class default parameter. So for case class Foo(x: Int = 5), the field's decode default is 5. But encode omits a primitive field whenever it equals the type's zero (value == 0, value.isEmpty, …) — it never consults the declared default. Result:

encode(Foo(0))  // x == 0 → omitted
decode(...)     // x absent → filled with 5
// => Foo(5), silently corrupting 0 into 5

Encode categorically ignores the declared default for every field category, so decode must too. For primitive fields the decode default is now the proto3 zero (getDefaultValue), matching encode. Message / enum / option / collection / transform defaults are untouched.

2. -0.0 is dropped

Presence for Double/Float used value == 0d / value == 0f. Since -0.0 == 0.0 under IEEE equality, -0.0 was treated as the omittable default and round-tripped to +0.0 — also diverging from protobuf-java, whose generated code gates on doubleToRawLongBits(value) != 0. Presence now checks raw bits, with a value == 0 short-circuit so ordinary non-zero values never call doubleToRawLongBits (keeping the JS hot path unaffected).

Tests

Two regression tests (red before the fix, green after): a primitive field with a non-zero default round-tripping its zero value, and -0.0 double/float fields preserving their sign bit (compared by raw bits). Full core suite: 139 passed.

Two encode/decode consistency bugs in the primitive codec:

- Decode restored an absent field from the schema-captured default (which
  the deriver populates from the Scala case-class default parameter), while
  encode omits a field whenever it equals the type's zero. A primitive field
  with a non-zero declared default (e.g. Int = 5) therefore turned an encoded
  0 into 5 on decode. For primitive fields, use the proto3 zero as the decode
  default so encode and decode agree; message/enum/option/collection defaults
  are unchanged.

- Field presence for Double/Float used value == 0, and -0.0 == 0.0 in IEEE,
  so -0.0 was dropped and round-tripped to +0.0 (also diverging from
  protobuf-java, which gates on raw bits). Presence now checks raw bits, with
  a value == 0 short-circuit so non-zero values never call doubleToRawLongBits.
@ghostdogpr ghostdogpr closed this Jul 9, 2026
@ghostdogpr ghostdogpr deleted the fix/codec-defaults-negzero branch July 9, 2026 03:39
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.

1 participant