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

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


The following commit(s) were added to refs/heads/master by this push:
     new f18d273d48b [FLINK-40854][table] Parse UTF-8 bytes directly in 
PARSE_JSON and TRY_PARSE_JSON
f18d273d48b is described below

commit f18d273d48bb5d4d75e3827177a5437ebba0a3b9
Author: Ramin Gharib <[email protected]>
AuthorDate: Fri Oct 2 09:34:50 2026 +0200

    [FLINK-40854][table] Parse UTF-8 bytes directly in PARSE_JSON and 
TRY_PARSE_JSON
    
    This closes #29342.
---
 .../variant/BinaryVariantInternalBuilder.java      | 20 +++++++-
 .../variant/BinaryVariantInternalBuilderTest.java  | 38 +++++++++++++++
 .../planner/functions/JsonFunctionsITCase.java     | 55 ++++++++++++++++++++--
 .../nodes/exec/stream/VariantSemanticTest.java     |  2 +-
 .../functions/scalar/ParseJsonFunction.java        | 15 +++++-
 .../functions/scalar/TryParseJsonFunction.java     |  2 +-
 6 files changed, 123 insertions(+), 9 deletions(-)

diff --git 
a/flink-core/src/main/java/org/apache/flink/types/variant/BinaryVariantInternalBuilder.java
 
b/flink-core/src/main/java/org/apache/flink/types/variant/BinaryVariantInternalBuilder.java
index 5e3520985d3..47a9bcd886f 100644
--- 
a/flink-core/src/main/java/org/apache/flink/types/variant/BinaryVariantInternalBuilder.java
+++ 
b/flink-core/src/main/java/org/apache/flink/types/variant/BinaryVariantInternalBuilder.java
@@ -21,6 +21,7 @@ package org.apache.flink.types.variant;
 import org.apache.flink.annotation.Internal;
 
 import org.apache.flink.shaded.jackson2.com.fasterxml.jackson.core.JsonFactory;
+import 
org.apache.flink.shaded.jackson2.com.fasterxml.jackson.core.JsonFactoryBuilder;
 import 
org.apache.flink.shaded.jackson2.com.fasterxml.jackson.core.JsonParseException;
 import org.apache.flink.shaded.jackson2.com.fasterxml.jackson.core.JsonParser;
 import org.apache.flink.shaded.jackson2.com.fasterxml.jackson.core.JsonToken;
