diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mappings.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mappings.java index 77f172cc..bdc9e0c4 100644 --- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mappings.java +++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mappings.java @@ -705,6 +705,12 @@ private MapperConverter findConverter(final boolean copyDate, final AccessMode.D if (Date.class.isAssignableFrom(type) && copyDate) { converter = new DateWithCopyConverter(Adapter.class.cast(adapters.get(new AdapterKey(Date.class, String.class)))); + } else if (type == BigDecimal.class || type == BigInteger.class) { + // BigDecimal/BigInteger are "primitives" in the mapper so they bypass + // config.findAdapter() in writeValue(). Use a direct get() to trigger + // lazy loading and respect useBigDecimalStringAdapter/useBigIntegerStringAdapter. + // this makes it symetric with READ + converter = adapters.get(new AdapterKey(type, String.class)); } else { for (final Map.Entry> adapterEntry : adapters.entrySet()) { if (adapterEntry.getKey().getFrom() == adapterEntry.getKey().getTo()) { // String -> String diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/converter/BigDecimalConverter.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/converter/BigDecimalConverter.java index b5864e09..891f60b1 100644 --- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/converter/BigDecimalConverter.java +++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/converter/BigDecimalConverter.java @@ -25,7 +25,9 @@ public class BigDecimalConverter implements Converter { @Override public String toString(final BigDecimal instance) { - return instance.toString(); + // when using the converter, user expects the decimal notation + // otherwise, JsonNumber will give the E (scientific) notation + return instance.toPlainString(); } @Override diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/map/LazyConverterMap.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/map/LazyConverterMap.java index cd1b1699..b0cee385 100644 --- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/map/LazyConverterMap.java +++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/map/LazyConverterMap.java @@ -85,8 +85,11 @@ public Object from(final Object a) { private boolean useShortISO8601Format = true; private DateTimeFormatter dateTimeFormatter; - private boolean useBigIntegerStringAdapter = true; - private boolean useBigDecimalStringAdapter = true; + // I-JSON (RFC 7493 Section 2.2): BigX exceed IEEE 754 double, string is safer by default. + // Set -Djohnzon.use-big-number-stringadapter=false for strict JSON-B 3.0 / TCK compliance. + private static final boolean IJSON_BIG_NUMBER_DEFAULT = !Boolean.getBoolean("johnzon.use-big-number-stringadapter.disabled"); + private boolean useBigIntegerStringAdapter = IJSON_BIG_NUMBER_DEFAULT; + private boolean useBigDecimalStringAdapter = IJSON_BIG_NUMBER_DEFAULT; public void setUseShortISO8601Format(final boolean useShortISO8601Format) { this.useShortISO8601Format = useShortISO8601Format; @@ -163,10 +166,10 @@ public Set adapterKeys() { if (from == String.class) { return add(key, new ConverterAdapter<>(new StringConverter(), String.class)); } - if (from == BigDecimal.class && useBigIntegerStringAdapter) { + if (from == BigDecimal.class && useBigDecimalStringAdapter) { return add(key, new ConverterAdapter<>(new BigDecimalConverter(), BigDecimal.class)); } - if (from == BigInteger.class && useBigDecimalStringAdapter) { + if (from == BigInteger.class && useBigIntegerStringAdapter) { return add(key, new ConverterAdapter<>(new BigIntegerConverter(), BigInteger.class)); } if (from == Locale.class) { diff --git a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/LiteralTest.java b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/LiteralTest.java index 830779ee..cce805d0 100644 --- a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/LiteralTest.java +++ b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/LiteralTest.java @@ -55,10 +55,13 @@ public int compare(final String o1, final String o2) { return expectedJson.indexOf(o1) - expectedJson.indexOf(o2); } }; - new MapperBuilder().setAttributeOrder(attributeOrder).build().writeObject(nc, sw); + new MapperBuilder().setAttributeOrder(attributeOrder) + .setUseBigDecimalStringAdapter(false).setUseBigIntegerStringAdapter(false) + .build().writeObject(nc, sw); assertEquals(expectedJson, sw.toString()); - final NumberClass read = new MapperBuilder().setAttributeOrder(attributeOrder).build() - .readObject(new StringReader(sw.toString()), NumberClass.class); + final NumberClass read = new MapperBuilder().setAttributeOrder(attributeOrder) + .setUseBigDecimalStringAdapter(false).setUseBigIntegerStringAdapter(false) + .build().readObject(new StringReader(sw.toString()), NumberClass.class); assertEquals(nc, read); } diff --git a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/MapperEnhancedTest.java b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/MapperEnhancedTest.java index 8073136a..30384ab7 100644 --- a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/MapperEnhancedTest.java +++ b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/MapperEnhancedTest.java @@ -137,7 +137,7 @@ public void writeTestclass() { public int compare(String o1, String o2) { return json.indexOf(o1) - json.indexOf(o2); } - }).build().writeObject(tc2, sw); + }).setUseBigDecimalStringAdapter(false).build().writeObject(tc2, sw); assertEquals(json, sw.toString()); } diff --git a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/NumberSerializationTest.java b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/NumberSerializationTest.java index 17590188..e18fa564 100644 --- a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/NumberSerializationTest.java +++ b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/NumberSerializationTest.java @@ -19,10 +19,16 @@ package org.apache.johnzon.mapper; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.math.BigDecimal; +import java.math.BigInteger; +import org.apache.johnzon.mapper.converter.BigDecimalConverter; +import org.apache.johnzon.mapper.internal.AdapterKey; +import org.apache.johnzon.mapper.map.LazyConverterMap; import org.junit.Test; public class NumberSerializationTest { @@ -46,6 +52,82 @@ public void numberFromJson() { mapper.close(); } + /** + * Bug: BigDecimalConverter used toString() which produces scientific notation + * (e.g. "7.33915E-7"). Should use toPlainString() to produce "0.000000733915". + */ + @Test + public void bigDecimalConverterUsesPlainNotation() { + final BigDecimalConverter converter = new BigDecimalConverter(); + final BigDecimal smallValue = new BigDecimal("0.000000733915"); + final String result = converter.toString(smallValue); + assertEquals("BigDecimalConverter should use plain notation, not scientific", + "0.000000733915", result); + } + + /** + * Bug fix: useBigDecimalStringAdapter and useBigIntegerStringAdapter flags + * were swapped in LazyConverterMap. Each flag must control its own type. + * Both default to true (string) for I-JSON (RFC 7493) interoperability. + */ + @Test + public void bigDecimalStringAdapterFlagControlsBigDecimal() { + // Default: BigDecimal adapter is ON (useBigDecimalStringAdapter=true) + final LazyConverterMap defaultAdapters = new LazyConverterMap(); + assertNotNull("BigDecimal adapter should be active by default", + defaultAdapters.get(new AdapterKey(BigDecimal.class, String.class))); + // Disabled: BigDecimal adapter is OFF + final LazyConverterMap disabledAdapters = new LazyConverterMap(); + disabledAdapters.setUseBigDecimalStringAdapter(false); + assertNull("BigDecimal adapter should be null when flag is false", + disabledAdapters.get(new AdapterKey(BigDecimal.class, String.class))); + } + + @Test + public void bigIntegerStringAdapterFlagControlsBigInteger() { + // Default: BigInteger adapter is ON (useBigIntegerStringAdapter=true) + final LazyConverterMap adapters = new LazyConverterMap(); + assertNotNull("BigInteger adapter should be active by default", + adapters.get(new AdapterKey(BigInteger.class, String.class))); + // Disabled: BigInteger adapter is OFF + final LazyConverterMap adapters2 = new LazyConverterMap(); + adapters2.setUseBigIntegerStringAdapter(false); + assertNull("BigInteger adapter should be null when flag is false", + adapters2.get(new AdapterKey(BigInteger.class, String.class))); + } + + /** + * With useBigDecimalStringAdapter=true (default), BigDecimal fields + * should be serialized as JSON strings using plain notation. + */ + @Test + public void bigDecimalDefaultSerializesAsString() { + try (final Mapper mapper = new MapperBuilder().build()) { + final BigDecimalHolder holder = new BigDecimalHolder(); + holder.score = new BigDecimal("0.000000733915"); + final String json = mapper.writeObjectAsString(holder); + // Default: BigDecimal as string with plain notation (I-JSON interoperability) + assertEquals("{\"score\":\"0.000000733915\"}", json); + } + } + + /** + * With useBigDecimalStringAdapter=false, BigDecimal fields should be + * serialized as JSON numbers (strict JSON-B 3.0 / TCK compliance). + */ + @Test + public void bigDecimalWithAdapterDisabledSerializesAsNumber() { + try (final Mapper mapper = new MapperBuilder() + .setUseBigDecimalStringAdapter(false) + .build()) { + final BigDecimalHolder holder = new BigDecimalHolder(); + holder.score = new BigDecimal("0.000000733915"); + final String json = mapper.writeObjectAsString(holder); + // Adapter disabled: BigDecimal as JSON number (scientific notation is valid per RFC 8259) + assertEquals("{\"score\":7.33915E-7}", json); + } + } + public static class Holder { public long value; } @@ -53,4 +135,8 @@ public static class Holder { public static class Num { public Number value; } + + public static class BigDecimalHolder { + public BigDecimal score; + } }