|
| 1 | +# Design choices: numeric handling (`JsonNumber`, BigDecimal/BigInteger) |
| 2 | + |
| 3 | +This repository is a **backport** of the upstream OpenJDK sandbox `java.util.json` work (mirrored here as `jdk.sandbox.java.util.json`). That matters for “why did X disappear?” questions: |
| 4 | + |
| 5 | +- This repo intentionally avoids *inventing* new public API shapes that diverge from upstream, because doing so makes syncing harder and breaks the point of the backport. |
| 6 | +- When upstream removes or reshapes API, this repo follows. |
| 7 | + |
| 8 | +## What changed (the “story”) |
| 9 | + |
| 10 | +Older revisions of this backport carried some convenience entry points that accepted `java.math.BigDecimal` and `java.math.BigInteger` when building JSON numbers. |
| 11 | + |
| 12 | +During the last upstream sync, those entry points were removed. **That is consistent with the upstream design direction**: keep `JsonNumber`’s public surface area small and make **lossless numeric interoperability** flow through the **lexical JSON number text** (`JsonNumber.toString()`), not through a growing matrix of overloads. |
| 13 | + |
| 14 | +Put differently: the design is “JSON numbers are text first”, not “JSON numbers are a Java numeric tower”. |
| 15 | + |
| 16 | +## Why upstream prefers `String` (and why BigDecimal constructors are a footgun) |
| 17 | + |
| 18 | +### 1) JSON numbers are arbitrary precision *text* |
| 19 | + |
| 20 | +RFC 8259 defines the *syntax* of a JSON number; it does **not** define a fixed precision or a single canonical format. The API aligns with that by treating the number as a string that: |
| 21 | + |
| 22 | +- can be preserved exactly when parsed from a document |
| 23 | +- can be created from a string when you need exact control |
| 24 | + |
| 25 | +### 2) `BigDecimal`/`BigInteger` introduce formatting policy into the API |
| 26 | + |
| 27 | +If `JsonNumber` has `of(BigDecimal)` / `of(BigInteger)`: |
| 28 | + |
| 29 | +- which textual form should be used (`toString()` vs `toPlainString()`)? |
| 30 | +- should `-0` be preserved, normalized, or rejected? |
| 31 | +- should `1e2` round-trip as `1e2` or normalize to `100`? |
| 32 | + |
| 33 | +Any choice becomes a **semantic commitment**: it changes `toString()`, equality and hash behavior, and round-trip characteristics. |
| 34 | + |
| 35 | +Upstream avoids baking those policy decisions into the core JSON API by: |
| 36 | + |
| 37 | +- providing `JsonNumber.of(String)` as the “I know what text I want” factory |
| 38 | +- documenting that you can always interoperate with arbitrary precision Java numerics by converting *from* `toString()` |
| 39 | + |
| 40 | +This intent is explicitly documented in `JsonNumber`’s own `@apiNote`. |
| 41 | + |
| 42 | +### 3) Minimal factories avoid overload explosion |
| 43 | + |
| 44 | +JSON object/array construction in this API already leans toward: |
| 45 | + |
| 46 | +- immutable values |
| 47 | +- static factories (`...of(...)`) |
| 48 | +- pattern matching / sealed types when consuming values |
| 49 | + |
| 50 | +That style is a natural fit for “a few sharp entry points” rather than the legacy OO pattern of ever-expanding constructor overloads for every “convenient” numeric type. |
| 51 | + |
| 52 | +## Recommended recipes (lossless + explicit) |
| 53 | + |
| 54 | +### Parse → BigDecimal (lossless) |
| 55 | + |
| 56 | +```java |
| 57 | +var n = (JsonNumber) Json.parse("3.141592653589793238462643383279"); |
| 58 | +var bd = new BigDecimal(n.toString()); // exact |
| 59 | +``` |
| 60 | + |
| 61 | +### Parse → BigInteger (lossless, when integral) |
| 62 | + |
| 63 | +```java |
| 64 | +var n = (JsonNumber) Json.parse("1.23e2"); |
| 65 | +var bi = new BigDecimal(n.toString()).toBigIntegerExact(); // 123 |
| 66 | +``` |
| 67 | + |
| 68 | +### BigDecimal → JsonNumber (pick your textual policy) |
| 69 | + |
| 70 | +If you want to preserve the *mathematical* value without scientific notation: |
| 71 | + |
| 72 | +```java |
| 73 | +var bd = new BigDecimal("1000"); |
| 74 | +var n = JsonNumber.of(bd.toPlainString()); // "1000" |
| 75 | +``` |
| 76 | + |
| 77 | +If you’re fine with scientific notation when `BigDecimal` chooses it: |
| 78 | + |
| 79 | +```java |
| 80 | +var bd = new BigDecimal("1E+3"); |
| 81 | +var n = JsonNumber.of(bd.toString()); // "1E+3" (still valid JSON number text) |
| 82 | +``` |
| 83 | + |
| 84 | +### JSON lexical preservation is not numeric normalization |
| 85 | + |
| 86 | +Two JSON numbers can represent the same numeric value but still be different JSON texts: |
| 87 | + |
| 88 | +```java |
| 89 | +var a = (JsonNumber) Json.parse("1e2"); |
| 90 | +var b = (JsonNumber) Json.parse("100"); |
| 91 | +assert !a.toString().equals(b.toString()); // lexical difference preserved |
| 92 | +``` |
| 93 | + |
| 94 | +If your application needs *numeric* equality or canonicalization, perform it explicitly with `BigDecimal` (or your own policy), rather than relying on the JSON value object to do it implicitly. |
| 95 | + |
| 96 | +## Runnable examples |
| 97 | + |
| 98 | +This document’s examples are mirrored in code: |
| 99 | + |
| 100 | +- `json-java21/src/test/java/jdk/sandbox/java/util/json/examples/DesignChoicesExamples.java` |
| 101 | +- `json-java21/src/test/java/jdk/sandbox/java/util/json/DesignChoicesNumberExamplesTest.java` |
| 102 | + |
0 commit comments