From 2f06c7daafffae594eac879fb92cbe74205b69d0 Mon Sep 17 00:00:00 2001
From: Zsolt Parragi <zsolt.parragi@percona.com>
Date: Wed, 8 Jul 2026 21:39:14 +0000
Subject: [PATCH 1/8] Add JSON5 comment support to the JSON lexer

Introduce a json5 flag on JsonLexContext (plus a param on both
makeJsonLexContext constructors, all existing callers pass false) and,
when set, skip // line and /* */ block comments wherever whitespace is
allowed, including across incremental chunk boundaries. json5=false is
byte-for-byte unchanged.
---
 src/backend/utils/adt/json.c                  |   2 +-
 src/backend/utils/adt/jsonb.c                 |   2 +-
 src/backend/utils/adt/jsonfuncs.c             |   7 +-
 src/backend/utils/adt/pg_dependencies.c       |   2 +-
 src/backend/utils/adt/pg_ndistinct.c          |   2 +-
 src/common/jsonapi.c                          | 163 +++++++++++++++++-
 src/common/parse_manifest.c                   |   4 +-
 src/include/common/jsonapi.h                  |   8 +-
 src/interfaces/libpq-oauth/oauth-curl.c       |   2 +-
 src/interfaces/libpq/fe-auth-oauth.c          |   2 +-
 src/test/modules/test_escape/test_escape.c    |   2 +-
 src/test/modules/test_json_parser/README      |   4 +-
 .../test_json_parser/json5_comments.json5     |  12 ++
 .../test_json_parser/json5_comments.out       |  10 ++
 src/test/modules/test_json_parser/meson.build |   3 +-
 .../test_json_parser/t/005_test_json5.pl      |  99 +++++++++++
 .../test_json_parser_incremental.c            |  16 +-
 .../test_json_parser/test_json_parser_perf.c  |   4 +-
 src/tools/pgindent/typedefs.list              |   1 +
 19 files changed, 316 insertions(+), 29 deletions(-)
 create mode 100644 src/test/modules/test_json_parser/json5_comments.json5
 create mode 100644 src/test/modules/test_json_parser/json5_comments.out
 create mode 100644 src/test/modules/test_json_parser/t/005_test_json5.pl

diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c
index 0fee1b40d63..94040c5e03e 100644
--- a/src/backend/utils/adt/json.c
+++ b/src/backend/utils/adt/json.c
@@ -159,7 +159,7 @@ json_recv(PG_FUNCTION_ARGS)
 
 	/* Validate it. */
 	makeJsonLexContextCstringLen(&lex, str, nbytes, GetDatabaseEncoding(),
-								 false);
+								 false, false);
 	pg_parse_json_or_ereport(&lex, &nullSemAction);
 
 	PG_RETURN_TEXT_P(cstring_to_text_with_len(str, nbytes));
diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c
index 864c5ac1c85..1c97ede567a 100644
--- a/src/backend/utils/adt/jsonb.c
+++ b/src/backend/utils/adt/jsonb.c
@@ -245,7 +245,7 @@ jsonb_from_cstring(char *json, int len, bool unique_keys, Node *escontext)
 
 	memset(&state, 0, sizeof(state));
 	memset(&sem, 0, sizeof(sem));
-	makeJsonLexContextCstringLen(&lex, json, len, GetDatabaseEncoding(), true);
+	makeJsonLexContextCstringLen(&lex, json, len, GetDatabaseEncoding(), true, false);
 
 	state.unique_keys = unique_keys;
 	state.escontext = escontext;
diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c
index 97cc3d60340..08aec32c0d6 100644
--- a/src/backend/utils/adt/jsonfuncs.c
+++ b/src/backend/utils/adt/jsonfuncs.c
@@ -552,7 +552,8 @@ makeJsonLexContext(JsonLexContext *lex, text *json, bool need_escapes)
 										VARDATA_ANY(json),
 										VARSIZE_ANY_EXHDR(json),
 										GetDatabaseEncoding(),
