You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@johnzon.apache.org by jl...@apache.org on 2026/03/25 20:29:12 UTC

(johnzon) branch johnzon-1.2.x updated: fix(JOHNZON-426): BigInteger/BigDecimal string adapter flags swapped fix(JOHNZON-427): BigDecimal/BigInteger READ/WRITE not consistent

This is an automated email from the ASF dual-hosted git repository.

jlmonteiro pushed a commit to branch johnzon-1.2.x
in repository https://gitbox.apache.org/repos/asf/johnzon.git


The following commit(s) were added to refs/heads/johnzon-1.2.x by this push:
     new 63f36b8c fix(JOHNZON-426): BigInteger/BigDecimal string adapter flags swapped fix(JOHNZON-427): BigDecimal/BigInteger READ/WRITE not consistent
63f36b8c is described below

commit 63f36b8c67a270d9158a96111095628f82b7f8a5
Author: Jean-Louis Monteiro <jl...@tomitribe.com>
AuthorDate: Wed Mar 25 15:28:45 2026 -0500

    fix(JOHNZON-426): BigInteger/BigDecimal string adapter flags swapped
    fix(JOHNZON-427): BigDecimal/BigInteger READ/WRITE not consistent
    
    Signed-off-by: Jean-Louis Monteiro <jl...@tomitribe.com>
---
 .../org/apache/johnzon/mapper/MapperBuilder.java   | 10 +++
 .../java/org/apache/johnzon/mapper/Mappings.java   |  6 ++
 .../mapper/converter/BigDecimalConverter.java      |  4 +-
 .../johnzon/mapper/map/LazyConverterMap.java       | 17 ++++-
 .../org/apache/johnzon/mapper/LiteralTest.java     |  9 ++-
 .../apache/johnzon/mapper/MapperEnhancedTest.java  |  2 +-
 .../johnzon/mapper/NumberSerializationTest.java    | 86 ++++++++++++++++++++++
 7 files changed, 127 insertions(+), 7 deletions(-)

diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperBuilder.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperBuilder.java
index d58a4409..77dbeb44 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperBuilder.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MapperBuilder.java
@@ -529,6 +529,16 @@ public class MapperBuilder {
         return this;
     }
 
+    public MapperBuilder setUseBigIntegerStringAdapter(final boolean convertBigIntegerToString) {
+        adapters.setUseBigIntegerStringAdapter(convertBigIntegerToString);
+        return this;
+    }
+
+    public MapperBuilder setUseBigDecimalStringAdapter(final boolean convertBigDecimalToString) {
+        adapters.setUseBigDecimalStringAdapter(convertBigDecimalToString);
+        return this;
+    }
+
     public MapperBuilder setUseBigDecimalForObjectNumbers(final boolean value) {
         this.useBigDecimalForObjectNumbers = value;
         return this;
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 7c1776e1..74a2fd6a 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
@@ -681,6 +681,12 @@ public class Mappings {
 
             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<AdapterKey, Adapter<?, ?>> 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 @@ import java.math.BigDecimal;
 public class BigDecimalConverter implements Converter<BigDecimal> {
     @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 943a1f1b..823065b7 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,6 +85,11 @@ public class LazyConverterMap extends ConcurrentHashMap<AdapterKey, Adapter<?, ?
 
     private boolean useShortISO8601Format = true;
     private DateTimeFormatter dateTimeFormatter;
+    // I-JSON (RFC 7493 Section 2.2): BigX exceed IEEE 754 double, string is safer by default.
+    // Set -Djohnzon.use-ijson-big-number-stringadapter.disabled=true for strict JSON-B 3.0 / TCK compliance.
+    private static final boolean IJSON_BIG_NUMBER_DEFAULT = !Boolean.getBoolean("johnzon.use-ijson-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;
@@ -94,6 +99,14 @@ public class LazyConverterMap extends ConcurrentHashMap<AdapterKey, Adapter<?, ?
         this.dateTimeFormatter = dateTimeFormatter;
     }
 
+    public void setUseBigDecimalStringAdapter(final boolean useBigDecimalStringAdapter) {
+        this.useBigDecimalStringAdapter = useBigDecimalStringAdapter;
+    }
+
+    public void setUseBigIntegerStringAdapter(final boolean useBigIntegerStringAdapter) {
+        this.useBigIntegerStringAdapter = useBigIntegerStringAdapter;
+    }
+
     @Override
     public Adapter<?, ?> get(final Object key) {
         final Adapter<?, ?> found = super.get(key);
@@ -153,10 +166,10 @@ public class LazyConverterMap extends ConcurrentHashMap<AdapterKey, Adapter<?, ?
         if (from == String.class) {
             return add(key, new ConverterAdapter<>(new StringConverter(), String.class));
         }
-        if (from == BigDecimal.class) {
+        if (from == BigDecimal.class && useBigDecimalStringAdapter) {
             return add(key, new ConverterAdapter<>(new BigDecimalConverter(), BigDecimal.class));
         }
-        if (from == BigInteger.class) {
+        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 class LiteralTest {
                 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 class MapperEnhancedTest {
             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 class NumberSerializationTest {
         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 class NumberSerializationTest {
     public static class Num {
         public Number value;
     }
+
+    public static class BigDecimalHolder {
+        public BigDecimal score;
+    }
 }