@@ -96,7 +97,10 @@ public class BinaryVariantInternalBuilder {
             new VariantTypeException("VARIANT_SIZE_LIMIT");
     public static final VariantTypeException VARIANT_DUPLICATE_KEY_EXCEPTION =
             new VariantTypeException("VARIANT_DUPLICATE_KEY");
-    private static final JsonFactory JSON_FACTORY = new JsonFactory();
+    // Byte input is always UTF-8. Charset detection would take NUL bytes or a 
BOM as a UTF-16 or
+    // UTF-32 hint and could turn input that is not valid JSON into a value.
+    private static final JsonFactory JSON_FACTORY =
+            new 
JsonFactoryBuilder().disable(JsonFactory.Feature.CHARSET_DETECTION).build();
 
     public BinaryVariantInternalBuilder(boolean allowDuplicateKeys) {
         this.allowDuplicateKeys = allowDuplicateKeys;
@@ -115,6 +119,20 @@ public class BinaryVariantInternalBuilder {
         }
     }
 
+    /**
+     * Parse UTF-8 encoded JSON bytes as a Variant value. Invalid UTF-8 is 
rejected rather than
+     * replaced with U+FFFD.
+     *
+     * @throws IOException if any JSON parsing error happens.
+     */
+    public static BinaryVariant parseJson(byte[] bytes, boolean 
allowDuplicateKeys)
+            throws IOException {
+        try (JsonParser parser = JSON_FACTORY.createParser(bytes)) {
+            parser.nextToken();
+            return parseJson(parser, allowDuplicateKeys);
+        }
+    }
+
     /**
      * Similar {@link #parseJson(String, boolean)}, but takes a JSON parser 
instead of string input.
      */
diff --git 
a/flink-core/src/test/java/org/apache/flink/types/variant/BinaryVariantInternalBuilderTest.java
 
b/flink-core/src/test/java/org/apache/flink/types/variant/BinaryVariantInternalBuilderTest.java
index a1b3eb5ef06..093be667a0c 100644
--- 
a/flink-core/src/test/java/org/apache/flink/types/variant/BinaryVariantInternalBuilderTest.java
+++ 
b/flink-core/src/test/java/org/apache/flink/types/variant/BinaryVariantInternalBuilderTest.java
@@ -20,13 +20,20 @@ package org.apache.flink.types.variant;
 
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
 import org.junit.jupiter.params.provider.ValueSource;
 
 import java.io.IOException;
 import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.stream.Stream;
 
+import static java.nio.charset.StandardCharsets.UTF_16;
+import static java.nio.charset.StandardCharsets.UTF_16BE;
+import static java.nio.charset.StandardCharsets.UTF_16LE;
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatCode;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -134,6 +141,37 @@ class BinaryVariantInternalBuilderTest {
         assertThat(variant.toJson()).isEqualTo(json);
     }
 
+    @Test
+    void testParseJsonFromUtf8Bytes() throws IOException {
+        final String json = "{\"schlüssel\":\"Grüße, 世界 🚀\",\"キー\":[\"äöü\"]}";
+
+        
assertThat(BinaryVariantInternalBuilder.parseJson(json.getBytes(UTF_8), false))
+                .isEqualTo(BinaryVariantInternalBuilder.parseJson(json, 
false));
+    }
+
+    private static Stream<Arguments> nonUtf8JsonBytes() {
+        return Stream.of(
+                // Charset detection would read these as UTF-16 or UTF-32, or 
skip the BOM.
+                Arguments.of("trailing NUL", "1\u0000".getBytes(UTF_8)),
+                Arguments.of("leading NUL", "\u00001".getBytes(UTF_8)),
+                Arguments.of("UTF-8 BOM", "\uFEFF1".getBytes(UTF_8)),
+                Arguments.of("UTF-16BE", "{\"a\":1}".getBytes(UTF_16BE)),
+                Arguments.of("UTF-16LE", "{\"a\":1}".getBytes(UTF_16LE)),
+                Arguments.of("UTF-16 with BOM", "{\"a\":1}".getBytes(UTF_16)),
+                Arguments.of("invalid start byte", new byte[] {'"', (byte) 
0xFF, '"'}),
+                Arguments.of("truncated sequence", new byte[] {'"', 'a', 
(byte) 0xC3, '"'}),
+                Arguments.of(
+                        "surrogate code point",
+                        new byte[] {'"', (byte) 0xED, (byte) 0xA0, (byte) 
0x80, '"'}));
+    }
+
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("nonUtf8JsonBytes")
+    void testParseJsonRejectsBytesThatAreNotUtf8Json(final String name, final 
byte[] bytes) {
+        assertThatThrownBy(() -> BinaryVariantInternalBuilder.parseJson(bytes, 
false))
+                .isInstanceOf(IOException.class);
+    }
+
     @ParameterizedTest
     @ValueSource(strings = {"NaN", "Infinity", "-Infinity", "1e400", "-1e400"})
     void testParseJsonRejectsNonFiniteNumbers(final String nonFiniteNumber) {
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/JsonFunctionsITCase.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/JsonFunctionsITCase.java
index 358e5dd0bd0..3d8d6eefa4e 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/JsonFunctionsITCase.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/JsonFunctionsITCase.java
@@ -1051,20 +1051,20 @@ class JsonFunctionsITCase extends 
BuiltInFunctionTestBase {
                         .testSqlRuntimeError(
                                 "PARSE_JSON(f1)",
                                 TableRuntimeException.class,
-                                "Failed to parse json string")
+                                "Failed to parse JSON string")
                         .testTableApiRuntimeError(
                                 $("f1").parseJson(),
                                 TableRuntimeException.class,
-                                "Failed to parse json string")
+                                "Failed to parse JSON string")
                         // allowDuplicateKeys: false (the default) rejects 
duplicate keys
                         .testSqlRuntimeError(
                                 "PARSE_JSON(f2, false)",
                                 TableRuntimeException.class,
-                                "Failed to parse json string")
+                                "Failed to parse JSON string")
                         .testTableApiRuntimeError(
                                 $("f2").parseJson(false),
                                 TableRuntimeException.class,
-                                "Failed to parse json string")
+                                "Failed to parse JSON string")
                         // allowDuplicateKeys: true keeps the last occurrence 
