Skip to content

Keep a float32 a float - #18

Merged
komamitsu merged 2 commits into
mainfrom
preserve-float32
Sep 27, 2026
Merged

komamitsu merged 2 commits into
mainfrom
preserve-float32

Conversation

@komamitsu

@komamitsu komamitsu commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

A behaviour change: a float32 on the wire now reads back as a float.

Why

The parser read float32 and float64 alike into a double and never told Jackson which it was (no getNumberTypeFP(), and getNumberType() always DOUBLE). So untyped binding (Object, Map<String, Object>, JsonNode) turned a Float into a Double, and its text became that of the widened double:

mapper.readValue(mapper.writeValueAsBytes(Map.of("f", 0.1f)), new TypeReference<Map<String, Object>>() {});
// before: {f=0.10000000149011612}  (a Double)
// after:  {f=0.1}                  (a Float)

Jackson's CBOR parser reports float32 as FLOAT32 (CBORParser.getNumberTypeFP()), and as documented, MessagePack libraries in languages with a 32-bit float type (Go, Rust, C#) keep it as one. msgpack-java core does not (unpackValue() widens every float to a double), but compatibility with that stack is not a goal here.

The change

  • A float32 is reported as NumberType.FLOAT and NumberTypeFP.FLOAT32; a float64 as DOUBLE and DOUBLE64.
  • getNumberValue() returns a Float for a float32.
  • Its text, a key named by it, and getDecimalValue() follow the float's own shortest text (0.1), not the widened double's.
  • Both widths stay in the one double field, which holds every float exactly, so the numeric conversions are shared.

DESIGN.md describes it in the parser section.

Tests

Four new tests in MessagePackParserTest, each failing on main: the accessors for a float32 and a float64, untyped reads (Float, Double, Float.NaN), a Float round trip through Map<String, Object>, and a float32 key's name. Two existing tests pinned the old behaviour (a float32 read untyped as a Double) and now expect a Float. ./gradlew clean build, 227 tests.

Performance

MsgpackReadBenchmark, 5 forks x 10 iterations x 2 s, both orders:

Run Order readPojoMsgpack main -> this PR readPojoJson (control) main -> this PR
1 this PR first 684,869 -> 681,426 (-0.5%) 685,905 -> 699,627
2 main first 691,698 -> 690,822 (-0.1%) 701,942 -> 686,074

The MessagePack difference is within error both times. The JSON control, whose code is identical, moved about 2% towards whichever jar ran first, so the MessagePack/JSON ratio flipped with the order. No measurable effect. The fixture has no floats, so this checks that the extra case in every accessor switch does not slow the rest of the parser.

The parser read float32 and float64 alike into a double and never reported which, so untyped binding turned a Float into a Double and its text into that of the widened double (0.10000000149011612 for 0.1f). A float32 is now reported as NumberType.FLOAT and NumberTypeFP.FLOAT32, as Jackson's CBOR parser does, and its text is the float's own; a float64 reports DOUBLE64. Both stay in one double field, which holds every float exactly.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Add focused float32 getDecimalValue() regression coverage.

Review effort: Lite
Findings: 1 Low severity

Open (1)
What changed in this PR

Preserves MessagePack float32 semantics during Jackson parsing while retaining float64 behavior.

Changes:

  • Adds width-aware floating-point types and values.
  • Preserves float32 text, decimal conversion, and map keys.
  • Updates tests and design documentation.
File Summary
src/​test/​java/​org/​komamitsu/​jackson/​dataformat/​msgpack/​MessagePackParserTest.java Adds and updates floating-point behavior tests.
src/​main/​java/​org/​komamitsu/​jackson/​dataformat/​msgpack/​MessagePackParser.java Implements float32-aware parsing and accessors.
docs/​DESIGN.md Documents floating-point width preservation.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +651 to +656
case FLOAT:
case DOUBLE:
if (!Double.isFinite(doubleValue)) {
return _reportError("Cannot convert non-finite double (" + doubleValue + ") to BigDecimal");
}
return BigDecimal.valueOf(doubleValue);
return new BigDecimal(floatingText());

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Added in 0fe509a: aFloat32IsReportedAsAFloat now asserts getDecimalValue() is new BigDecimal("0.1") for the float32 0.1f (and for the float64 0.1). With the old widened conversion (BigDecimal.valueOf(doubleValue)) put back temporarily, it fails with 0.10000000149011612.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟢 Approval recommended

All reviewed changes are covered by tests and no unresolved blocking issues were identified.

Review effort: Lite
Findings: 1 Low severity

Open (1)

@komamitsu
komamitsu merged commit b819c80 into main Sep 27, 2026
5 checks passed
@komamitsu
komamitsu deleted the preserve-float32 branch September 27, 2026 07:15
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