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

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 5a63d7d0d3 [format] Keep DECIMAL precision when reading JSON numbers 
(#10376)
5a63d7d0d3 is described below

commit 5a63d7d0d3e646e0f6cb63153b45dbb83bc693f6
Author: Eunbin Son <[email protected]>
AuthorDate: Fri Oct 16 14:03:06 2026 +0900

    [format] Keep DECIMAL precision when reading JSON numbers (#10376)
---
 .../apache/paimon/format/json/JsonFileReader.java  | 37 ++++++++-
 .../paimon/format/json/JsonFileFormatTest.java     | 95 ++++++++++++++++++++++
 2 files changed, 130 insertions(+), 2 deletions(-)

diff --git 
a/paimon-format/src/main/java/org/apache/paimon/format/json/JsonFileReader.java 
b/paimon-format/src/main/java/org/apache/paimon/format/json/JsonFileReader.java
index e776170816..6cb07fe4cc 100644
--- 
a/paimon-format/src/main/java/org/apache/paimon/format/json/JsonFileReader.java
+++ 
b/paimon-format/src/main/java/org/apache/paimon/format/json/JsonFileReader.java
@@ -29,6 +29,9 @@ import org.apache.paimon.fs.Path;
 import org.apache.paimon.types.ArrayType;
 import org.apache.paimon.types.DataField;
 import org.apache.paimon.types.DataType;
+import org.apache.paimon.types.DataTypeChecks;
+import org.apache.paimon.types.DataTypeFamily;
+import org.apache.paimon.types.DataTypeRoot;
 import org.apache.paimon.types.DataTypes;
 import org.apache.paimon.types.MapType;
 import org.apache.paimon.types.RowType;
@@ -36,7 +39,9 @@ import org.apache.paimon.types.VectorType;
 import org.apache.paimon.utils.JsonSerdeUtil;
 
 import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.core.JsonProcessingException;
+import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.databind.DeserializationFeature;
 import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.databind.JsonNode;
+import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.databind.ObjectReader;
 
 import javax.annotation.Nullable;
 
@@ -53,6 +58,7 @@ public class JsonFileReader extends AbstractTextFileReader {
     private static final Base64.Decoder BASE64_DECODER = Base64.getDecoder();
 
     private final JsonOptions options;
+    private final ObjectReader jsonReader;
 
     public JsonFileReader(
             FileIO fileIO,
@@ -64,12 +70,34 @@ public class JsonFileReader extends AbstractTextFileReader {
             throws IOException {
         super(fileIO, filePath, rowType, options.getLineDelimiter(), offset, 
length);
         this.options = options;
+        // Parse floating-point numbers as BigDecimal only when needed, so 
DECIMAL values keep
+        // their precision instead of being rounded through double.
+        this.jsonReader =
+                containsDecimal(rowType)
+                        ? JsonSerdeUtil.OBJECT_MAPPER_INSTANCE
+                                .reader()
+                                
.with(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)
+                        : JsonSerdeUtil.OBJECT_MAPPER_INSTANCE.reader();
+    }
+
+    private static boolean containsDecimal(DataType type) {
+        if (type.is(DataTypeRoot.DECIMAL)) {
+            return true;
+        }
+        if (type.is(DataTypeFamily.CONSTRUCTED)) {
+            for (DataType nested : DataTypeChecks.getNestedTypes(type)) {
+                if (containsDecimal(nested)) {
+                    return true;
+                }
+            }
+        }
+        return false;
     }
 
     @Override
     protected InternalRow parseLine(String line) throws IOException {
         try {
-            JsonNode jsonNode = 
JsonSerdeUtil.OBJECT_MAPPER_INSTANCE.readTree(line);
+            JsonNode jsonNode = jsonReader.readTree(line);
             return (InternalRow) convertJsonValue(jsonNode, rowType, options);
         } catch (JsonProcessingException e) {
             if (options.ignoreParseErrors()) {
@@ -108,7 +136,12 @@ public class JsonFileReader extends AbstractTextFileReader 
{
             case ROW:
                 return convertJsonRow(node, (RowType) dataType, options);
             default:
-                return convertPrimitiveStringToType(node.asText(), dataType, 
options);
+                // Non-DECIMAL types keep the double text they got before 
BigDecimal parsing.
+                String text =
+                        node.isBigDecimal() && 
!dataType.is(DataTypeRoot.DECIMAL)
+                                ? String.valueOf(node.doubleValue())
+                                : node.asText();
+                return convertPrimitiveStringToType(text, dataType, options);
         }
     }
 
diff --git 
a/paimon-format/src/test/java/org/apache/paimon/format/json/JsonFileFormatTest.java
 
b/paimon-format/src/test/java/org/apache/paimon/format/json/JsonFileFormatTest.java
index ddb5be035f..3ddd28b5ee 100644
--- 
a/paimon-format/src/test/java/org/apache/paimon/format/json/JsonFileFormatTest.java
+++ 
b/paimon-format/src/test/java/org/apache/paimon/format/json/JsonFileFormatTest.java
@@ -44,6 +44,7 @@ import org.apache.paimon.types.RowType;
 import org.junit.jupiter.api.Test;
 
 import java.io.IOException;
+import java.math.BigDecimal;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Paths;
@@ -666,6 +667,77 @@ public class JsonFileFormatTest extends 
FormatReadWriteTest {
         
assertThat(result.get(0).getVector(1).toFloatArray()).isEqualTo(values);
     }
 
+    @Test
+    public void testReadNumericDecimalKeepsPrecision() throws IOException {
+        // DECIMAL maps to a JSON number, but Paimon's own writer emits 
DECIMAL as a string, so
+        // numeric literals only come from external writers.
+        RowType rowType =
+                RowType.of(
+                        new DataType[] {
+                            DataTypes.DECIMAL(20, 2),
+                            DataTypes.DECIMAL(38, 18),
+                            DataTypes.ARRAY(DataTypes.DECIMAL(20, 2)),
+                            DataTypes.MAP(DataTypes.STRING(), 
DataTypes.DECIMAL(20, 2)),
+                            DataTypes.ROW(DataTypes.FIELD(0, "d", 
DataTypes.DECIMAL(20, 2)))
+                        },
+                        new String[] {"d1", "d2", "arr", "m", "r"});
+        String json =
+                "{\"d1\":12345678901234567.89,\"d2\":123456789.123456789,"
+                        + 
"\"arr\":[12345678901234567.89],\"m\":{\"k\":12345678901234567.89},"
+                        + "\"r\":{\"d\":12345678901234567.89}}";
+
+        List<InternalRow> result = readRawJson(rowType, json);
+
+        assertThat(result).hasSize(1);
+        InternalRow row = result.get(0);
+        BigDecimal expected = new BigDecimal("12345678901234567.89");
+        assertThat(row.getDecimal(0, 20, 
2).toBigDecimal()).isEqualTo(expected);
+        assertThat(row.getDecimal(1, 38, 18).toBigDecimal())
+                .isEqualTo(new BigDecimal("123456789.123456789").setScale(18));
+        assertThat(row.getArray(2).getDecimal(0, 20, 
2).toBigDecimal()).isEqualTo(expected);
+        assertThat(row.getMap(3).valueArray().getDecimal(0, 20, 
2).toBigDecimal())
+                .isEqualTo(expected);
+        assertThat(row.getRow(4, 1).getDecimal(0, 20, 
2).toBigDecimal()).isEqualTo(expected);
+    }
+
+    @Test
+    public void testReadNumericNonDecimalFieldsInDecimalRowUnchanged() throws 
IOException {
+        RowType rowType =
+                RowType.of(
+                        new DataType[] {
+                            DataTypes.DECIMAL(20, 2),
+                            DataTypes.STRING(),
+                            DataTypes.STRING(),
+                            DataTypes.DOUBLE()
+                        },
+                        new String[] {"d", "s1", "s2", "dbl"});
+        String json = "{\"d\":1.5,\"s1\":1e20,\"s2\":1000.0,\"dbl\":1000.0}";
+
+        List<InternalRow> result = readRawJson(rowType, json);
+
+        assertThat(result).hasSize(1);
+        InternalRow row = result.get(0);
+        assertThat(row.getDecimal(0, 20, 2).toBigDecimal()).isEqualTo(new 
BigDecimal("1.50"));
+        assertThat(row.getString(1).toString()).isEqualTo("1.0E20");
+        assertThat(row.getString(2).toString()).isEqualTo("1000.0");
+        assertThat(row.getDouble(3)).isEqualTo(1000.0d);
+    }
+
+    @Test
+    public void testReadNegativeZeroDoubleWithoutDecimal() throws IOException {
+        RowType rowType =
+                RowType.of(
+                        new DataType[] {DataTypes.DOUBLE(), 
DataTypes.STRING()},
+                        new String[] {"dbl", "s"});
+
+        List<InternalRow> result = readRawJson(rowType, 
"{\"dbl\":-0.0,\"s\":-0.0}");
+
+        assertThat(result).hasSize(1);
+        assertThat(Double.doubleToRawLongBits(result.get(0).getDouble(0)))
+                .isEqualTo(Double.doubleToRawLongBits(-0.0d));
+        assertThat(result.get(0).getString(1).toString()).isEqualTo("-0.0");
+    }
+
     @Override
     public boolean supportDataFileWithoutExtension() {
         return true;
@@ -713,6 +785,29 @@ public class JsonFileFormatTest extends 
FormatReadWriteTest {
         return new String(Files.readAllBytes(Paths.get(testFile.toUri())), 
StandardCharsets.UTF_8);
     }
 
+    private List<InternalRow> readRawJson(RowType rowType, String jsonLine) 
throws IOException {
+        FileFormat format =
+                new JsonFileFormat(new FileFormatFactory.FormatContext(new 
Options(), 1024, 1024));
+        Path testFile = new Path(parent, "raw_json_" + UUID.randomUUID() + 
".json");
+        try (PositionOutputStream out = fileIO.newOutputStream(testFile, 
false)) {
+            out.write((jsonLine + "\n").getBytes(StandardCharsets.UTF_8));
+        }
+        try (RecordReader<InternalRow> reader =
+                format.createReaderFactory(rowType, rowType, new ArrayList<>())
+                        .createReader(
+                                new FormatReaderContext(
+                                        fileIO,
+                                        testFile,
+                                        fileIO.getFileSize(testFile),
+                                        null,
+                                        null))) {
+            InternalRowSerializer serializer = new 
InternalRowSerializer(rowType);
+            List<InternalRow> result = new ArrayList<>();
+            reader.forEachRemaining(row -> result.add(serializer.copy(row)));
+            return result;
+        }
+    }
+
     private List<InternalRow> writeThenRead(
             Options options, RowType rowType, List<InternalRow> testData, 
String testPrefix)
             throws IOException {

Reply via email to