From 413042b8068966a163c085e9f7f5eebd51689245 Mon Sep 17 00:00:00 2001
From: Matt Blewitt <mble@planetscale.com>
Date: Tue, 10 Mar 2026 21:39:36 +0000
Subject: [PATCH] Fix JSON_SERIALIZE() coercion placeholder type for jsonb
 input

When JSON_SERIALIZE() receives a jsonb-typed argument, the CaseTestExpr
placeholder used to set up coercion was unconditionally assigned JSONOID
(derived from the RETURNING format, which defaults to JS_FORMAT_JSON).
However, the executor passes the input argument value through directly
for JSON_SERIALIZE (see ExecInitExprRec in execExpr.c), so the actual
datum at runtime is jsonb, not json.  This type mismatch between the
placeholder and the runtime value caused the wrong coercion path to be
selected.

Fix by deriving the placeholder type from the actual argument type via
exprType(linitial(args)) when the constructor type is JSCTOR_JSON_SERIALIZE,
rather than from returning->format->format_type.

Add an Assert to guard the assumption that args is non-empty for this
path, and update the block comment to explain why JSON_SERIALIZE differs
from the other constructor types (it consumes json/jsonb rather than
producing it).

Extend the sqljson regression tests with EXPLAIN output, additional
RETURNING variants (varchar, bytea), and error cases (RETURNING int,
RETURNING jsonb) for jsonb-typed input to JSON_SERIALIZE().
---
 src/backend/parser/parse_expr.c       | 18 ++++++++--
 src/test/regress/expected/sqljson.out | 52 +++++++++++++++++++++++++++
 src/test/regress/sql/sqljson.sql      | 13 +++++++
 3 files changed, 80 insertions(+), 3 deletions(-)

diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index dcfe1acc..ba02b117 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -3694,7 +3694,13 @@ makeJsonConstructorExpr(ParseState *pstate, JsonConstructorType type,
 	 * Coerce to the RETURNING type and format, if needed.  We abuse
 	 * CaseTestExpr here as placeholder to pass the result of either
 	 * evaluating 'fexpr' or whatever is produced by ExecEvalJsonConstructor()
-	 * that is of type JSON or JSONB to the coercion function.
+	 * to the coercion function.
+	 *
+	 * For most constructor types the placeholder type is JSON or JSONB,
+	 * determined by the RETURNING format.  JSON_SERIALIZE is different: it
+	 * doesn't produce json/jsonb but rather consumes it, and the executor
+	 * passes the input argument through directly (see execExpr.c), so the
+	 * placeholder must reflect the actual argument type.
 	 */
 	if (fexpr)
 	{
@@ -3710,8 +3716,14 @@ makeJsonConstructorExpr(ParseState *pstate, JsonConstructorType type,
 	{
 		CaseTestExpr *cte = makeNode(CaseTestExpr);
 
-		cte->typeId = returning->format->format_type == JS_FORMAT_JSONB ?
-			JSONBOID : JSONOID;
+		if (type == JSCTOR_JSON_SERIALIZE)
+		{
+			Assert(args != NIL);
+			cte->typeId = exprType(linitial(args));
+		}
+		else
+			cte->typeId = returning->format->format_type == JS_FORMAT_JSONB ?
+				JSONBOID : JSONOID;
 		cte->typeMod = -1;
 		cte->collation = InvalidOid;
 
diff --git a/src/test/regress/expected/sqljson.out b/src/test/regress/expected/sqljson.out
index c7b9e575..e8e6bcf7 100644
--- a/src/test/regress/expected/sqljson.out
+++ b/src/test/regress/expected/sqljson.out
@@ -288,6 +288,58 @@ EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}' RETURNING bytea);
    Output: JSON_SERIALIZE('{}'::json RETURNING bytea)
 (2 rows)
 
+-- JSON_SERIALIZE() with jsonb input
+SELECT JSON_SERIALIZE('[1,2,4,5]'::jsonb);
+ json_serialize 
+----------------
+ [1, 2, 4, 5]
+(1 row)
+
+SELECT JSON_SERIALIZE('{"a": 1}'::jsonb);
+ json_serialize 
+----------------
+ {"a": 1}
+(1 row)
+
+SELECT JSON_SERIALIZE('{"a": 1}'::jsonb RETURNING text);
+ json_serialize 
+----------------
+ {"a": 1}
+(1 row)
+
+SELECT JSON_SERIALIZE('{"a": 1}'::jsonb RETURNING bytea);
+   json_serialize   
+--------------------
+ \x7b2261223a20317d
+(1 row)
+
+SELECT JSON_SERIALIZE('{"a": 1}'::jsonb RETURNING varchar);
+ json_serialize 
+----------------
+ {"a": 1}
+(1 row)
+
+EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{"a": 1}'::jsonb);
+                         QUERY PLAN                         
+------------------------------------------------------------
+ Result
+   Output: JSON_SERIALIZE('{"a": 1}'::jsonb RETURNING text)
+(2 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{"a": 1}'::jsonb RETURNING bytea);
+                         QUERY PLAN                          
+-------------------------------------------------------------
+ Result
+   Output: JSON_SERIALIZE('{"a": 1}'::jsonb RETURNING bytea)
+(2 rows)
+
+-- jsonb input: error cases
+SELECT JSON_SERIALIZE('{"a": 1}'::jsonb RETURNING int);
+ERROR:  cannot use type integer in RETURNING clause of JSON_SERIALIZE()
+HINT:  Try returning a string type or bytea.
+SELECT JSON_SERIALIZE('{"a": 1}'::jsonb RETURNING jsonb);
+ERROR:  cannot use type jsonb in RETURNING clause of JSON_SERIALIZE()
+HINT:  Try returning a string type or bytea.
 -- JSON_OBJECT()
 SELECT JSON_OBJECT();
  json_object 
diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql
index 343d344d..4a45fcdc 100644
--- a/src/test/regress/sql/sqljson.sql
+++ b/src/test/regress/sql/sqljson.sql
@@ -62,6 +62,19 @@ SELECT JSON_SERIALIZE('{ "a" : 1 } ' RETURNING jsonb);
 EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}');
 EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}' RETURNING bytea);
 
+-- JSON_SERIALIZE() with jsonb input
+SELECT JSON_SERIALIZE('[1,2,4,5]'::jsonb);
+SELECT JSON_SERIALIZE('{"a": 1}'::jsonb);
+SELECT JSON_SERIALIZE('{"a": 1}'::jsonb RETURNING text);
+SELECT JSON_SERIALIZE('{"a": 1}'::jsonb RETURNING bytea);
+SELECT JSON_SERIALIZE('{"a": 1}'::jsonb RETURNING varchar);
+EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{"a": 1}'::jsonb);
+EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{"a": 1}'::jsonb RETURNING bytea);
+
+-- jsonb input: error cases
+SELECT JSON_SERIALIZE('{"a": 1}'::jsonb RETURNING int);
+SELECT JSON_SERIALIZE('{"a": 1}'::jsonb RETURNING jsonb);
+
 -- JSON_OBJECT()
 SELECT JSON_OBJECT();
 SELECT JSON_OBJECT(RETURNING json);
-- 
2.52.0

