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

snuyanzin 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 bf4a71a990d [FLINK-40545][table] Add `parseJson()` and 
`tryParseJson()` Table API expression methods
bf4a71a990d is described below

commit bf4a71a990dd82225ab33ba331c2f80021b5f1da
Author: Ramin Gharib <[email protected]>
AuthorDate: Sun Sep 6 10:41:44 2026 +0200

    [FLINK-40545][table] Add `parseJson()` and `tryParseJson()` Table API 
expression methods
---
 docs/data/sql_functions.yml                        |  12 +-
 docs/data/sql_functions_zh.yml                     |   2 +
 flink-python/pyflink/table/expression.py           |  35 +++++-
 .../flink/table/api/internal/BaseExpressions.java  |  52 ++++++++
 .../planner/functions/CastFunctionITCase.java      | 136 ++++++++++-----------
 .../planner/functions/JsonFunctionsITCase.java     |  86 +++++++++----
 6 files changed, 227 insertions(+), 96 deletions(-)

diff --git a/docs/data/sql_functions.yml b/docs/data/sql_functions.yml
index e6ce79e47f7..8dc465da407 100644
--- a/docs/data/sql_functions.yml
+++ b/docs/data/sql_functions.yml
@@ -1338,17 +1338,19 @@ json:
 
 variant:
   - sql: PARSE_JSON(json_string[, allow_duplicate_keys])
+    table: STRING.parseJson([allowDuplicateKeys])
     description: |
-      Parse a JSON string into a Variant. If the JSON string is invalid, an 
error will be thrown. 
+      Parse a JSON string into a Variant. If the JSON string is invalid, an 
error will be thrown.
       To return NULL instead of an error, use the `TRY_PARSE_JSON` function.
-      
-      If there are duplicate keys in the input JSON string, when 
`allowDuplicateKeys` is true, the 
-      parser will keep the last occurrence of all fields with the same key, 
otherwise when 
-      `allowDuplicateKeys` is false it will throw an error. The default value 
of 
+
+      If there are duplicate keys in the input JSON string, when 
`allowDuplicateKeys` is true, the
+      parser will keep the last occurrence of all fields with the same key, 
otherwise when
+      `allowDuplicateKeys` is false it will throw an error. The default value 
of
       `allowDuplicateKeys` is false.
 
       See the `VARIANT` data type documentation for how JSON values are mapped 
to variant kinds.
   - sql: TRY_PARSE_JSON(json_string[, allow_duplicate_keys])
+    table: STRING.tryParseJson([allowDuplicateKeys])
     description: |
       Try to parse a JSON string into a Variant if possible. If the JSON 
string is invalid, return 
       NULL. To throw an error instead of returning NULL, use the `PARSE_JSON` 
function.
diff --git a/docs/data/sql_functions_zh.yml b/docs/data/sql_functions_zh.yml
index b5819bbede2..85e28b198c3 100644
--- a/docs/data/sql_functions_zh.yml
+++ b/docs/data/sql_functions_zh.yml
@@ -1423,6 +1423,7 @@ json:
 
 variant:
   - sql: PARSE_JSON(json_string[, allow_duplicate_keys])
+    table: STRING.parseJson([allowDuplicateKeys])
     description: |
       将 JSON 字符串解析为 Variant。如果 JSON 字符串无效,将抛出错误。如果希望返回 NULL 而不是抛出错误,
       请使用 `TRY_PARSE_JSON` 函数。  
@@ -1434,6 +1435,7 @@ variant:
       See the `VARIANT` data type documentation for how JSON values are mapped 
to variant kinds.
 
   - sql: TRY_PARSE_JSON(json_string[, allow_duplicate_keys])
+    table: STRING.tryParseJson([allowDuplicateKeys])
     description: |
       尽可能将 JSON 字符串解析为 Variant。如果 JSON 字符串无效,则返回 NULL。如果希望抛出错误而不是返回 NULL,
       请使用 `PARSE_JSON` 函数。  
diff --git a/flink-python/pyflink/table/expression.py 
b/flink-python/pyflink/table/expression.py
index e82e3bc02d2..be21605124a 100644
--- a/flink-python/pyflink/table/expression.py
+++ b/flink-python/pyflink/table/expression.py
@@ -95,7 +95,8 @@ _string_doc_seealso = """
              :func:`~Expression.regexp_extract`, :func:`~Expression.substring`,
              :py:attr:`~Expression.from_base64`, 
:py:attr:`~Expression.to_base64`,
              :func:`~Expression.ltrim`, :func:`~Expression.rtrim`, 
:func:`~Expression.repeat`,
-             :func:`~Expression.json_quote`, :func:`~Expression.json_unquote`
+             :func:`~Expression.json_quote`, :func:`~Expression.json_unquote`,
+             :func:`~Expression.parse_json`, :func:`~Expression.try_parse_json`
 """
 
 _temporal_doc_seealso = """
@@ -194,7 +195,8 @@ def _make_string_doc():
         Expression.lpad, Expression.rpad, Expression.overlay, 
Expression.regexp_replace,
         Expression.regexp_extract, Expression.from_base64, 
Expression.to_base64,
         Expression.ltrim, Expression.rtrim, Expression.repeat,
-        Expression.json_quote, Expression.json_unquote
+        Expression.json_quote, Expression.json_unquote,
+        Expression.parse_json, Expression.try_parse_json
     ]
 
     for func in string_funcs:
@@ -2280,6 +2282,35 @@ class Expression(Generic[T]):
         """
         return _unary_op("jsonUnquote")(self)
 