-										need_escapes);
+										need_escapes,
+										false);
 }
 
 /*
@@ -2792,7 +2793,7 @@ populate_array_json(PopulateArrayContext *ctx, const char *json, int len)
 	JsonSemAction sem;
 
 	state.lex = makeJsonLexContextCstringLen(NULL, json, len,
-											 GetDatabaseEncoding(), true);
+											 GetDatabaseEncoding(), true, false);
 	state.ctx = ctx;
 
 	memset(&sem, 0, sizeof(sem));
@@ -3830,7 +3831,7 @@ get_json_object_as_hash(const char *json, int len, const char *funcname,
 	state->function_name = funcname;
 	state->hash = tab;
 	state->lex = makeJsonLexContextCstringLen(NULL, json, len,
-											  GetDatabaseEncoding(), true);
+											  GetDatabaseEncoding(), true, false);
 
 	sem->semstate = state;
 	sem->array_start = hash_array_start;
diff --git a/src/backend/utils/adt/pg_dependencies.c b/src/backend/utils/adt/pg_dependencies.c
index 9c3db103d42..b5049d6b4b6 100644
--- a/src/backend/utils/adt/pg_dependencies.c
+++ b/src/backend/utils/adt/pg_dependencies.c
@@ -778,7 +778,7 @@ pg_dependencies_in(PG_FUNCTION_ARGS)
 	sem_action.object_field_end = NULL;
 	sem_action.scalar = dependencies_scalar;
 
-	lex = makeJsonLexContextCstringLen(NULL, str, strlen(str), PG_UTF8, true);
+	lex = makeJsonLexContextCstringLen(NULL, str, strlen(str), PG_UTF8, true, false);
 
 	result = pg_parse_json(lex, &sem_action);
 	freeJsonLexContext(lex);
diff --git a/src/backend/utils/adt/pg_ndistinct.c b/src/backend/utils/adt/pg_ndistinct.c
index 8d854012d6e..5d28dd48a3a 100644
--- a/src/backend/utils/adt/pg_ndistinct.c
+++ b/src/backend/utils/adt/pg_ndistinct.c
@@ -756,7 +756,7 @@ pg_ndistinct_in(PG_FUNCTION_ARGS)
 	sem_action.scalar = ndistinct_scalar;
 
 	lex = makeJsonLexContextCstringLen(NULL, str, strlen(str),
-									   PG_UTF8, true);
+									   PG_UTF8, true, false);
 	result = pg_parse_json(lex, &sem_action);
 	freeJsonLexContext(lex);
 
diff --git a/src/common/jsonapi.c b/src/common/jsonapi.c
index d3860197dad..4ca70c5fea9 100644
--- a/src/common/jsonapi.c
+++ b/src/common/jsonapi.c
@@ -153,6 +153,16 @@ struct JsonParserStack
 	char	   *scalar_val;
 };
 
+/* json5 comment lexing state saved across incremental chunks */
+typedef enum Json5CommentState
+{
+	JSON5_COMMENT_NONE = 0,
+	JSON5_COMMENT_SLASH,		/* saw '/', comment kind not known yet */
+	JSON5_COMMENT_LINE,			/* inside a // comment */
+	JSON5_COMMENT_BLOCK,		/* inside a block comment */
+	JSON5_COMMENT_BLOCK_STAR,	/* inside a block comment, last byte was '*' */
+} Json5CommentState;
+
 /*
  * struct containing state used when there is a possible partial token at the
  * end of a json chunk when we are doing incremental parsing.
@@ -165,6 +175,8 @@ struct JsonIncrementalState
 	bool		is_last_chunk;
 	bool		partial_completed;
 	jsonapi_StrValType partial_token;
+	/* json5 comment resume state */
+	Json5CommentState comment_state;
 };
 
 /*
@@ -390,7 +402,8 @@ IsValidJsonNumber(const char *str, size_t len)
  */
 JsonLexContext *
 makeJsonLexContextCstringLen(JsonLexContext *lex, const char *json,
-							 size_t len, int encoding, bool need_escapes)
+							 size_t len, int encoding, bool need_escapes,
+							 bool json5)
 {
 	if (lex == NULL)
 	{
@@ -408,6 +421,7 @@ makeJsonLexContextCstringLen(JsonLexContext *lex, const char *json,
 	lex->input_length = len;
 	lex->input_encoding = encoding;
 	lex->need_escapes = need_escapes;
+	lex->json5 = json5;
 	if (need_escapes)
 	{
 		/*
@@ -496,7 +510,7 @@ allocate_incremental_state(JsonLexContext *lex)
  */
 JsonLexContext *
 makeJsonLexContextIncremental(JsonLexContext *lex, int encoding,
-							  bool need_escapes)
+							  bool need_escapes, bool json5)
 {
 	if (lex == NULL)
 	{
@@ -525,6 +539,7 @@ makeJsonLexContextIncremental(JsonLexContext *lex, int encoding,
 	}
 
 	lex->need_escapes = need_escapes;
+	lex->json5 = json5;
 	if (need_escapes)
 	{
 		/*
@@ -1809,6 +1824,7 @@ json_lex(JsonLexContext *lex)
 		dummy_lex.input_encoding = lex->input_encoding;
 		dummy_lex.incremental = false;
 		dummy_lex.need_escapes = lex->need_escapes;
+		dummy_lex.json5 = lex->json5;
 		dummy_lex.strval = lex->strval;
 
 		partial_result = json_lex(&dummy_lex);
@@ -1849,15 +1865,146 @@ json_lex(JsonLexContext *lex)
 		/* end of partial token processing */
 	}
 
-	/* Skip leading whitespace. */
-	while (s < end && (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r'))
+	/*
+	 * Skip whitespace and, in json5 mode, comments.  Comments count as
+	 * whitespace, so this alternates between the two until neither matches.
+	 * At an incremental chunk boundary the comment state is saved in
+	 * inc_state and picked up again here, since the bytes deciding comment
+	 * kind and termination can open the next chunk.
+	 */
+	for (;;)
 	{
-		if (*s++ == '\n')
+		Json5CommentState cstate = JSON5_COMMENT_NONE;
+
+		if (lex->json5 && lex->incremental)
 		{
-			++lex->line_number;
-			lex->line_start = s;
+			cstate = lex->inc_state->comment_state;
+			lex->inc_state->comment_state = JSON5_COMMENT_NONE;
+		}
+
+		if (cstate == JSON5_COMMENT_NONE)
+		{
+			/* Skip leading whitespace. */
+			while (s < end && (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r'))
+			{
+				if (*s++ == '\n')
+				{
+					++lex->line_number;
+					lex->line_start = s;
+				}
+			}
+
+			if (!lex->json5 || s >= end || *s != '/')
+				break;
+
+			if (s + 1 >= end)
+			{
+				/*
+				 * A lone '/' at the end of the chunk might start a comment;
+				 * ask for more input in incremental mode.  Otherwise fall
+				 * out and report the '/' as an invalid token.
+				 */
+				if (lex->incremental && !lex->inc_state->is_last_chunk)
+				{
+					lex->inc_state->comment_state = JSON5_COMMENT_SLASH;
+					return JSON_INCOMPLETE;
+				}
+				break;
+			}
+			if (*(s + 1) == '/')
+			{
+				s += 2;
+				cstate = JSON5_COMMENT_LINE;
+			}
+			else if (*(s + 1) == '*')
+			{
+				s += 2;
+				cstate = JSON5_COMMENT_BLOCK;
+			}
+			else
+				break;			/* '/' not a comment: invalid token */
+		}
+		else if (cstate == JSON5_COMMENT_SLASH)
+		{
+			/* '/' seen at the end of the previous chunk */
+			if (s < end && *s == '/')
+			{
+				s++;
+				cstate = JSON5_COMMENT_LINE;
+			}
+			else if (s < end && *s == '*')
+			{
+				s++;
+				cstate = JSON5_COMMENT_BLOCK;
+			}
+			else
+			{
+				if (s >= end && !lex->inc_state->is_last_chunk)
+				{
+					lex->inc_state->comment_state = JSON5_COMMENT_SLASH;
+					return JSON_INCOMPLETE;
+				}
+				lex->token_start = s;
+				lex->prev_token_terminator = lex->token_terminator;
+				lex->token_terminator = s;
+				return JSON_INVALID_TOKEN;
+			}
+		}
+
+		if (cstate == JSON5_COMMENT_LINE)
+		{
+			/* json5 line terminators are LF and CR */
+			while (s < end && *s != '\n' && *s != '\r')
+				s++;
+			if (s >= end && lex->incremental && !lex->inc_state->is_last_chunk)
+			{
+				lex->inc_state->comment_state = JSON5_COMMENT_LINE;
+				return JSON_INCOMPLETE;
+			}
+
+			/*
+			 * The terminator, if any, is consumed as whitespace on the next
+			 * round; at end of input the comment is implicitly closed.
+			 */
+		}
+		else
+		{
+			/* block comment body; star tracks a possible closing '*' */
+			bool		star = (cstate == JSON5_COMMENT_BLOCK_STAR);
+			bool		closed = false;
+
+			while (s < end)
+			{
+				char		c = *s++;
+
+				if (star && c == '/')
+				{
+					closed = true;
+					break;
+				}
+				star = (c == '*');
+				if (c == '\n')
+				{
+					++lex->line_number;
+					lex->line_start = s;
+				}
+			}
+			if (!closed)
+			{
+				if (lex->incremental && !lex->inc_state->is_last_chunk)
+				{
+					lex->inc_state->comment_state =
+						star ? JSON5_COMMENT_BLOCK_STAR : JSON5_COMMENT_BLOCK;
+					return JSON_INCOMPLETE;
+				}
+				lex->token_start = s;
+				lex->prev_token_terminator = lex->token_terminator;
+				lex->token_terminator = s;
+				return JSON_UNTERMINATED_COMMENT;
+			}
 		}
 	}
+
 	lex->token_start = s;
 
 	/* Determine token type. */
@@ -2551,6 +2698,8 @@ json_errdetail(JsonParseErrorType error, JsonLexContext *lex)
 			return _("Unicode high surrogate must not follow a high surrogate.");
 		case JSON_UNICODE_LOW_SURROGATE:
 			return _("Unicode low surrogate must follow a high surrogate.");
+		case JSON_UNTERMINATED_COMMENT:
+			return _("Block comment is not terminated.");
 		case JSON_SEM_ACTION_FAILED:
 			/* fall through to the error code after switch */
 			break;
diff --git a/src/common/parse_manifest.c b/src/common/parse_manifest.c
index 5065c9bf39c..23ae27dfa98 100644
--- a/src/common/parse_manifest.c
+++ b/src/common/parse_manifest.c
@@ -139,7 +139,7 @@ json_parse_manifest_incremental_init(JsonManifestParseContext *context)
 	parse->state = JM_EXPECT_TOPLEVEL_START;
 	parse->saw_version_field = false;
 
-	makeJsonLexContextIncremental(&(incstate->lex), PG_UTF8, true);
+	makeJsonLexContextIncremental(&(incstate->lex), PG_UTF8, true, false);
 
 	incstate->sem.semstate = parse;
 	incstate->sem.object_start = json_manifest_object_start;
@@ -238,7 +238,7 @@ json_parse_manifest(JsonManifestParseContext *context, const char *buffer,
 	parse.saw_version_field = false;
 
 	/* Create a JSON lexing context. */
-	lex = makeJsonLexContextCstringLen(NULL, buffer, size, PG_UTF8, true);
+	lex = makeJsonLexContextCstringLen(NULL, buffer, size, PG_UTF8, true, false);
 
 	/* Set up semantic actions. */
 	sem.semstate = &parse;
diff --git a/src/include/common/jsonapi.h b/src/include/common/jsonapi.h
index 85cc9a11d97..8b4d8d818c7 100644
--- a/src/include/common/jsonapi.h
+++ b/src/include/common/jsonapi.h
@@ -56,6 +56,7 @@ typedef enum JsonParseErrorType
 	JSON_UNICODE_UNTRANSLATABLE,
 	JSON_UNICODE_HIGH_SURROGATE,
 	JSON_UNICODE_LOW_SURROGATE,
+	JSON_UNTERMINATED_COMMENT,
 	JSON_SEM_ACTION_FAILED,		/* error should already be reported */
 } JsonParseErrorType;
 
@@ -106,6 +107,7 @@ typedef struct JsonLexContext
 	const char *token_terminator;
 	const char *prev_token_terminator;
 	bool		incremental;
+	bool		json5;
 	JsonTokenType token_type;
 	int			lex_level;
 	uint32		flags;
@@ -219,7 +221,8 @@ extern JsonLexContext *makeJsonLexContextCstringLen(JsonLexContext *lex,
 													const char *json,
 													size_t len,
 													int encoding,
-													bool need_escapes);
+													bool need_escapes,
+													bool json5);
 
 /*
  * make a JsonLexContext suitable for incremental parsing.
@@ -228,7 +231,8 @@ extern JsonLexContext *makeJsonLexContextCstringLen(JsonLexContext *lex,
  */
 extern JsonLexContext *makeJsonLexContextIncremental(JsonLexContext *lex,
 													 int encoding,
-													 bool need_escapes);
+													 bool need_escapes,
+													 bool json5);
 
 /*
  * Sets whether tokens passed to semantic action callbacks are owned by the
diff --git a/src/interfaces/libpq-oauth/oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c
index d4dcc4cd7a5..941a8d89000 100644
--- a/src/interfaces/libpq-oauth/oauth-curl.c
+++ b/src/interfaces/libpq-oauth/oauth-curl.c
@@ -896,7 +896,7 @@ parse_oauth_json(struct async_ctx *actx, const struct json_field *fields)
 		return false;
 	}
 
-	makeJsonLexContextCstringLen(&lex, resp->data, resp->len, PG_UTF8, true);
+	makeJsonLexContextCstringLen(&lex, resp->data, resp->len, PG_UTF8, true, false);
 	setJsonLexContextOwnsTokens(&lex, true);	/* must not leak on error */
 
 	ctx.errbuf = &actx->errbuf;
diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c
index 826f7461cb3..c8cc0f0cf70 100644
--- a/src/interfaces/libpq/fe-auth-oauth.c
+++ b/src/interfaces/libpq/fe-auth-oauth.c
@@ -547,7 +547,7 @@ handle_oauth_sasl_error(PGconn *conn, const char *msg, int msglen)
 		return false;
 	}
 
-	lex = makeJsonLexContextCstringLen(NULL, msg, msglen, PG_UTF8, true);
+	lex = makeJsonLexContextCstringLen(NULL, msg, msglen, PG_UTF8, true, false);
 	setJsonLexContextOwnsTokens(lex, true); /* must not leak on error */
 
 	initPQExpBuffer(&ctx.errbuf);
diff --git a/src/test/modules/test_escape/test_escape.c b/src/test/modules/test_escape/test_escape.c
index 6234a9bd129..7da1241a8ac 100644
--- a/src/test/modules/test_escape/test_escape.c
+++ b/src/test/modules/test_escape/test_escape.c
@@ -235,7 +235,7 @@ test_gb18030_json(pe_test_config *tc)
 
 	/* test itself */
 	lex = makeJsonLexContextCstringLen(NULL, raw_buf->data, input_len,
-									   PG_GB18030, false);
+									   PG_GB18030, false, false);
 	json_error = pg_parse_json(lex, &sem);
 	report_result(tc, json_error == JSON_UNICODE_ESCAPE_FORMAT,
 				  testname->data, "",
diff --git a/src/test/modules/test_json_parser/README b/src/test/modules/test_json_parser/README
index 61e7c78d588..494ce80fcf0 100644
--- a/src/test/modules/test_json_parser/README
+++ b/src/test/modules/test_json_parser/README
@@ -10,8 +10,8 @@ This module contains two programs for testing the json parsers.
   alternative chunk size, "-r nn" runs a range of chunk sizes down to one byte
   on the same input (with output separated by null bytes), and "-s" specifies
   using semantic routines. The semantic routines re-output the json, although
-  not in a very pretty form. The required non-option argument is the input file
-  name.
+  not in a very pretty form. The option "--json5" makes the parser accept
+  JSON5 input. The required non-option argument is the input file name.
 - `test_json_parser_perf` is for speed testing both the standard
   recursive descent parser and the non-recursive incremental
   parser. If given the `-i` flag it uses the non-recursive parser,
diff --git a/src/test/modules/test_json_parser/json5_comments.json5 b/src/test/modules/test_json_parser/json5_comments.json5
new file mode 100644
index 00000000000..9cc9e539f94
--- /dev/null
+++ b/src/test/modules/test_json_parser/json5_comments.json5
@@ -0,0 +1,12 @@
+// leading line comment
+{
+    /* block comment before key */ "key1": "value1", // trailing line comment on a pair
+    "key2": /* inline block comment */ "value2",
+    "arr": [
+        1,
+        /* mid-array block comment */
+        2,
+        3
+    ]
+}
+/* trailing block comment */
diff --git a/src/test/modules/test_json_parser/json5_comments.out b/src/test/modules/test_json_parser/json5_comments.out
new file mode 100644
index 00000000000..1ad7126976e
--- /dev/null
+++ b/src/test/modules/test_json_parser/json5_comments.out
@@ -0,0 +1,10 @@
+{
+"key1": "value1",
+"key2": "value2",
+"arr": [
+1,
+2,
+3
+]
+
+}
diff --git a/src/test/modules/test_json_parser/meson.build b/src/test/modules/test_json_parser/meson.build
index 2688686e37b..b56a1b3ce22 100644
--- a/src/test/modules/test_json_parser/meson.build
+++ b/src/test/modules/test_json_parser/meson.build
@@ -59,7 +59,8 @@ tests += {
       't/001_test_json_parser_incremental.pl',
       't/002_inline.pl',
       't/003_test_semantic.pl',
-      't/004_test_parser_perf.pl'
+      't/004_test_parser_perf.pl',
+      't/005_test_json5.pl'
     ],
     'deps': [
       test_json_parser_incremental,
diff --git a/src/test/modules/test_json_parser/t/005_test_json5.pl b/src/test/modules/test_json_parser/t/005_test_json5.pl
new file mode 100644
index 00000000000..7aac5005ab6
--- /dev/null
+++ b/src/test/modules/test_json_parser/t/005_test_json5.pl
@@ -0,0 +1,99 @@
+
+# Copyright (c) 2021-2026, PostgreSQL Global Development Group
+
+# Test JSON5 support in the incremental JSON parser.  Each feature
+# fixture must be accepted in --json5 mode, whole-input and
+# byte-at-a-time, with identical semantic output, and rejected without
+# --json5.  The non-incremental parser is exercised by SQL-level
+# regression tests (see src/test/regress/sql/json5.sql).
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Utils;
+use Test::More;
+use FindBin;
+use File::Temp qw(tempfile);
+
+my $dir = PostgreSQL::Test::Utils::tempdir;
+
+my @exes = (
+	[ "test_json_parser_incremental", ],
+	[ "test_json_parser_incremental", "-o", ],
+	[ "test_json_parser_incremental_shlib", ],
+	[ "test_json_parser_incremental_shlib", "-o", ]);
+
+# One entry per feature: fixture basename plus, where the failing token
+# is predictable, a regex for the error reported without --json5.
+my @features = (
+	{
+		name => 'comments',
+		file => 'json5_comments',
+		error => qr/Token "\/" is invalid/,
+	},);
+
+# Parse $file with --json5 and compare the semantic output against
+# $expected, whole-input and one byte at a time.
+sub check_accepted
+{
+	my ($exe, $file, $expected, $label) = @_;
+
+	foreach my $chunk (undef, 1)
+	{
+		my $mode = defined $chunk ? 'byte-at-a-time' : 'whole input';
+		my @cmd = (@$exe, "-s", "--json5");
+		push(@cmd, "-c", $chunk) if defined $chunk;
+
+		my ($stdout, $stderr) = run_command([ @cmd, $file ]);
+
+		is($stderr, "", "$label ($mode): no error output");
+
+		my ($fh, $fname) = tempfile(DIR => $dir);
+		print $fh $stdout, "\n";
+		close($fh);
+
+		my @diffopts = ("-u");
+		push(@diffopts, "--strip-trailing-cr") if $windows_os;
+		($stdout, $stderr) =
+		  run_command([ "diff", @diffopts, $fname, $expected ]);
+
+		is($stdout, "", "$label ($mode): no output diff");
+		is($stderr, "", "$label ($mode): no diff error");
+	}
+}
+
+# Check that parsing $file fails, matching $error on stderr if given.
+sub check_rejected
+{
+	my ($exe, $file, $label, $error, @flags) = @_;
+
+	my ($stdout, $stderr) = run_command([ @$exe, "-s", @flags, $file ]);
+
+	unlike($stdout, qr/SUCCESS/, "$label: parsing fails");
+	if (defined $error)
+	{
+		like($stderr, $error, "$label: correct error output");
+	}
+	else
+	{
+		isnt($stderr, "", "$label: error output");
+	}
+}
+
+foreach my $exe (@exes)
+{
+	note "testing executable @$exe";
+
+	foreach my $f (@features)
+	{
+		my $file = "$FindBin::RealBin/../$f->{file}.json5";
+		my $expected = "$FindBin::RealBin/../$f->{file}.out";
+
+		check_accepted($exe, $file, $expected, "json5 $f->{name}");
+		check_rejected($exe, $file, "non-json5 mode: $f->{name}",
+			$f->{error});
+	}
+
+}
+
+done_testing();
diff --git a/src/test/modules/test_json_parser/test_json_parser_incremental.c b/src/test/modules/test_json_parser/test_json_parser_incremental.c
index 8fbd180c861..63c15acdb52 100644
--- a/src/test/modules/test_json_parser/test_json_parser_incremental.c
+++ b/src/test/modules/test_json_parser/test_json_parser_incremental.c
@@ -41,9 +41,9 @@
 
 #include "common/jsonapi.h"
 #include "common/logging.h"
+#include "getopt_long.h"
 #include "lib/stringinfo.h"
 #include "mb/pg_wchar.h"
-#include "pg_getopt.h"
 
 #define BUFSIZE 6000
 #define DEFAULT_CHUNK_SIZE 60
@@ -99,15 +99,21 @@ main(int argc, char **argv)
 	char	   *testfile;
 	int			c;
 	bool		need_strings = false;
+	bool		json5 = false;
 	int			ret = 0;
 
+	static const struct option long_options[] = {
+		{"json5", no_argument, NULL, '5'},
+		{NULL, 0, NULL, 0},
+	};
+
 	pg_logging_init(argv[0]);
 
 	lex = calloc(1, sizeof(JsonLexContext));
 	if (!lex)
 		pg_fatal("out of memory");
 
-	while ((c = getopt(argc, argv, "r:c:os")) != -1)
+	while ((c = getopt_long(argc, argv, "r:c:os", long_options, NULL)) != -1)
 	{
 		switch (c)
 		{
@@ -129,6 +135,9 @@ main(int argc, char **argv)
 				((struct DoState *) sem.semstate)->buf = makeStringInfo();
 				need_strings = true;
 				break;
+			case '5':			/* parse as json5 */
+				json5 = true;
+				break;
 		}
 	}
 
@@ -161,7 +170,7 @@ main(int argc, char **argv)
 		off_t		bytes_left = statbuf.st_size;
 		size_t		to_read = chunk_size;
 
-		makeJsonLexContextIncremental(lex, PG_UTF8, need_strings);
+		makeJsonLexContextIncremental(lex, PG_UTF8, need_strings, json5);
 		setJsonLexContextOwnsTokens(lex, lex_owns_tokens);
 
 		rewind(json_file);
@@ -406,5 +415,6 @@ usage(const char *progname)
 	fprintf(stderr, "  -c chunksize      size of piece fed to parser (default 64)\n");
 	fprintf(stderr, "  -o                set JSONLEX_CTX_OWNS_TOKENS for leak checking\n");
 	fprintf(stderr, "  -s                do semantic processing\n");
+	fprintf(stderr, "  --json5           parse input as json5\n");
 
 }
diff --git a/src/test/modules/test_json_parser/test_json_parser_perf.c b/src/test/modules/test_json_parser/test_json_parser_perf.c
index 9786263a191..0ca71c9c0a4 100644
--- a/src/test/modules/test_json_parser/test_json_parser_perf.c
+++ b/src/test/modules/test_json_parser/test_json_parser_perf.c
@@ -67,7 +67,7 @@ main(int argc, char **argv)
 	{
 		if (use_inc)
 		{
-			lex = makeJsonLexContextIncremental(NULL, PG_UTF8, false);
+			lex = makeJsonLexContextIncremental(NULL, PG_UTF8, false, false);
 			result = pg_parse_json_incremental(lex, &nullSemAction,
 											   json.data, json.len,
 											   true);
@@ -76,7 +76,7 @@ main(int argc, char **argv)
 		else
 		{
 			lex = makeJsonLexContextCstringLen(NULL, json.data, json.len,
-											   PG_UTF8, false);
+											   PG_UTF8, false, false);
 			result = pg_parse_json(lex, &nullSemAction);
 			freeJsonLexContext(lex);
 		}
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index ffb413ab612..68a93e27ab9 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1433,6 +1433,7 @@ JoinTreeItem
 JoinType
 JsObject
 JsValue
+Json5CommentState
 JsonAggConstructor
 JsonAggState
 JsonArgument
-- 
2.43.0

