-
Notifications
You must be signed in to change notification settings - Fork 0
Json numeric type design #120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
7186065
2e222bd
1196242
3f62ed2
0903da3
07c0545
46da612
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| # Design choices: numeric handling (`JsonNumber`, BigDecimal/BigInteger) | ||
|
|
||
| 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: | ||
|
|
||
| - 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. | ||
| - When upstream removes or reshapes API, this repo follows. | ||
|
|
||
| ## What changed (the “story”) | ||
|
|
||
| Older revisions of this backport carried some convenience entry points that accepted `java.math.BigDecimal` and `java.math.BigInteger` when building JSON numbers. | ||
|
|
||
| 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. | ||
|
|
||
| Put differently: the design is “JSON numbers are text first”, not “JSON numbers are a Java numeric tower”. | ||
|
|
||
| ## Why upstream prefers `String` (and why BigDecimal constructors are a footgun) | ||
|
|
||
| ### 1) JSON numbers are arbitrary precision *text* | ||
|
|
||
| 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: | ||
|
|
||
| - can be preserved exactly when parsed from a document | ||
| - can be created from a string when you need exact control | ||
|
|
||
| ### 2) `BigDecimal`/`BigInteger` introduce formatting policy into the API | ||
|
|
||
| If `JsonNumber` has `of(BigDecimal)` / `of(BigInteger)`: | ||
|
|
||
| - which textual form should be used (`toString()` vs `toPlainString()`)? | ||
| - should `-0` be preserved, normalized, or rejected? | ||
| - should `1e2` round-trip as `1e2` or normalize to `100`? | ||
|
|
||
| Any choice becomes a **semantic commitment**: it changes `toString()`, equality and hash behavior, and round-trip characteristics. | ||
|
|
||
| Upstream avoids baking those policy decisions into the core JSON API by: | ||
|
|
||
| - providing `JsonNumber.of(String)` as the “I know what text I want” factory | ||
| - documenting that you can always interoperate with arbitrary precision Java numerics by converting *from* `toString()` | ||
|
|
||
| This intent is explicitly documented in `JsonNumber`’s own `@apiNote`. | ||
|
|
||
| ### 3) Minimal factories avoid overload explosion | ||
|
|
||
| JSON object/array construction in this API already leans toward: | ||
|
|
||
| - immutable values | ||
| - static factories (`...of(...)`) | ||
| - pattern matching / sealed types when consuming values | ||
|
|
||
| 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. | ||
|
|
||
| ## Recommended recipes (lossless + explicit) | ||
|
|
||
| ### Parse → BigDecimal (lossless) | ||
|
|
||
| ```java | ||
| var n = (JsonNumber) Json.parse("3.141592653589793238462643383279"); | ||
| var bd = new BigDecimal(n.toString()); // exact | ||
| ``` | ||
|
|
||
| ### Parse → BigInteger (lossless, when integral) | ||
|
|
||
| ```java | ||
| var n = (JsonNumber) Json.parse("1.23e2"); | ||
| var bi = new BigDecimal(n.toString()).toBigIntegerExact(); // 123 | ||
| ``` | ||
|
|
||
| ### BigDecimal → JsonNumber (pick your textual policy) | ||
|
|
||
| If you want to preserve the *mathematical* value without scientific notation: | ||
|
|
||
| ```java | ||
| var bd = new BigDecimal("1000"); | ||
| var n = JsonNumber.of(bd.toPlainString()); // "1000" | ||
| ``` | ||
|
|
||
| If you’re fine with scientific notation when `BigDecimal` chooses it: | ||
|
|
||
| ```java | ||
| var bd = new BigDecimal("1E+3"); | ||
| var n = JsonNumber.of(bd.toString()); // "1E+3" (still valid JSON number text) | ||
| ``` | ||
|
|
||
| ### JSON lexical preservation is not numeric normalization | ||
|
|
||
| Two JSON numbers can represent the same numeric value but still be different JSON texts: | ||
|
|
||
| ```java | ||
| var a = (JsonNumber) Json.parse("1e2"); | ||
| var b = (JsonNumber) Json.parse("100"); | ||
| assert !a.toString().equals(b.toString()); // lexical difference preserved | ||
| ``` | ||
|
|
||
| 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. | ||
|
|
||
| ## Runnable examples | ||
|
|
||
| This document’s examples are mirrored in code: | ||
|
|
||
| - `json-java21/src/test/java/jdk/sandbox/java/util/json/examples/DesignChoicesExamples.java` | ||
| - `json-java21/src/test/java/jdk/sandbox/java/util/json/DesignChoicesNumberExamplesTest.java` | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| package jdk.sandbox.java.util.json; | ||
|
|
||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| import java.math.BigDecimal; | ||
| import java.math.BigInteger; | ||
| import java.util.logging.Logger; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
|
|
||
| public class DesignChoicesNumberExamplesTest { | ||
| private static final Logger LOGGER = Logger.getLogger(DesignChoicesNumberExamplesTest.class.getName()); | ||
|
|
||
| @Test | ||
| void parseHighPrecisionNumberIsLosslessViaToString() { | ||
| LOGGER.info("Executing parseHighPrecisionNumberIsLosslessViaToString"); | ||
|
|
||
| var text = "3.141592653589793238462643383279"; | ||
| var n = (JsonNumber) Json.parse(text); | ||
|
|
||
| assertThat(n.toString()).isEqualTo(text); | ||
| assertThat(new BigDecimal(n.toString())).isEqualByComparingTo(new BigDecimal(text)); | ||
| } | ||
|
|
||
| @Test | ||
| void convertingToDoubleIsPotentiallyLossy() { | ||
| LOGGER.info("Executing convertingToDoubleIsPotentiallyLossy"); | ||
|
|
||
| var text = "3.141592653589793238462643383279"; | ||
| var n = (JsonNumber) Json.parse(text); | ||
|
|
||
| var lossless = new BigDecimal(n.toString()); | ||
| var lossy = new BigDecimal(Double.toString(n.toDouble())); | ||
|
|
||
| assertThat(lossy).isNotEqualByComparingTo(lossless); | ||
| } | ||
|
|
||
| @Test | ||
| void parseExponentFormToBigIntegerExactWorks() { | ||
| LOGGER.info("Executing parseExponentFormToBigIntegerExactWorks"); | ||
|
|
||
| var n = (JsonNumber) Json.parse("1.23e2"); | ||
| BigInteger bi = new BigDecimal(n.toString()).toBigIntegerExact(); | ||
|
|
||
| assertThat(bi).isEqualTo(BigInteger.valueOf(123)); | ||
| } | ||
|
|
||
| @Test | ||
| void bigDecimalToJsonNumberRequiresChoosingATextPolicy() { | ||
| LOGGER.info("Executing bigDecimalToJsonNumberRequiresChoosingATextPolicy"); | ||
|
|
||
| var thousand = new BigDecimal("1000"); | ||
|
|
||
| var plain = JsonNumber.of(thousand.toPlainString()); | ||
| assertThat(plain.toString()).isEqualTo("1000"); | ||
|
|
||
| var scientific = JsonNumber.of(new BigDecimal("1E+3").toString()); | ||
| assertThat(scientific.toString()).isEqualTo("1E+3"); | ||
| } | ||
|
|
||
| @Test | ||
| void lexicalPreservationDiffersFromNumericNormalization() { | ||
| LOGGER.info("Executing lexicalPreservationDiffersFromNumericNormalization"); | ||
|
|
||
| var a = (JsonNumber) Json.parse("1e2"); | ||
| var b = (JsonNumber) Json.parse("100"); | ||
|
|
||
| // lexical forms differ | ||
| assertThat(a.toString()).isEqualTo("1e2"); | ||
| assertThat(b.toString()).isEqualTo("100"); | ||
|
|
||
| // but numeric values compare equal when canonicalized explicitly | ||
| assertThat(new BigDecimal(a.toString())).isEqualByComparingTo(new BigDecimal(b.toString())); | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| package jdk.sandbox.java.util.json.examples; | ||
|
|
||
| import jdk.sandbox.java.util.json.Json; | ||
| import jdk.sandbox.java.util.json.JsonNumber; | ||
|
|
||
| import java.math.BigDecimal; | ||
| import java.math.BigInteger; | ||
|
|
||
| /** | ||
| * Standalone examples demonstrating numeric design choices. | ||
| * | ||
| * <p> | ||
| * This file mirrors the examples in {@code DESIGN_CHOICES.md}. | ||
| */ | ||
| public final class DesignChoicesExamples { | ||
|
|
||
| public static void main(String[] args) { | ||
| System.out.println("=== Numeric design choices examples ===\n"); | ||
|
|
||
| parseToBigDecimalLossless(); | ||
| parseToBigIntegerExact(); | ||
| bigDecimalToJsonNumberChooseTextPolicy(); | ||
| lexicalPreservationNotNormalization(); | ||
|
|
||
| System.out.println("\n=== All examples completed successfully! ==="); | ||
| } | ||
|
|
||
| public static BigDecimal parseToBigDecimalLossless() { | ||
| var n = (JsonNumber) Json.parse("3.141592653589793238462643383279"); | ||
| var bd = new BigDecimal(n.toString()); | ||
| System.out.println("lossless BigDecimal: " + bd.toPlainString()); | ||
| return bd; | ||
| } | ||
|
|
||
| public static BigInteger parseToBigIntegerExact() { | ||
| var n = (JsonNumber) Json.parse("1.23e2"); | ||
| var bi = new BigDecimal(n.toString()).toBigIntegerExact(); | ||
| System.out.println("exact BigInteger: " + bi); | ||
| return bi; | ||
| } | ||
|
|
||
| public static JsonNumber bigDecimalToJsonNumberChooseTextPolicy() { | ||
| var bd = new BigDecimal("1000"); | ||
|
|
||
| var plain = JsonNumber.of(bd.toPlainString()); | ||
| System.out.println("BigDecimal.toPlainString() -> JsonNumber: " + plain); | ||
|
|
||
| var scientific = JsonNumber.of(new BigDecimal("1E+3").toString()); | ||
| System.out.println("BigDecimal.toString() -> JsonNumber: " + scientific); | ||
|
|
||
| return plain; | ||
| } | ||
|
Comment on lines
+53
to
+66
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This example method can be made clearer and more consistent. The first part defines a public static JsonNumber bigDecimalToJsonNumberChooseTextPolicy() {
// Example with toPlainString() to avoid scientific notation.
var bdPlain = new BigDecimal("1000");
var plain = JsonNumber.of(bdPlain.toPlainString());
System.out.println("BigDecimal.toPlainString() -> JsonNumber: " + plain);
// Example with toString(), which may use scientific notation.
var bdScientific = new BigDecimal("1E+3");
var scientific = JsonNumber.of(bdScientific.toString());
System.out.println("BigDecimal.toString() -> JsonNumber: " + scientific);
return plain;
} |
||
|
|
||
| public static boolean lexicalPreservationNotNormalization() { | ||
| var a = (JsonNumber) Json.parse("1e2"); | ||
| var b = (JsonNumber) Json.parse("100"); | ||
|
|
||
| System.out.println("a.toString(): " + a); | ||
| System.out.println("b.toString(): " + b); | ||
| System.out.println("same numeric value? " + new BigDecimal(a.toString()).compareTo(new BigDecimal(b.toString()))); | ||
|
|
||
| return a.toString().equals(b.toString()); | ||
| } | ||
|
|
||
| private DesignChoicesExamples() {} | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic in this test can be made clearer and more consistent. The first part of the test defines a
BigDecimalvariablethousand, but the second part creates aBigDecimalinline. For better readability and to more clearly demonstrate the policies, consider defining separate, descriptively named variables for both cases. This makes the intent of each part of the test more explicit.