+    def parse_json(self, allow_duplicate_keys=None) -> 'Expression':
+        """
+        Parses a JSON string into a value of VARIANT type. If the JSON string 
is invalid,
+        an error is thrown. To return None instead of an error, use
+        :func:`~Expression.try_parse_json`.
+
+        If there are duplicate keys in the input, allow_duplicate_keys 
controls whether the
+        parser keeps the last occurrence of each duplicated key (True) or 
throws an error
+        (False). The default value of allow_duplicate_keys is False.
+        """
+        if allow_duplicate_keys is None:
+            return _unary_op("parseJson")(self)
+        else:
+            return _binary_op("parseJson")(self, allow_duplicate_keys)
+
+    def try_parse_json(self, allow_duplicate_keys=None) -> 'Expression':
+        """
+        Parses a JSON string into a value of VARIANT type. If the JSON string 
is invalid,
+        None is returned. To throw an error instead, use 
:func:`~Expression.parse_json`.
+
+        If there are duplicate keys in the input, allow_duplicate_keys 
controls whether the
+        parser keeps the last occurrence of each duplicated key (True) or 
throws an error
+        (False). The default value of allow_duplicate_keys is False.
+        """
+        if allow_duplicate_keys is None:
+            return _unary_op("tryParseJson")(self)
+        else:
+            return _binary_op("tryParseJson")(self, allow_duplicate_keys)
+
     def json_length(self, path=None) -> 'Expression':
         """
         Returns the number of elements in a JSON document, or the length of 
the value at the
diff --git 
a/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/api/internal/BaseExpressions.java
 
b/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/api/internal/BaseExpressions.java
index bd95cb9a73f..536105ea6bc 100644
--- 
a/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/api/internal/BaseExpressions.java
+++ 
b/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/api/internal/BaseExpressions.java
@@ -180,6 +180,7 @@ import static 
org.apache.flink.table.functions.BuiltInFunctionDefinitions.ORDER_
 import static 
org.apache.flink.table.functions.BuiltInFunctionDefinitions.ORDER_DESC;
 import static org.apache.flink.table.functions.BuiltInFunctionDefinitions.OVER;
 import static 
org.apache.flink.table.functions.BuiltInFunctionDefinitions.OVERLAY;
+import static 
org.apache.flink.table.functions.BuiltInFunctionDefinitions.PARSE_JSON;
 import static 
org.apache.flink.table.functions.BuiltInFunctionDefinitions.PARSE_URL;
 import static 
org.apache.flink.table.functions.BuiltInFunctionDefinitions.PERCENTILE;
 import static org.apache.flink.table.functions.BuiltInFunctionDefinitions.PLUS;
@@ -232,6 +233,7 @@ import static 
org.apache.flink.table.functions.BuiltInFunctionDefinitions.TRANSL
 import static org.apache.flink.table.functions.BuiltInFunctionDefinitions.TRIM;
 import static 
org.apache.flink.table.functions.BuiltInFunctionDefinitions.TRUNCATE;
 import static 
org.apache.flink.table.functions.BuiltInFunctionDefinitions.TRY_CAST;
+import static 
org.apache.flink.table.functions.BuiltInFunctionDefinitions.TRY_PARSE_JSON;
 import static 
org.apache.flink.table.functions.BuiltInFunctionDefinitions.UNHEX;
 import static 
org.apache.flink.table.functions.BuiltInFunctionDefinitions.UPPER;
 import static 
org.apache.flink.table.functions.BuiltInFunctionDefinitions.URL_DECODE;
@@ -1367,6 +1369,56 @@ public abstract class BaseExpressions<InType, OutType> {
         return toApiSpecificExpression(unresolvedCall(JSON_UNQUOTE, 
objectToExpression(toExpr())));
     }
 
+    /**
+     * Parses a JSON string into a value of type {@link DataTypes#VARIANT()}. 
If the JSON string is
+     * invalid, an error is thrown. To return {@code NULL} instead of an 
error, use {@link
+     * #tryParseJson()}.
+     *
+     * <p>This is a shortcut for {@code parseJson(false)}. See {@link 
#parseJson(boolean)}.
+     */
+    public OutType parseJson() {
+        return toApiSpecificExpression(unresolvedCall(PARSE_JSON, 
objectToExpression(toExpr())));
+    }
+
+    /**
+     * Parses a JSON string into a value of type {@link DataTypes#VARIANT()}. 
If the JSON string is
+     * invalid, an error is thrown. To return {@code NULL} instead of an 
error, use {@link
+     * #tryParseJson(boolean)}.
+     *
+     * <p>If there are duplicate keys in the input, {@code allowDuplicateKeys} 
controls whether the
+     * parser keeps the last occurrence of each duplicated key ({@code true}) 
or throws an error
+     * ({@code false}).
+     */
+    public OutType parseJson(boolean allowDuplicateKeys) {
+        return toApiSpecificExpression(
+                unresolvedCall(PARSE_JSON, toExpr(), 
valueLiteral(allowDuplicateKeys)));
+    }
+
+    /**
+     * Parses a JSON string into a value of type {@link DataTypes#VARIANT()}. 
If the JSON string is
+     * invalid, {@code NULL} is returned. To throw an error instead, use 
{@link #parseJson()}.
+     *
+     * <p>This is a shortcut for {@code tryParseJson(false)}. See {@link 
#tryParseJson(boolean)}.
+     */
+    public OutType tryParseJson() {
+        return toApiSpecificExpression(
+                unresolvedCall(TRY_PARSE_JSON, objectToExpression(toExpr())));
+    }
+
+    /**
+     * Parses a JSON string into a value of type {@link DataTypes#VARIANT()}. 
If the JSON string is
+     * invalid, {@code NULL} is returned. To throw an error instead, use {@link
+     * #parseJson(boolean)}.
+     *
+     * <p>If there are duplicate keys in the input, {@code allowDuplicateKeys} 
controls whether the
+     * parser keeps the last occurrence of each duplicated key ({@code true}) 
or throws an error
+     * ({@code false}).
+     */
+    public OutType tryParseJson(boolean allowDuplicateKeys) {
+        return toApiSpecificExpression(
+                unresolvedCall(TRY_PARSE_JSON, toExpr(), 
valueLiteral(allowDuplicateKeys)));
+    }
+
     /** Returns the base string decoded with base64. */
     public OutType fromBase64() {
         return toApiSpecificExpression(unresolvedCall(FROM_BASE64, toExpr()));
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/CastFunctionITCase.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/CastFunctionITCase.java
index 083fda47dff..4c87bcb62bb 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/CastFunctionITCase.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/CastFunctionITCase.java
@@ -79,7 +79,7 @@ import static org.apache.flink.table.api.DataTypes.VARCHAR;
 import static org.apache.flink.table.api.DataTypes.VARIANT;
 import static org.apache.flink.table.api.DataTypes.YEAR;
 import static org.apache.flink.table.api.Expressions.$;
-import static org.apache.flink.table.api.Expressions.call;
+import static org.apache.flink.table.api.Expressions.lit;
 import static org.apache.flink.util.CollectionUtil.entry;
 import static org.apache.flink.util.CollectionUtil.map;
 import static org.assertj.core.api.Assertions.assertThat;
@@ -147,238 +147,238 @@ public class CastFunctionITCase extends 
BuiltInFunctionTestBase {
 
     private static List<TestSetSpec> variantPrimitiveCasts() {
         return List.of(
-                TestSetSpec.forExpression("Cast a VARIANT produced by 
PARSE_JSON to a primitive")
+                TestSetSpec.forExpression("Cast a VARIANT produced by 
parseJson() to a primitive")
                         .onFieldsWithData("unused")
                         .andDataTypes(STRING())
                         // An integer converts to any integer target while the 
value stays in
                         // range, and to FLOAT or DOUBLE which are approximate 
by definition.
                         .testResult(
-                                call("PARSE_JSON", "42").cast(TINYINT()),
+                                lit("42").parseJson().cast(TINYINT()),
                                 "CAST(PARSE_JSON('42') AS TINYINT)",
                                 (byte) 42,
                                 TINYINT().notNull())
                         .testResult(
-                                call("PARSE_JSON", "42").cast(SMALLINT()),
+                                lit("42").parseJson().cast(SMALLINT()),
                                 "CAST(PARSE_JSON('42') AS SMALLINT)",
                                 (short) 42,
                                 SMALLINT().notNull())
                         .testResult(
-                                call("PARSE_JSON", "42").cast(INT()),
+                                lit("42").parseJson().cast(INT()),
                                 "CAST(PARSE_JSON('42') AS INT)",
                                 42,
                                 INT().notNull())
                         .testResult(
-                                call("PARSE_JSON", "42").cast(BIGINT()),
+                                lit("42").parseJson().cast(BIGINT()),
                                 "CAST(PARSE_JSON('42') AS BIGINT)",
                                 42L,
                                 BIGINT().notNull())
                         .testResult(
-                                call("PARSE_JSON", "42").cast(FLOAT()),
+                                lit("42").parseJson().cast(FLOAT()),
                                 "CAST(PARSE_JSON('42') AS FLOAT)",
                                 42.0f,
                                 FLOAT().notNull())
                         .testResult(
-                                call("PARSE_JSON", "42").cast(DOUBLE()),
+                                lit("42").parseJson().cast(DOUBLE()),
                                 "CAST(PARSE_JSON('42') AS DOUBLE)",
                                 42.0d,
                                 DOUBLE().notNull())
                         // An out-of-range value is rejected rather than 
wrapped.
                         .testResult(
-                                call("PARSE_JSON", "1000").cast(SMALLINT()),
+                                lit("1000").parseJson().cast(SMALLINT()),
                                 "CAST(PARSE_JSON('1000') AS SMALLINT)",
                                 (short) 1000,
                                 SMALLINT().notNull())
                         .testTableApiRuntimeError(
-                                call("PARSE_JSON", "1000").cast(TINYINT()), 
"overflowed")
+                                lit("1000").parseJson().cast(TINYINT()), 
"overflowed")
                         .testSqlRuntimeError("CAST(PARSE_JSON('1000') AS 
TINYINT)", "overflowed")
                         .testResult(
-                                call("PARSE_JSON", "1000").tryCast(TINYINT()),
+                                lit("1000").parseJson().tryCast(TINYINT()),
                                 "TRY_CAST(PARSE_JSON('1000') AS TINYINT)",
                                 null,
                                 TINYINT())
                         // A decimal reaches an integer target when it is 
already integral.
                         .testResult(
-                                call("PARSE_JSON", "7.0").cast(INT()),
+                                lit("7.0").parseJson().cast(INT()),
                                 "CAST(PARSE_JSON('7.0') AS INT)",
                                 7,
                                 INT().notNull())
                         // A fractional value is rejected, since converting 
would drop digits.
                         .testTableApiRuntimeError(
-                                call("PARSE_JSON", "123.456").cast(INT()), 
"lose precision")
+                                lit("123.456").parseJson().cast(INT()), "lose 
precision")
                         .testResult(
-                                call("PARSE_JSON", "123.456").tryCast(INT()),
+                                lit("123.456").parseJson().tryCast(INT()),
                                 "TRY_CAST(PARSE_JSON('123.456') AS INT)",
                                 null,
                                 INT())
                         // A DECIMAL target has to hold the value exactly.
                         .testResult(
-                                call("PARSE_JSON", "123.456").cast(DECIMAL(6, 
3)),
+                                lit("123.456").parseJson().cast(DECIMAL(6, 3)),
                                 "CAST(PARSE_JSON('123.456') AS DECIMAL(6, 3))",
                                 new BigDecimal("123.456"),
                                 DECIMAL(6, 3).notNull())
                         .testTableApiRuntimeError(
-                                call("PARSE_JSON", "123.456").cast(DECIMAL(6, 
2)), "lose precision")
+                                lit("123.456").parseJson().cast(DECIMAL(6, 
2)), "lose precision")
                         .testResult(
-                                call("PARSE_JSON", 
"123.456").tryCast(DECIMAL(6, 2)),
+                                lit("123.456").parseJson().tryCast(DECIMAL(6, 
2)),
                                 "TRY_CAST(PARSE_JSON('123.456') AS DECIMAL(6, 
2))",
                                 null,
                                 DECIMAL(6, 2))
                         .testTableApiRuntimeError(
-                                call("PARSE_JSON", "123.456").cast(DECIMAL(5, 
3)), "overflowed")
+                                lit("123.456").parseJson().cast(DECIMAL(5, 
3)), "overflowed")
                         .testResult(
-                                call("PARSE_JSON", 
"123.456").tryCast(DECIMAL(5, 3)),
+                                lit("123.456").parseJson().tryCast(DECIMAL(5, 
3)),
                                 "TRY_CAST(PARSE_JSON('123.456') AS DECIMAL(5, 
3))",
                                 null,
                                 DECIMAL(5, 3))
                         // An integer is exact, so it reaches a DECIMAL that 
has room for it.
                         .testResult(
-                                call("PARSE_JSON", "42").cast(DECIMAL(5, 2)),
+                                lit("42").parseJson().cast(DECIMAL(5, 2)),
                                 "CAST(PARSE_JSON('42') AS DECIMAL(5, 2))",
                                 new BigDecimal("42.00"),
                                 DECIMAL(5, 2).notNull())
                         // A decimal reaches an approximate target, where 
losing digits is expected.
                         .testResult(
-                                call("PARSE_JSON", "123.456").cast(FLOAT()),
+                                lit("123.456").parseJson().cast(FLOAT()),
                                 "CAST(PARSE_JSON('123.456') AS FLOAT)",
                                 123.456f,
                                 FLOAT().notNull())
                         .testResult(
-                                call("PARSE_JSON", "123.456").cast(DOUBLE()),
+                                lit("123.456").parseJson().cast(DOUBLE()),
                                 "CAST(PARSE_JSON('123.456') AS DOUBLE)",
                                 123.456d,
                                 DOUBLE().notNull())
                         // A magnitude the target cannot represent is still 
rejected.
                         .testTableApiRuntimeError(
-                                call("PARSE_JSON", "1e40").cast(FLOAT()), 
"overflowed")
+                                lit("1e40").parseJson().cast(FLOAT()), 
"overflowed")
                         .testResult(
-                                call("PARSE_JSON", "1e40").tryCast(FLOAT()),
+                                lit("1e40").parseJson().tryCast(FLOAT()),
                                 "TRY_CAST(PARSE_JSON('1e40') AS FLOAT)",
                                 null,
                                 FLOAT())
                         .testResult(
-                                call("PARSE_JSON", "1e20").cast(DOUBLE()),
+                                lit("1e20").parseJson().cast(DOUBLE()),
                                 "CAST(PARSE_JSON('1e20') AS DOUBLE)",
                                 1e20,
                                 DOUBLE().notNull())
                         .testResult(
-                                call("PARSE_JSON", "true").cast(BOOLEAN()),
+                                lit("true").parseJson().cast(BOOLEAN()),
                                 "CAST(PARSE_JSON('true') AS BOOLEAN)",
                                 true,
                                 BOOLEAN().notNull())
                         // CAST returns the raw scalar value (string unquoted)
                         .testResult(
-                                call("PARSE_JSON", "\"foo\"").cast(STRING()),
+                                lit("\"foo\"").parseJson().cast(STRING()),
                                 "CAST(PARSE_JSON('\"foo\"') AS STRING)",
                                 "foo",
                                 STRING().notNull())
                         .testResult(
-                                call("PARSE_JSON", "123.456").cast(STRING()),
+                                lit("123.456").parseJson().cast(STRING()),
                                 "CAST(PARSE_JSON('123.456') AS STRING)",
                                 "123.456",
                                 STRING().notNull())
                         // The rendering matches a regular cast of the stored 
kind, so a boolean
                         // becomes TRUE rather than the JSON true.
                         .testResult(
-                                call("PARSE_JSON", "true").cast(STRING()),
+                                lit("true").parseJson().cast(STRING()),
                                 "CAST(PARSE_JSON('true') AS STRING)",
                                 "TRUE",
                                 STRING().notNull())
                         // An object or array has no scalar value, so the 
error points to
                         // JSON_STRING.
                         .testTableApiRuntimeError(
-                                call("PARSE_JSON", "[\"a\", 
\"b\"]").cast(STRING()), "JSON_STRING")
+                                lit("[\"a\", 
\"b\"]").parseJson().cast(STRING()), "JSON_STRING")
                         .testResult(
-                                call("PARSE_JSON", "[\"a\", 
\"b\"]").tryCast(STRING()),
+                                lit("[\"a\", 
\"b\"]").parseJson().tryCast(STRING()),
                                 "TRY_CAST(PARSE_JSON('[\"a\", \"b\"]') AS 
STRING)",
                                 null,
                                 STRING())
                         .testTableApiRuntimeError(
-                                call("PARSE_JSON", "{\"a\": 
1}").cast(STRING()), "JSON_STRING")
+                                lit("{\"a\": 1}").parseJson().cast(STRING()), 
"JSON_STRING")
                         .testResult(
-                                call("PARSE_JSON", "{\"a\": 
1}").tryCast(STRING()),
+                                lit("{\"a\": 
1}").parseJson().tryCast(STRING()),
                                 "TRY_CAST(PARSE_JSON('{\"a\": 1}') AS STRING)",
                                 null,
                                 STRING())
                         // A bounded CHAR/VARCHAR target trims a longer value, 
and CHAR pads a
                         // shorter one to its fixed width, the same as a 
regular cast into it.
                         .testResult(
-                                call("PARSE_JSON", "\"ab\"").cast(VARCHAR(3)),
+                                lit("\"ab\"").parseJson().cast(VARCHAR(3)),
                                 "CAST(PARSE_JSON('\"ab\"') AS VARCHAR(3))",
                                 "ab",
                                 VARCHAR(3).notNull())
                         .testResult(
-                                call("PARSE_JSON", 
"\"foobar\"").cast(VARCHAR(3)),
+                                lit("\"foobar\"").parseJson().cast(VARCHAR(3)),
                                 "CAST(PARSE_JSON('\"foobar\"') AS VARCHAR(3))",
                                 "foo",
                                 VARCHAR(3).notNull())
                         .testResult(
-                                call("PARSE_JSON", 
"\"foobar\"").tryCast(VARCHAR(3)),
+                                
lit("\"foobar\"").parseJson().tryCast(VARCHAR(3)),
                                 "TRY_CAST(PARSE_JSON('\"foobar\"') AS 
VARCHAR(3))",
                                 "foo",
                                 VARCHAR(3))
                         .testResult(
-                                call("PARSE_JSON", "\"abc\"").cast(CHAR(3)),
+                                lit("\"abc\"").parseJson().cast(CHAR(3)),
                                 "CAST(PARSE_JSON('\"abc\"') AS CHAR(3))",
                                 "abc",
                                 CHAR(3).notNull())
                         .testResult(
-                                call("PARSE_JSON", "\"abcdef\"").cast(CHAR(3)),
+                                lit("\"abcdef\"").parseJson().cast(CHAR(3)),
                                 "CAST(PARSE_JSON('\"abcdef\"') AS CHAR(3))",
                                 "abc",
                                 CHAR(3).notNull())
                         .testResult(
-                                call("PARSE_JSON", "\"ab\"").cast(CHAR(5)),
+                                lit("\"ab\"").parseJson().cast(CHAR(5)),
                                 "CAST(PARSE_JSON('\"ab\"') AS CHAR(5))",
                                 "ab   ",
                                 CHAR(5).notNull())
                         .testResult(
-                                call("PARSE_JSON", "\"ab\"").tryCast(CHAR(5)),
+                                lit("\"ab\"").parseJson().tryCast(CHAR(5)),
                                 "TRY_CAST(PARSE_JSON('\"ab\"') AS CHAR(5))",
                                 "ab   ",
                                 CHAR(5))
                         // A variant holding a JSON null casts to SQL NULL, 
not to the text 'null'.
                         // The length of that text must not be checked against 
the target either.
                         .testResult(
-                                call("TRY_PARSE_JSON", "null").cast(STRING()),
+                                lit("null").tryParseJson().cast(STRING()),
                                 "CAST(TRY_PARSE_JSON('null') AS STRING)",
                                 null,
                                 STRING())
                         .testResult(
-                                call("PARSE_JSON", "null").tryCast(STRING()),
+                                lit("null").parseJson().tryCast(STRING()),
                                 "TRY_CAST(PARSE_JSON('null') AS STRING)",
                                 null,
                                 STRING())
                         .testResult(
-                                call("TRY_PARSE_JSON", "null").cast(CHAR(2)),
+                                lit("null").tryParseJson().cast(CHAR(2)),
                                 "CAST(TRY_PARSE_JSON('null') AS CHAR(2))",
                                 null,
                                 CHAR(2))
                         .testResult(
-                                call("PARSE_JSON", "null").tryCast(VARCHAR(2)),
+                                lit("null").parseJson().tryCast(VARCHAR(2)),
                                 "TRY_CAST(PARSE_JSON('null') AS VARCHAR(2))",
                                 null,
                                 VARCHAR(2))
                         // TRY_CAST of a value whose kind does not match the 
target returns NULL
                         .testResult(
-                                call("PARSE_JSON", "\"foo\"").tryCast(INT()),
+                                lit("\"foo\"").parseJson().tryCast(INT()),
                                 "TRY_CAST(PARSE_JSON('\"foo\"') AS INT)",
                                 null,
                                 INT())
                         // A variant that stores a JSON null casts to SQL NULL 
when the target is
                         // nullable: a nullable variant source, or TRY_CAST 
which forces nullable.
                         .testResult(
-                                call("TRY_PARSE_JSON", "null").cast(INT()),
+                                lit("null").tryParseJson().cast(INT()),
                                 "CAST(TRY_PARSE_JSON('null') AS INT)",
                                 null,
                                 INT())
                         .testResult(
-                                call("PARSE_JSON", "null").tryCast(BOOLEAN()),
+                                lit("null").parseJson().tryCast(BOOLEAN()),
                                 "TRY_CAST(PARSE_JSON('null') AS BOOLEAN)",
                                 null,
                                 BOOLEAN())
                         // A nullable variant with a concrete value still 
casts normally.
                         .testResult(
-                                call("TRY_PARSE_JSON", "42").cast(TINYINT()),
+                                lit("42").tryParseJson().cast(TINYINT()),
                                 "CAST(TRY_PARSE_JSON('42') AS TINYINT)",
                                 (byte) 42,
                                 TINYINT()));
@@ -386,91 +386,91 @@ public class CastFunctionITCase extends 
BuiltInFunctionTestBase {
 
     private static List<TestSetSpec> variantArrayCasts() {
         return List.of(
-                TestSetSpec.forExpression("Cast a VARIANT produced by 
PARSE_JSON to an ARRAY")
+                TestSetSpec.forExpression("Cast a VARIANT produced by 
parseJson() to an ARRAY")
                         .onFieldsWithData("unused")
                         .andDataTypes(STRING())
                         // ARRAY: each element casts by the same 
VARIANT-to-element rule.
                         .testResult(
-                                call("PARSE_JSON", "[1, 2, 
3]").cast(ARRAY(INT())),
+                                lit("[1, 2, 
3]").parseJson().cast(ARRAY(INT())),
                                 "CAST(PARSE_JSON('[1, 2, 3]') AS ARRAY<INT>)",
                                 new Integer[] {1, 2, 3},
                                 ARRAY(INT()).notNull())
                         // an approximate leaf takes any numeric kind
                         .testResult(
-                                call("PARSE_JSON", "[1, 2, 
3]").cast(ARRAY(DOUBLE())),
+                                lit("[1, 2, 
3]").parseJson().cast(ARRAY(DOUBLE())),
                                 "CAST(PARSE_JSON('[1, 2, 3]') AS 
ARRAY<DOUBLE>)",
                                 new Double[] {1.0, 2.0, 3.0},
                                 ARRAY(DOUBLE()).notNull())
                         // each element renders to string like the scalar cast
                         .testResult(
-                                call("PARSE_JSON", "[1, 2, 
3]").cast(ARRAY(STRING())),
+                                lit("[1, 2, 
3]").parseJson().cast(ARRAY(STRING())),
                                 "CAST(PARSE_JSON('[1, 2, 3]') AS 
ARRAY<STRING>)",
                                 new String[] {"1", "2", "3"},
                                 ARRAY(STRING()).notNull())
                         // a heterogeneous array renders every element to 
string
                         .testResult(
-                                call("PARSE_JSON", "[1, \"a\", 2, 
\"b\"]").cast(ARRAY(STRING())),
+                                lit("[1, \"a\", 2, 
\"b\"]").parseJson().cast(ARRAY(STRING())),
                                 "CAST(PARSE_JSON('[1, \"a\", 2, \"b\"]') AS 
ARRAY<STRING>)",
                                 new String[] {"1", "a", "2", "b"},
                                 ARRAY(STRING()).notNull())
                         .testResult(
-                                call("PARSE_JSON", "[]").cast(ARRAY(INT())),
+                                lit("[]").parseJson().cast(ARRAY(INT())),
                                 "CAST(PARSE_JSON('[]') AS ARRAY<INT>)",
                                 new Integer[] {},
                                 ARRAY(INT()).notNull())
                         // a VARIANT null element maps to SQL NULL for a 
nullable element type
                         .testResult(
-                                call("PARSE_JSON", "[1, null, 
3]").cast(ARRAY(INT())),
+                                lit("[1, null, 
3]").parseJson().cast(ARRAY(INT())),
                                 "CAST(PARSE_JSON('[1, null, 3]') AS 
ARRAY<INT>)",
                                 new Integer[] {1, null, 3},
                                 ARRAY(INT()).notNull())
                         // a VARIANT null element fails a NOT NULL element type
                         .testTableApiRuntimeError(
-                                call("PARSE_JSON", "[1, null, 
3]").cast(ARRAY(INT().notNull())),
+                                lit("[1, null, 
3]").parseJson().cast(ARRAY(INT().notNull())),
                                 "NOT NULL element type")
                         .testSqlRuntimeError(
                                 "CAST(PARSE_JSON('[1, null, 3]') AS ARRAY<INT 
NOT NULL>)",
                                 "NOT NULL element type")
                         // a stored string is never parsed into an integer
                         .testTableApiRuntimeError(
-                                call("PARSE_JSON", "[\"1\", 
\"2\"]").cast(ARRAY(INT())),
+                                lit("[\"1\", 
\"2\"]").parseJson().cast(ARRAY(INT())),
                                 "does not change the type")
                         .testSqlRuntimeError(
                                 "CAST(PARSE_JSON('[\"1\", \"2\"]') AS 
ARRAY<INT>)",
                                 "does not change the type")
                         .testResult(
-                                call("PARSE_JSON", "[\"1\", 
\"2\"]").tryCast(ARRAY(INT())),
+                                lit("[\"1\", 
\"2\"]").parseJson().tryCast(ARRAY(INT())),
                                 "TRY_CAST(PARSE_JSON('[\"1\", \"2\"]') AS 
ARRAY<INT>)",
                                 null,
                                 ARRAY(INT()))
                         // a heterogeneous array fails on the first element 
that is not an integer
                         .testTableApiRuntimeError(
-                                call("PARSE_JSON", "[1, \"a\", 2, 
\"b\"]").cast(ARRAY(INT())),
+                                lit("[1, \"a\", 2, 
\"b\"]").parseJson().cast(ARRAY(INT())),
                                 "does not change the type")
                         // a fractional element cannot narrow to INT without 
dropping digits
                         .testTableApiRuntimeError(
-                                call("PARSE_JSON", "[1.23, 2.45, 
3.67]").cast(ARRAY(INT())),
+                                lit("[1.23, 2.45, 
3.67]").parseJson().cast(ARRAY(INT())),
                                 "lose precision")
                         .testSqlRuntimeError(
                                 "CAST(PARSE_JSON('[1.23, 2.45, 3.67]') AS 
ARRAY<INT>)",
                                 "lose precision")
                         // TRY_CAST swallows the failure and returns NULL for 
the whole array
                         .testResult(
-                                call("PARSE_JSON", "[1.23, 2.45, 
3.67]").tryCast(ARRAY(INT())),
+                                lit("[1.23, 2.45, 
3.67]").parseJson().tryCast(ARRAY(INT())),
                                 "TRY_CAST(PARSE_JSON('[1.23, 2.45, 3.67]') AS 
ARRAY<INT>)",
                                 null,
                                 ARRAY(INT()))
                         // an object is not an array
                         .testTableApiRuntimeError(
-                                call(
-                                                "PARSE_JSON",
-                                                "{\"id\": 7, \"name\": 
\"ada\", \"active\": true}")
+                                lit("{\"id\": 7, \"name\": \"ada\", 
\"active\": true}")
+                                        .parseJson()
                                         .cast(ARRAY(INT())),
                                 "requires an array")
                         // ARRAY<VARIANT> shreds one level and keeps the 
elements as variants, which
                         // then cast back to INT unchanged
                         .testResult(
-                                call("PARSE_JSON", "[1, 2, 3]")
+                                lit("[1, 2, 3]")
+                                        .parseJson()
                                         .cast(ARRAY(VARIANT()))
                                         .cast(ARRAY(INT())),
                                 "CAST(CAST(PARSE_JSON('[1, 2, 3]') AS 
ARRAY<VARIANT>) AS ARRAY<INT>)",
@@ -478,19 +478,19 @@ public class CastFunctionITCase extends 
BuiltInFunctionTestBase {
                                 ARRAY(INT()).notNull())
                         // the recursion composes for a nested array of arrays
                         .testResult(
-                                call("PARSE_JSON", "[[1, 2], 
[3]]").cast(ARRAY(ARRAY(INT()))),
+                                lit("[[1, 2], 
[3]]").parseJson().cast(ARRAY(ARRAY(INT()))),
                                 "CAST(PARSE_JSON('[[1, 2], [3]]') AS 
ARRAY<ARRAY<INT>>)",
                                 new Integer[][] {{1, 2}, {3}},
                                 ARRAY(ARRAY(INT())).notNull())
                         // a top-level VARIANT null casts to SQL NULL for a 
nullable target
                         .testResult(
-                                call("TRY_PARSE_JSON", 
"null").cast(ARRAY(INT())),
+                                lit("null").tryParseJson().cast(ARRAY(INT())),
                                 "CAST(TRY_PARSE_JSON('null') AS ARRAY<INT>)",
                                 null,
                                 ARRAY(INT()))
                         // an element with no variant counterpart is rejected 
at validation
                         .testTableApiValidationError(
-                                call("PARSE_JSON", 
"[1]").cast(ARRAY(INTERVAL(MONTH()))),
+                                
lit("[1]").parseJson().cast(ARRAY(INTERVAL(MONTH()))),
                                 "Unsupported cast"));
     }
 
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 3d2e36ba482..358e5dd0bd0 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
@@ -254,8 +254,11 @@ class JsonFunctionsITCase extends BuiltInFunctionTestBase {
                         INT().nullable())
 
                 // WILDCARDS matching MULTIPLE nodes -> NULL
-                // PARSE_JSON has no Table API equivalent, so this stays 
SQL-only
-                .testSqlResult("JSON_LENGTH(PARSE_JSON(f0), '$.*')", null, 
INT().nullable())
+                .testResult(
+                        $("f0").parseJson().jsonLength("$.*"),
+                        "JSON_LENGTH(PARSE_JSON(f0), '$.*')",
+                        null,
+                        INT().nullable())
                 .testResult(
                         $("f0").jsonLength("$.*"), "JSON_LENGTH(f0, '$.*')", 
null, INT().nullable())
                 .testResult(
@@ -302,12 +305,26 @@ class JsonFunctionsITCase extends BuiltInFunctionTestBase 
{
                         1,
                         INT().nullable())
                 // JSON_LENGTH variant support (runtime path, no constant 
folding)
-                // PARSE_JSON has no Table API equivalent, so these stay 
SQL-only
-                .testSqlResult("JSON_LENGTH(PARSE_JSON(f0))", 3, 
INT().nullable())
-                .testSqlResult("JSON_LENGTH(PARSE_JSON('[1,2,3,4,5]'))", 5, 
INT().nullable())
-                .testSqlResult("JSON_LENGTH(PARSE_JSON('\"hello\"'))", 1, 
INT().nullable())
-                .testSqlResult(
-                        "JSON_LENGTH(PARSE_JSON(f0), '$.metadata.tags')", 3, 
INT().nullable())
+                .testResult(
+                        $("f0").parseJson().jsonLength(),
+                        "JSON_LENGTH(PARSE_JSON(f0))",
+                        3,
+                        INT().nullable())
+                .testResult(
+                        lit("[1,2,3,4,5]").parseJson().jsonLength(),
+                        "JSON_LENGTH(PARSE_JSON('[1,2,3,4,5]'))",
+                        5,
+                        INT().nullable())
+                .testResult(
+                        lit("\"hello\"").parseJson().jsonLength(),
+                        "JSON_LENGTH(PARSE_JSON('\"hello\"'))",
+                        1,
+                        INT().nullable())
+                .testResult(
+                        $("f0").parseJson().jsonLength("$.metadata.tags"),
+                        "JSON_LENGTH(PARSE_JSON(f0), '$.metadata.tags')",
+                        3,
+                        INT().nullable())
                 .testResult(
                         $("f0").jsonLength("$.items[*]"),
                         "JSON_LENGTH(f0, '$.items[*]')",
@@ -994,7 +1011,7 @@ class JsonFunctionsITCase extends BuiltInFunctionTestBase {
                                 "{\"f0\":[{\"f0\":1,\"f1\":2}]}",
                                 STRING().notNull())
                         .testResult(
-                                jsonString(call("PARSE_JSON", $("f14"))),
+                                jsonString($("f14").parseJson()),
                                 
"JSON_STRING(PARSE_JSON('{\"key\":\"value\"}'))",
                                 "{\"key\":\"value\"}",
                                 STRING().notNull()),
@@ -1019,15 +1036,15 @@ class JsonFunctionsITCase extends 
BuiltInFunctionTestBase {
         // The bulk of parsing behavior is covered by 
BinaryVariantInternalBuilderTest.
         return List.of(
                 TestSetSpec.forFunction(BuiltInFunctionDefinitions.PARSE_JSON)
-                        .onFieldsWithData("{\"a\":1,\"b\":[2,3]}", "1e400")
-                        .andDataTypes(STRING().notNull(), STRING().notNull())
+                        .onFieldsWithData("{\"a\":1,\"b\":[2,3]}", "1e400", 
"{\"a\":1,\"a\":2}")
+                        .andDataTypes(STRING().notNull(), STRING().notNull(), 
STRING().notNull())
                         .testResult(
-                                jsonString(call("PARSE_JSON", $("f0"))),
+                                jsonString($("f0").parseJson()),
                                 "JSON_STRING(PARSE_JSON(f0))",
                                 "{\"a\":1,\"b\":[2,3]}",
                                 STRING().notNull())
                         .testResult(
-                                jsonString(call("PARSE_JSON", 
nullOf(STRING()))),
+                                jsonString(nullOf(STRING()).parseJson()),
                                 "JSON_STRING(PARSE_JSON(CAST(NULL AS 
STRING)))",
                                 null,
                                 STRING().nullable())
@@ -1036,21 +1053,48 @@ class JsonFunctionsITCase extends 
BuiltInFunctionTestBase {
                                 TableRuntimeException.class,
                                 "Failed to parse json string")
                         .testTableApiRuntimeError(
-                                call("PARSE_JSON", $("f1")),
+                                $("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")
+                        .testTableApiRuntimeError(
+                                $("f2").parseJson(false),
+                                TableRuntimeException.class,
+                                "Failed to parse json string")
+                        // allowDuplicateKeys: true keeps the last occurrence 
of the duplicated key
+                        .testResult(
+                                jsonString($("f2").parseJson(true)),
+                                "JSON_STRING(PARSE_JSON(f2, true))",
+                                "{\"a\":2}",
+                                STRING().notNull()),
                 
TestSetSpec.forFunction(BuiltInFunctionDefinitions.TRY_PARSE_JSON)
-                        .onFieldsWithData("{\"a\":1}", "1e400")
-                        .andDataTypes(STRING().notNull(), STRING().notNull())
+                        .onFieldsWithData("{\"a\":1}", "1e400", 
"{\"a\":1,\"a\":2}")
+                        .andDataTypes(STRING().notNull(), STRING().notNull(), 
STRING().notNull())
                         .testResult(
-                                jsonString(call("TRY_PARSE_JSON", $("f0"))),
+                                jsonString($("f0").tryParseJson()),
                                 "JSON_STRING(TRY_PARSE_JSON(f0))",
                                 "{\"a\":1}",
                                 STRING())
                         .testResult(
-                                jsonString(call("TRY_PARSE_JSON", $("f1"))),
+                                jsonString($("f1").tryParseJson()),
                                 "JSON_STRING(TRY_PARSE_JSON(f1))",
                                 null,
+                                STRING())
+                        // allowDuplicateKeys: false (the default) yields NULL 
on duplicate keys
+                        .testResult(
+                                jsonString($("f2").tryParseJson(false)),
+                                "JSON_STRING(TRY_PARSE_JSON(f2, false))",
+                                null,
+                                STRING())
+                        // allowDuplicateKeys: true keeps the last occurrence 
of the duplicated key
+                        .testResult(
+                                jsonString($("f2").tryParseJson(true)),
+                                "JSON_STRING(TRY_PARSE_JSON(f2, true))",
+                                "{\"a\":2}",
                                 STRING()),
                 TestSetSpec.forFunction(
                                 BuiltInFunctionDefinitions.PARSE_JSON,
@@ -1061,13 +1105,13 @@ class JsonFunctionsITCase extends 
BuiltInFunctionTestBase {
                         .withConstantFoldingEnabled()
                         .testResult(
                                 resultSpec(
-                                        call("PARSE_JSON", $("f0")),
+                                        $("f0").parseJson(),
                                         "PARSE_JSON(f0)",
                                         getVariantForJson("{\"a\": 1}"),
                                         VARIANT().notNull(),
                                         VARIANT().notNull()),
                                 resultSpec(
-                                        jsonString(call("PARSE_JSON", 
$("f0"))),
+                                        jsonString($("f0").parseJson()),
                                         "JSON_STRING(PARSE_JSON(f0))",
                                         "{\"a\":1}",
                                         STRING().notNull(),

Reply via email to