From ee7db0d84bed0d9608555fc847d485ed51d98006 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Sat, 4 Apr 2026 18:22:33 -0400
Subject: [PATCH v1 3/5] Create jsontypes.c/h and use that to tidy up a few
 things

To avoid a circular dependency with jsonb.h, datum_to_jsonb has
up until now been declared in jsonfuncs.h, because it depends on
JsonTypeCategory, which is also declared in jsonfuncs.h. This is
not great, because it's defined in jsonb.c. The natural way to
get this declaration into jsonb.h would be to make that file
include jsonfuncs.h, but that doesn't work because jsonfuncs.h
already includes jsonb.h.

The best solution seems to be to create a new header file, since over
time jsonfuncs.h has become cluttered with various kinds of unrelated
declarations. So, create jsontypes.h and move the declaration of
JsonTypeCategory there, along with the public functions that depend on
it, and move the corresponding C code to a new file jsontypes.c. This
new header file is then included by both json.h and jsonb.h, allowing
datum_to_json and datum_to_jsonb to be declared in json.h and jsonb.h,
matching where they are defined.

This also paves the way for broader use of JsonTypeCategory, which
would otherwise require increasingly ugly hacks.
---
 src/backend/utils/adt/Makefile    |   1 +
 src/backend/utils/adt/jsonfuncs.c | 221 ---------------------------
 src/backend/utils/adt/jsontypes.c | 244 ++++++++++++++++++++++++++++++
 src/backend/utils/adt/meson.build |   1 +
 src/include/utils/json.h          |   3 +
 src/include/utils/jsonb.h         |   3 +
 src/include/utils/jsonfuncs.h     |  25 ---
 src/include/utils/jsontypes.h     |  41 +++++
 8 files changed, 293 insertions(+), 246 deletions(-)
 create mode 100644 src/backend/utils/adt/jsontypes.c
 create mode 100644 src/include/utils/jsontypes.h

diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 0c7621957c1..5d604850466 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -56,6 +56,7 @@ OBJS = \
 	jsonb_util.o \
 	jsonfuncs.o \
 	jsonbsubs.o \
+	jsontypes.o \
 	jsonpath.o \
 	jsonpath_exec.o \
 	jsonpath_gram.o \
diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c
index c289dbae715..f931f388465 100644
--- a/src/backend/utils/adt/jsonfuncs.c
+++ b/src/backend/utils/adt/jsonfuncs.c
@@ -506,8 +506,6 @@ static JsonParseErrorType transform_string_values_object_field_start(void *state
 static JsonParseErrorType transform_string_values_array_element_start(void *state, bool isnull);
 static JsonParseErrorType transform_string_values_scalar(void *state, char *token, JsonTokenType tokentype);
 
-/* function supporting json_categorize_type/json_check_mutability */
-static Oid get_json_cast_for_type(Oid typoid);
 
 /*
  * pg_parse_json_or_errsave
@@ -5957,222 +5955,3 @@ json_get_first_token(text *json, bool throw_error)
 
 	return JSON_TOKEN_INVALID;	/* invalid json */
 }
