Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
for k in totals: totals[k]+=int(r.get(k,'0'))
except Exception:
pass
exp_tests=511
exp_tests=519
exp_skipped=0
if totals['tests']!=exp_tests or totals['skipped']!=exp_skipped:
print(f"Unexpected test totals: {totals} != expected tests={exp_tests}, skipped={exp_skipped}")
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ java -cp ./json-java21/target/java.util.json-*.jar:./json-java21/target/test-cla

*Replace `*` with the actual version number from the JAR filename.*

## Numeric handling (JsonNumber and native Java numbers)

Prior versions of this backport included convenience entry points for building JSON numbers from `BigDecimal` / `BigInteger`. Upstream has moved away from that shape: `JsonNumber` preserves the JSON number text and you convert explicitly to native Java numeric types depending on your policy (lossless via `toString()` + `BigDecimal`, or range/precision-limited via `toLong()` / `toDouble()`).

For runnable examples (including counter-examples where conversions throw or lose precision, plus a pattern-matching “map to native types” helper), see:

- `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`

## API Overview

The API provides immutable JSON value types:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package jdk.sandbox.java.util.json;

import org.junit.jupiter.api.Test;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Map;
import java.util.logging.Logger;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

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 convertingNonIntegralNumberToLongThrows() {
LOGGER.info("Executing convertingNonIntegralNumberToLongThrows");

var n = (JsonNumber) Json.parse("5.5");
assertThatThrownBy(n::toLong)
.isInstanceOf(JsonAssertionException.class);
}

@Test
void convertingOutOfRangeNumberToDoubleThrows() {
LOGGER.info("Executing convertingOutOfRangeNumberToDoubleThrows");

var n = (JsonNumber) Json.parse("1e309");
assertThatThrownBy(n::toDouble)
.isInstanceOf(JsonAssertionException.class);
}

@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");

// Using toPlainString() for a plain number representation
var bdPlain = new BigDecimal("1000");

var plain = JsonNumber.of(bdPlain.toPlainString());
assertThat(plain.toString()).isEqualTo("1000");

// Using toString(), which may produce scientific notation
var bdScientific = new BigDecimal("1E+3");
var scientific = JsonNumber.of(bdScientific.toString());
assertThat(scientific.toString()).isEqualTo("1E+3");
Comment on lines 70 to 81

Choose a reason for hiding this comment

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

medium

The logic in this test can be made clearer and more consistent. The first part of the test defines a BigDecimal variable thousand, but the second part creates a BigDecimal inline. 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.

        LOGGER.info("Executing bigDecimalToJsonNumberRequiresChoosingATextPolicy");

        // Using toPlainString() for a plain number representation
        var bdPlain = new BigDecimal("1000");
        var plain = JsonNumber.of(bdPlain.toPlainString());
        assertThat(plain.toString()).isEqualTo("1000");

        // Using toString() which may produce scientific notation
        var bdScientific = new BigDecimal("1E+3");
        var scientific = JsonNumber.of(bdScientific.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()));
}