of the duplicated key
                         .testResult(
                                 jsonString($("f2").parseJson(true)),
@@ -1096,6 +1096,53 @@ class JsonFunctionsITCase extends 
BuiltInFunctionTestBase {
                                 "JSON_STRING(TRY_PARSE_JSON(f2, true))",
                                 "{\"a\":2}",
                                 STRING()),
+                // Input is parsed from its UTF-8 bytes. A NUL or a BOM must 
not be read as a
+                // UTF-16 or UTF-32 hint that turns invalid JSON into a value. 
Only input that is
+                // not valid UTF-8 reports the index of the invalid byte.
+                // FROM_BASE64 wraps the decoded bytes without validating 
them, so 'Iv8i' yields
+                // the invalid UTF-8 bytes 22 FF 22.
+                TestSetSpec.forFunction(
+                                BuiltInFunctionDefinitions.PARSE_JSON,
+                                "input bytes are always read as UTF-8")
+                        .onFieldsWithData("1\u0000", "\u00001", "\uFEFF1", 
"[\u65E5]", "Iv8i")
+                        .andDataTypes(
+                                STRING().notNull(),
+                                STRING().notNull(),
+                                STRING().notNull(),
+                                STRING().notNull(),
+                                STRING().notNull())
+                        .testSqlRuntimeError(
+                                "PARSE_JSON(f0)",
+                                "Unexpected character ((CTRL-CHAR, code 0)): 
Expected space separating root-level values")
+                        .testSqlRuntimeError(
+                                "PARSE_JSON(f1)",
+                                "Illegal character ((CTRL-CHAR, code 0)): only 
regular white space")
+                        .testSqlRuntimeError(
+                                "PARSE_JSON(f2)",
+                                TableRuntimeException.class,
+                                "Failed to parse JSON string: \uFEFF1")
+                        .testSqlRuntimeError(
+                                "PARSE_JSON(f3)",
+                                TableRuntimeException.class,
+                                "Failed to parse JSON string: [\u65E5]")
+                        .testSqlRuntimeError(
+                                "PARSE_JSON(FROM_BASE64(f4))",
+                                TableRuntimeException.class,
+                                "Failed to parse JSON string: Invalid UTF-8 
byte at index 1 of 3."),
+                TestSetSpec.forFunction(
+                                BuiltInFunctionDefinitions.TRY_PARSE_JSON,
+                                "input bytes are always read as UTF-8")
+                        .onFieldsWithData("1\u0000", "\u00001", "\uFEFF1", 
"Iv8i")
+                        .andDataTypes(
+                                STRING().notNull(),
+                                STRING().notNull(),
+                                STRING().notNull(),
+                                STRING().notNull())
+                        .testSqlResult("JSON_STRING(TRY_PARSE_JSON(f0))", 
null, STRING())
+                        .testSqlResult("JSON_STRING(TRY_PARSE_JSON(f1))", 
null, STRING())
+                        .testSqlResult("JSON_STRING(TRY_PARSE_JSON(f2))", 
null, STRING())
+                        .testSqlResult(
+                                
"JSON_STRING(TRY_PARSE_JSON(FROM_BASE64(f3)))", null, STRING()),
                 TestSetSpec.forFunction(
                                 BuiltInFunctionDefinitions.PARSE_JSON,
                                 "VARIANT expression preceding another 
expression in a"
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/VariantSemanticTest.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/VariantSemanticTest.java
index c3653a2e194..86e5b660210 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/VariantSemanticTest.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/VariantSemanticTest.java
@@ -91,7 +91,7 @@ public class VariantSemanticTest extends SemanticTestBase {
                     .runFailingSql(
                             "INSERT INTO sink_t SELECT PARSE_JSON(v) FROM t",
                             TableRuntimeException.class,
-                            "Failed to parse json string")
+                            "Failed to parse JSON string")
                     .build();
 
     static final TableTestProgram TRY_PARSE_JSON_HANDLE_MALFORMED_JSON =
diff --git 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/scalar/ParseJsonFunction.java
 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/scalar/ParseJsonFunction.java
index 94ed8c980e2..50f758e9b4d 100644
--- 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/scalar/ParseJsonFunction.java
+++ 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/scalar/ParseJsonFunction.java
@@ -20,6 +20,7 @@ package org.apache.flink.table.runtime.functions.scalar;
 
 import org.apache.flink.table.api.TableRuntimeException;
 import org.apache.flink.table.data.StringData;
+import org.apache.flink.table.data.binary.StringUtf8Utils;
 import org.apache.flink.table.functions.BuiltInFunctionDefinitions;
 import org.apache.flink.table.functions.SpecializedFunction;
 import org.apache.flink.types.variant.BinaryVariantInternalBuilder;
@@ -43,11 +44,21 @@ public class ParseJsonFunction extends 
BuiltInScalarFunction {
             return null;
         }
 
+        final byte[] bytes = jsonStr.toBytes();
         try {
-            return BinaryVariantInternalBuilder.parseJson(jsonStr.toString(), 
allowDuplicateKeys);
+            return BinaryVariantInternalBuilder.parseJson(bytes, 
allowDuplicateKeys);
         } catch (Throwable e) {
+            final int invalidIndex =
+                    StringUtf8Utils.firstInvalidUtf8ByteIndex(bytes, 0, 
bytes.length);
+            if (invalidIndex >= 0) {
+                throw new TableRuntimeException(
+                        String.format(
+                                "Failed to parse JSON string: Invalid UTF-8 
byte at index %d of %d.",
+                                invalidIndex, bytes.length),
+                        e);
+            }
             throw new TableRuntimeException(
-                    String.format("Failed to parse json string: %s", jsonStr), 
e);
+                    String.format("Failed to parse JSON string: %s", jsonStr), 
e);
         }
     }
 }
diff --git 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/scalar/TryParseJsonFunction.java
 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/scalar/TryParseJsonFunction.java
index 9cdde087ad7..161e1c65131 100644
--- 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/scalar/TryParseJsonFunction.java
+++ 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/scalar/TryParseJsonFunction.java
@@ -43,7 +43,7 @@ public class TryParseJsonFunction extends 
BuiltInScalarFunction {
         }
 
         try {
-            return BinaryVariantInternalBuilder.parseJson(jsonStr.toString(), 
allowDuplicateKeys);
+            return BinaryVariantInternalBuilder.parseJson(jsonStr.toBytes(), 
allowDuplicateKeys);
         } catch (Throwable e) {
             return null;
         }

Reply via email to