-
-/*
- * Determine how we want to print values of a given type in datum_to_json(b).
- *
- * Given the datatype OID, return its JsonTypeCategory, as well as an FmgrInfo
- * for the type's output function or cast function.  For categories that do not
- * require calling a function, outflinfo is not touched.
- */
-void
-json_categorize_type(Oid typoid, bool is_jsonb,
-					 JsonTypeCategory *tcategory, FmgrInfo *outflinfo)
-{
-	bool		use_type_output_function = false;
-
-	/* Look through any domain */
-	typoid = getBaseType(typoid);
-
-	switch (typoid)
-	{
-		case BOOLOID:
-			*tcategory = JSONTYPE_BOOL;
-			break;
-
-		case INT2OID:
-		case INT4OID:
-		case INT8OID:
-		case FLOAT4OID:
-		case FLOAT8OID:
-		case NUMERICOID:
-			use_type_output_function = true;
-			*tcategory = JSONTYPE_NUMERIC;
-			break;
-
-		case DATEOID:
-			*tcategory = JSONTYPE_DATE;
-			break;
-
-		case TIMESTAMPOID:
-			*tcategory = JSONTYPE_TIMESTAMP;
-			break;
-
-		case TIMESTAMPTZOID:
-			*tcategory = JSONTYPE_TIMESTAMPTZ;
-			break;
-
-		case JSONOID:
-			use_type_output_function = !is_jsonb;
-			*tcategory = JSONTYPE_JSON;
-			break;
-
-		case JSONBOID:
-			use_type_output_function = !is_jsonb;
-			*tcategory = is_jsonb ? JSONTYPE_JSONB : JSONTYPE_JSON;
-			break;
-
-		default:
-			/* Check for arrays and composites */
-			if (OidIsValid(get_element_type(typoid)) || typoid == ANYARRAYOID
-				|| typoid == ANYCOMPATIBLEARRAYOID || typoid == RECORDARRAYOID)
-				*tcategory = JSONTYPE_ARRAY;
-			else if (type_is_rowtype(typoid))	/* includes RECORDOID */
-				*tcategory = JSONTYPE_COMPOSITE;
-			else
-			{
-				Oid			castfunc = get_json_cast_for_type(typoid);
-
-				if (OidIsValid(castfunc))
-				{
-					fmgr_info(castfunc, outflinfo);
-					*tcategory = JSONTYPE_CAST;
-				}
-				else
-				{
-					use_type_output_function = true;
-					*tcategory = JSONTYPE_OTHER;
-				}
-			}
-			break;
-	}
-
-	if (use_type_output_function)
-	{
-		Oid			typoutput;
-		bool		typisvarlena;
-
-		getTypeOutputInfo(typoid, &typoutput, &typisvarlena);
-		fmgr_info(typoutput, outflinfo);
-	}
-}
-
-/*
- * Check whether a type conversion to JSON or JSONB involves any mutable
- * functions.  This recurses into container types (arrays, composites,
- * ranges, multiranges, domains) to check their element/sub types.
- *
- * The caller must initialize *has_mutable to false before calling.
- * If any mutable function is found, *has_mutable is set to true.
- */
-void
-json_check_mutability(Oid typoid, bool *has_mutable)
-{
-	char		att_typtype = get_typtype(typoid);
-
-	/* since this function recurses, it could be driven to stack overflow */
-	check_stack_depth();
-
-	Assert(has_mutable != NULL);
-
-	if (*has_mutable)
-		return;
-
-	if (att_typtype == TYPTYPE_DOMAIN)
-	{
-		json_check_mutability(getBaseType(typoid), has_mutable);
-		return;
-	}
-	else if (att_typtype == TYPTYPE_COMPOSITE)
-	{
-		/*
-		 * For a composite type, recurse into its attributes.  Use the
-		 * typcache to avoid opening the relation directly.
-		 */
-		TupleDesc	tupdesc = lookup_rowtype_tupdesc(typoid, -1);
-
-		for (int i = 0; i < tupdesc->natts; i++)
-		{
-			Form_pg_attribute attr = TupleDescAttr(tupdesc, i);
-
-			if (attr->attisdropped)
-				continue;
-
-			json_check_mutability(attr->atttypid, has_mutable);
-			if (*has_mutable)
-				break;
-		}
-		ReleaseTupleDesc(tupdesc);
-		return;
-	}
-	else if (att_typtype == TYPTYPE_RANGE)
-	{
-		json_check_mutability(get_range_subtype(typoid), has_mutable);
-		return;
-	}
-	else if (att_typtype == TYPTYPE_MULTIRANGE)
-	{
-		json_check_mutability(get_multirange_range(typoid), has_mutable);
-		return;
-	}
-	else
-	{
-		Oid			att_typelem = get_element_type(typoid);
-
-		if (OidIsValid(att_typelem))
-		{
-			/* recurse into array element type */
-			json_check_mutability(att_typelem, has_mutable);
-			return;
-		}
-	}
-
-	switch (typoid)
-	{
-		case BOOLOID:
-		case INT2OID:
-		case INT4OID:
-		case INT8OID:
-		case FLOAT4OID:
-		case FLOAT8OID:
-		case NUMERICOID:
-		case JSONOID:
-		case JSONBOID:
-			/* known immutable */
-			break;
-		case DATEOID:
-		case TIMESTAMPOID:
-		case TIMESTAMPTZOID:
-			*has_mutable = true;
-			break;
-		default:
-			{
-				Oid			castfunc = get_json_cast_for_type(typoid);
-				Oid			funcoid;
-
-				if (OidIsValid(castfunc))
-					funcoid = castfunc;
-				else
-				{
-					bool		typisvarlena;
-
-					getTypeOutputInfo(typoid, &funcoid, &typisvarlena);
-				}
-				if (func_volatile(funcoid) != PROVOLATILE_IMMUTABLE)
-					*has_mutable = true;
-			}
-			break;
-	}
-}
-
-/*
- * Return the OID of a cast function from typoid to JSON, or InvalidOid if
- * there is no such cast.  As a matter of policy, we only consider explicit,
- * user-defined casts.
- */
-static Oid
-get_json_cast_for_type(Oid typoid)
-{
-	if (typoid >= FirstNormalObjectId)
-	{
-		Oid			castfunc;
-		CoercionPathType ctype;
-
-		ctype = find_coercion_pathway(JSONOID, typoid,
-									  COERCION_EXPLICIT,
-									  &castfunc);
-		if (ctype == COERCION_PATH_FUNC && OidIsValid(castfunc))
-			return castfunc;
-	}
-	return InvalidOid;
-}
diff --git a/src/backend/utils/adt/jsontypes.c b/src/backend/utils/adt/jsontypes.c
new file mode 100644
index 00000000000..adbaceebc8a
--- /dev/null
+++ b/src/backend/utils/adt/jsontypes.c
@@ -0,0 +1,244 @@
+/*-------------------------------------------------------------------------
+ *
+ * jsontypes.c
+ *	  Functions for JSON type categorization.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/jsontypes.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/transam.h"
+#include "catalog/pg_proc.h"
+#include "catalog/pg_type.h"
+#include "miscadmin.h"
+#include "parser/parse_coerce.h"
+#include "utils/jsontypes.h"
+#include "utils/lsyscache.h"
+#include "utils/typcache.h"
+
+static Oid	get_json_cast_for_type(Oid typoid);
+
+/*
+ * Determine how we want to print values of a given type in datum_to_json(b).
+ *
+ * Given the datatype OID, return its JsonTypeCategory, as well as an FmgrInfo
+ * for the type's output function or cast function.  For categories that do not
+ * require calling a function, outflinfo is not touched.
+ */
+void
+json_categorize_type(Oid typoid, bool is_jsonb,
+					 JsonTypeCategory *tcategory, FmgrInfo *outflinfo)
+{
+	bool		use_type_output_function = false;
+
+	/* Look through any domain */
+	typoid = getBaseType(typoid);
+
+	switch (typoid)
+	{
+		case BOOLOID:
+			*tcategory = JSONTYPE_BOOL;
+			break;
+
+		case INT2OID:
+		case INT4OID:
+		case INT8OID:
+		case FLOAT4OID:
+		case FLOAT8OID:
+		case NUMERICOID:
+			use_type_output_function = true;
+			*tcategory = JSONTYPE_NUMERIC;
+			break;
+
+		case DATEOID:
+			*tcategory = JSONTYPE_DATE;
+			break;
+
+		case TIMESTAMPOID:
+			*tcategory = JSONTYPE_TIMESTAMP;
+			break;
+
+		case TIMESTAMPTZOID:
+			*tcategory = JSONTYPE_TIMESTAMPTZ;
+			break;
+
+		case JSONOID:
+			use_type_output_function = !is_jsonb;
+			*tcategory = JSONTYPE_JSON;
+			break;
+
+		case JSONBOID:
+			use_type_output_function = !is_jsonb;
+			*tcategory = is_jsonb ? JSONTYPE_JSONB : JSONTYPE_JSON;
+			break;
+
+		default:
+			/* Check for arrays and composites */
+			if (OidIsValid(get_element_type(typoid)) || typoid == ANYARRAYOID
+				|| typoid == ANYCOMPATIBLEARRAYOID || typoid == RECORDARRAYOID)
+				*tcategory = JSONTYPE_ARRAY;
+			else if (type_is_rowtype(typoid))	/* includes RECORDOID */
+				*tcategory = JSONTYPE_COMPOSITE;
+			else
+			{
+				Oid			castfunc = get_json_cast_for_type(typoid);
+
+				if (OidIsValid(castfunc))
+				{
+					fmgr_info(castfunc, outflinfo);
+					*tcategory = JSONTYPE_CAST;
+				}
+				else
+				{
+					use_type_output_function = true;
+					*tcategory = JSONTYPE_OTHER;
+				}
+			}
+			break;
+	}
+
+	if (use_type_output_function)
+	{
+		Oid			typoutput;
+		bool		typisvarlena;
+
+		getTypeOutputInfo(typoid, &typoutput, &typisvarlena);
+		fmgr_info(typoutput, outflinfo);
+	}
+}
+
+/*
+ * Check whether a type conversion to JSON or JSONB involves any mutable
+ * functions.  This recurses into container types (arrays, composites,
+ * ranges, multiranges, domains) to check their element/sub types.
+ *
+ * The caller must initialize *has_mutable to false before calling.
+ * If any mutable function is found, *has_mutable is set to true.
+ */
+void
+json_check_mutability(Oid typoid, bool *has_mutable)
+{
+	char		att_typtype = get_typtype(typoid);
+
+	/* since this function recurses, it could be driven to stack overflow */
+	check_stack_depth();
+
+	Assert(has_mutable != NULL);
+
+	if (*has_mutable)
+		return;
+
+	if (att_typtype == TYPTYPE_DOMAIN)
+	{
+		json_check_mutability(getBaseType(typoid), has_mutable);
+		return;
+	}
+	else if (att_typtype == TYPTYPE_COMPOSITE)
+	{
+		/*
+		 * For a composite type, recurse into its attributes.  Use the
+		 * typcache to avoid opening the relation directly.
+		 */
+		TupleDesc	tupdesc = lookup_rowtype_tupdesc(typoid, -1);
+
+		for (int i = 0; i < tupdesc->natts; i++)
+		{
+			Form_pg_attribute attr = TupleDescAttr(tupdesc, i);
+
+			if (attr->attisdropped)
+				continue;
+
+			json_check_mutability(attr->atttypid, has_mutable);
+			if (*has_mutable)
+				break;
+		}
+		ReleaseTupleDesc(tupdesc);
+		return;
+	}
+	else if (att_typtype == TYPTYPE_RANGE)
+	{
+		json_check_mutability(get_range_subtype(typoid), has_mutable);
+		return;
+	}
+	else if (att_typtype == TYPTYPE_MULTIRANGE)
+	{
+		json_check_mutability(get_multirange_range(typoid), has_mutable);
+		return;
+	}
+	else
+	{
+		Oid			att_typelem = get_element_type(typoid);
+
+		if (OidIsValid(att_typelem))
+		{
+			/* recurse into array element type */
+			json_check_mutability(att_typelem, has_mutable);
+			return;
+		}
+	}
+
+	switch (typoid)
+	{
+		case BOOLOID:
+		case INT2OID:
+		case INT4OID:
+		case INT8OID:
+		case FLOAT4OID:
+		case FLOAT8OID:
+		case NUMERICOID:
+		case JSONOID:
+		case JSONBOID:
+			/* known immutable */
+			break;
+		case DATEOID:
+		case TIMESTAMPOID:
+		case TIMESTAMPTZOID:
+			*has_mutable = true;
+			break;
+		default:
+			{
+				Oid			castfunc = get_json_cast_for_type(typoid);
+				Oid			funcoid;
+
+				if (OidIsValid(castfunc))
+					funcoid = castfunc;
+				else
+				{
+					bool		typisvarlena;
+
+					getTypeOutputInfo(typoid, &funcoid, &typisvarlena);
+				}
+				if (func_volatile(funcoid) != PROVOLATILE_IMMUTABLE)
+					*has_mutable = true;
+			}
+			break;
+	}
+}
+
+/*
+ * Return the OID of a cast function from typoid to JSON, or InvalidOid if
+ * there is no such cast.  As a matter of policy, we only consider explicit,
+ * user-defined casts.
+ */
+static Oid
+get_json_cast_for_type(Oid typoid)
+{
+	if (typoid >= FirstNormalObjectId)
+	{
+		Oid			castfunc;
+		CoercionPathType ctype;
+
+		ctype = find_coercion_pathway(JSONOID, typoid,
+									  COERCION_EXPLICIT,
+									  &castfunc);
+		if (ctype == COERCION_PATH_FUNC && OidIsValid(castfunc))
+			return castfunc;
+	}
+	return InvalidOid;
+}
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index d793f8145f6..be8411c66f2 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -55,6 +55,7 @@ backend_sources += files(
   'jsonb_util.c',
   'jsonbsubs.c',
   'jsonfuncs.c',
+  'jsontypes.c',
   'jsonpath.c',
   'jsonpath_exec.c',
   'like.c',
diff --git a/src/include/utils/json.h b/src/include/utils/json.h
index 2f4be40518d..31f88fbbff6 100644
--- a/src/include/utils/json.h
+++ b/src/include/utils/json.h
@@ -15,8 +15,11 @@
 #define JSON_H
 
 #include "lib/stringinfo.h"
+#include "utils/jsontypes.h"
 
 /* functions in json.c */
+extern Datum datum_to_json(Datum val, JsonTypeCategory tcategory,
+						   FmgrInfo *outflinfo);
 extern void composite_to_json(Datum composite, StringInfo result,
 							  bool use_line_feeds);
 extern void escape_json(StringInfo buf, const char *str);
diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h
index ca13efba0fb..67e4675a908 100644
--- a/src/include/utils/jsonb.h
+++ b/src/include/utils/jsonb.h
@@ -14,6 +14,7 @@
 
 #include "lib/stringinfo.h"
 #include "utils/array.h"
+#include "utils/jsontypes.h"
 #include "utils/numeric.h"
 
 /* Tokens used when sequentially processing a jsonb value */
@@ -457,6 +458,8 @@ extern Datum jsonb_set_element(Jsonb *jb, const Datum *path, int path_len,
 							   JsonbValue *newval);
 extern Datum jsonb_get_element(Jsonb *jb, const Datum *path, int npath,
 							   bool *isnull, bool as_text);
+extern Datum datum_to_jsonb(Datum val, JsonTypeCategory tcategory,
+							FmgrInfo *outflinfo);
 extern bool to_jsonb_is_immutable(Oid typoid);
 extern Datum jsonb_build_object_worker(int nargs, const Datum *args, const bool *nulls,
 									   const Oid *types, bool absent_on_null,
diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h
index 96409557f29..8db155d6d23 100644
--- a/src/include/utils/jsonfuncs.h
+++ b/src/include/utils/jsonfuncs.h
@@ -64,31 +64,6 @@ extern Jsonb *transform_jsonb_string_values(Jsonb *jsonb, void *action_state,
 extern text *transform_json_string_values(text *json, void *action_state,
 										  JsonTransformStringValuesAction transform_action);
 
-/* Type categories returned by json_categorize_type */
-typedef enum
-{
-	JSONTYPE_NULL,				/* null, so we didn't bother to identify */
-	JSONTYPE_BOOL,				/* boolean (built-in types only) */
-	JSONTYPE_NUMERIC,			/* numeric (ditto) */
-	JSONTYPE_DATE,				/* we use special formatting for datetimes */
-	JSONTYPE_TIMESTAMP,
-	JSONTYPE_TIMESTAMPTZ,
-	JSONTYPE_JSON,				/* JSON (and JSONB, if not is_jsonb) */
-	JSONTYPE_JSONB,				/* JSONB (if is_jsonb) */
-	JSONTYPE_ARRAY,				/* array */
-	JSONTYPE_COMPOSITE,			/* composite */
-	JSONTYPE_CAST,				/* something with an explicit cast to JSON */
-	JSONTYPE_OTHER,				/* all else */
-} JsonTypeCategory;
-
-extern void json_categorize_type(Oid typoid, bool is_jsonb,
-								 JsonTypeCategory *tcategory,
-								 FmgrInfo *outflinfo);
-extern void json_check_mutability(Oid typoid, bool *has_mutable);
-extern Datum datum_to_json(Datum val, JsonTypeCategory tcategory,
-						   FmgrInfo *outflinfo);
-extern Datum datum_to_jsonb(Datum val, JsonTypeCategory tcategory,
-							FmgrInfo *outflinfo);
 extern Datum jsonb_from_text(text *js, bool unique_keys);
 
 extern Datum json_populate_type(Datum json_val, Oid json_type,
diff --git a/src/include/utils/jsontypes.h b/src/include/utils/jsontypes.h
new file mode 100644
index 00000000000..4fe3911f69c
--- /dev/null
+++ b/src/include/utils/jsontypes.h
@@ -0,0 +1,41 @@
+/*-------------------------------------------------------------------------
+ *
+ * jsontypes.h
+ *	  Declarations for JSON type categorization.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/jsontypes.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef JSONTYPES_H
+#define JSONTYPES_H
+
+#include "fmgr.h"
+
+/* Type categories returned by json_categorize_type */
+typedef enum
+{
+	JSONTYPE_NULL,				/* null, so we didn't bother to identify */
+	JSONTYPE_BOOL,				/* boolean (built-in types only) */
+	JSONTYPE_NUMERIC,			/* numeric (ditto) */
+	JSONTYPE_DATE,				/* we use special formatting for datetimes */
+	JSONTYPE_TIMESTAMP,
+	JSONTYPE_TIMESTAMPTZ,
+	JSONTYPE_JSON,				/* JSON (and JSONB, if not is_jsonb) */
+	JSONTYPE_JSONB,				/* JSONB (if is_jsonb) */
+	JSONTYPE_ARRAY,				/* array */
+	JSONTYPE_COMPOSITE,			/* composite */
+	JSONTYPE_CAST,				/* something with an explicit cast to JSON */
+	JSONTYPE_OTHER,				/* all else */
+} JsonTypeCategory;
+
+extern void json_categorize_type(Oid typoid, bool is_jsonb,
+								 JsonTypeCategory *tcategory,
+								 FmgrInfo *outflinfo);
+extern void json_check_mutability(Oid typoid, bool *has_mutable);
+
+#endif							/* JSONTYPES_H */
-- 
2.50.1 (Apple Git-155)