@Test
void mappingToNativeTypesUsesPatternMatchingAndExplicitNumberPolicy() {
LOGGER.info("Executing mappingToNativeTypesUsesPatternMatchingAndExplicitNumberPolicy");

JsonValue json = Json.parse("""
{
"smallInt": 42,
"decimal": 5.5
}
""");

Object nativeValue = jdk.sandbox.java.util.json.examples.DesignChoicesExamples.toNative(json);
assertThat(nativeValue).isInstanceOf(Map.class);

@SuppressWarnings("unchecked")
var map = (Map<String, Object>) nativeValue;

assertThat(map.get("smallInt")).isEqualTo(42L);
assertThat(map.get("decimal")).isInstanceOf(BigDecimal.class);
assertThat((BigDecimal) map.get("decimal")).isEqualByComparingTo(new BigDecimal("5.5"));
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package jdk.sandbox.java.util.json.examples;

import jdk.sandbox.java.util.json.Json;
import jdk.sandbox.java.util.json.JsonAssertionException;
import jdk.sandbox.java.util.json.JsonArray;
import jdk.sandbox.java.util.json.JsonBoolean;
import jdk.sandbox.java.util.json.JsonNumber;
import jdk.sandbox.java.util.json.JsonNull;
import jdk.sandbox.java.util.json.JsonObject;
import jdk.sandbox.java.util.json.JsonString;
import jdk.sandbox.java.util.json.JsonValue;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Map;
import java.util.stream.Collectors;

/**
* Standalone examples demonstrating numeric design choices.
*
* <p>
* See the repository README for context and pointers.
*/
public final class DesignChoicesExamples {

public static void main(String[] args) {
System.out.println("=== Numeric design choices examples ===\n");

parseToBigDecimalLossless();
parseToBigIntegerExact();
bigDecimalToJsonNumberChooseTextPolicy();
lexicalPreservationNotNormalization();
counterExamples();
mappingToNativeTypesWithPatternMatching();

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() {
// 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;
}
Comment on lines 53 to 66

Choose a reason for hiding this comment

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

medium

This example method can be made clearer and more consistent. The first part defines a BigDecimal variable bd, but the second part creates a BigDecimal inline. To improve readability and better illustrate the different text policies, it would be beneficial to use separate, descriptively named variables for both the "plain" and "scientific" cases.

    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());
}

public static void counterExamples() {
System.out.println("Counter-examples");
System.out.println("----------------");

// 1) Converting a non-integral JSON number to long throws.
try {
var nonIntegral = (JsonNumber) Json.parse("5.5");
System.out.println("nonIntegral.toString(): " + nonIntegral);
System.out.println("nonIntegral.toLong(): " + nonIntegral.toLong());
} catch (JsonAssertionException e) {
System.out.println("Expected toLong() failure: " + e.getMessage());
}

// 2) Converting an out-of-range JSON number to double throws.
try {
var tooBig = (JsonNumber) Json.parse("1e309");
System.out.println("tooBig.toString(): " + tooBig);
System.out.println("tooBig.toDouble(): " + tooBig.toDouble());
} catch (JsonAssertionException e) {
System.out.println("Expected toDouble() failure: " + e.getMessage());
}

// 3) Converting to double can be lossy even when it does not throw.
var highPrecision = (JsonNumber) Json.parse("3.141592653589793238462643383279");
var lossless = new BigDecimal(highPrecision.toString());
var lossy = new BigDecimal(Double.toString(highPrecision.toDouble()));
System.out.println("lossless (BigDecimal): " + lossless.toPlainString());
System.out.println("lossy (double->BD): " + lossy.toPlainString());
System.out.println("lossless equals lossy? " + (lossless.compareTo(lossy) == 0));
System.out.println();
}

public static Object toNative(JsonValue v) {
return switch (v) {
case JsonNull ignored -> null;
case JsonBoolean b -> b.bool();
case JsonString s -> s.string();
case JsonNumber n -> {
try {
yield n.toLong();
} catch (JsonAssertionException ignored) {
yield new BigDecimal(n.toString());
}
}
case JsonArray a -> a.elements().stream().map(DesignChoicesExamples::toNative).toList();
case JsonObject o -> o.members().entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> toNative(e.getValue())));
Comment on lines +123 to +125

Choose a reason for hiding this comment

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

P2 Badge Handle JsonNull values when collecting object members

toNative returns null for JsonNull, but the Collectors.toMap(...) call rejects null values and will throw a NullPointerException when a JSON object contains a null member value. This makes the documented “Map/List/primitives” recipe fail on valid JSON like {"a": null}. Consider collecting into a map that tolerates null values (e.g., manual HashMap population) or mapping JsonNull to a sentinel instead of null.

Useful? React with 👍 / 👎.

};
}

public static Object mappingToNativeTypesWithPatternMatching() {
System.out.println("Mapping to native types (pattern matching)");
System.out.println("------------------------------------------");

JsonValue json = Json.parse("""
{
"smallInt": 42,
"decimal": 5.5,
"huge": 3.141592653589793238462643383279,
"flag": true,
"name": "Ada",
"items": [1, 2, 3]
}
""");

Object nativeValue = toNative(json);
System.out.println("native class: " + nativeValue.getClass().getName());
System.out.println("native value: " + nativeValue);
System.out.println();
return nativeValue;
}

private DesignChoicesExamples() {}
}