Subject: [PATCH] Validate JSON object keys before omitting null values JSON_OBJECT with ABSENT ON NULL can skip key validation when it omits an entry with a null value. In the json path this accepts null keys; both json and jsonb paths can accept non-scalar keys. The same keys are rejected when the value is non-null or key uniqueness is requested. Move the json null-key check before the skip decision. Before skipping an entry without a uniqueness check, classify the key type and reject the same categories rejected by the existing conversion routines. This avoids invoking output or cast functions for omitted keys. Add regression coverage for null, array, composite, json and jsonb keys with null values, including keys with a cast to json and the existing null-key checks with unique keys. Reported in BUG #19736: https://www.postgresql.org/message-id/19736-3a44617ea5534c77@postgresql.org diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c index 28e5f3c..ae4f57d 100644 --- a/src/backend/utils/adt/json.c +++ b/src/backend/utils/adt/json.c @@ -1220,6 +1220,12 @@ json_build_object_worker(int nargs, const Datum *args, const bool *nulls, const bool skip; int key_offset; + /* Keys must not be null, even if their values will be omitted. */ + if (nulls[i]) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("null value not allowed for object key"))); + /* Skip null values if absent_on_null */ skip = absent_on_null && nulls[i + 1]; @@ -1227,7 +1233,21 @@ json_build_object_worker(int nargs, const Datum *args, const bool *nulls, const { /* If key uniqueness check is needed we must save skipped keys */ if (!unique_keys) + { + JsonTypeCategory tcategory; + Oid outfuncoid; + + /* Validate the key type without converting the omitted key. */ + json_categorize_type(types[i], false, &tcategory, &outfuncoid); + if (tcategory == JSONTYPE_ARRAY || + tcategory == JSONTYPE_COMPOSITE || + tcategory == JSONTYPE_JSON || + tcategory == JSONTYPE_CAST) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("key value must be scalar, not array, composite, or json"))); continue; + } out = json_unique_builder_get_throwawaybuf(&unique_check); } @@ -1238,12 +1258,6 @@ json_build_object_worker(int nargs, const Datum *args, const bool *nulls, const out = result; } - /* process key */ - if (nulls[i]) - ereport(ERROR, - (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), - errmsg("null value not allowed for object key"))); - /* save key offset before appending it */ key_offset = out->len; diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index da0061b..c8f1252 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -1162,7 +1162,22 @@ jsonb_build_object_worker(int nargs, const Datum *args, const bool *nulls, const /* we need to save skipped keys for the key uniqueness check */ if (skip && !unique_keys) + { + JsonTypeCategory tcategory; + Oid outfuncoid; + + /* Validate the key type without converting the omitted key. */ + json_categorize_type(types[i], true, &tcategory, &outfuncoid); + if (tcategory == JSONTYPE_ARRAY || + tcategory == JSONTYPE_COMPOSITE || + tcategory == JSONTYPE_JSON || + tcategory == JSONTYPE_JSONB || + tcategory == JSONTYPE_CAST) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("key value must be scalar, not array, composite, or json"))); continue; + } add_jsonb(args[i], false, &result, types[i], true); diff --git a/src/test/regress/expected/sqljson.out b/src/test/regress/expected/sqljson.out index 30f9e10..29e2ee8 100644 --- a/src/test/regress/expected/sqljson.out +++ b/src/test/regress/expected/sqljson.out @@ -539,6 +539,31 @@ SELECT JSON_OBJECT('a': '1', 'b': NULL, 'c': 2 ABSENT ON NULL); {"a" : "1", "c" : 2} (1 row) +-- Keys must be valid even when ABSENT ON NULL omits their values. +SELECT JSON_OBJECT((NULL::text) VALUE NULL::text ABSENT ON NULL RETURNING json); +ERROR: null value not allowed for object key +SELECT JSON_OBJECT((NULL::text) VALUE NULL::text ABSENT ON NULL WITH UNIQUE KEYS RETURNING json); +ERROR: null value not allowed for object key +SELECT JSON_OBJECT((ARRAY[1,2]) VALUE NULL::text ABSENT ON NULL RETURNING json); +ERROR: key value must be scalar, not array, composite, or json +SELECT JSON_OBJECT((ROW(1,2)) VALUE NULL::text ABSENT ON NULL RETURNING json); +ERROR: key value must be scalar, not array, composite, or json +SELECT JSON_OBJECT((json '1') VALUE NULL::text ABSENT ON NULL RETURNING json); +ERROR: key value must be scalar, not array, composite, or json +SELECT JSON_OBJECT((jsonb '1') VALUE NULL::text ABSENT ON NULL RETURNING json); +ERROR: key value must be scalar, not array, composite, or json +SELECT JSON_OBJECT((NULL::text) VALUE NULL::text ABSENT ON NULL RETURNING jsonb); +ERROR: argument 1: key must not be null +SELECT JSON_OBJECT((NULL::text) VALUE NULL::text ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb); +ERROR: argument 1: key must not be null +SELECT JSON_OBJECT((ARRAY[1,2]) VALUE NULL::text ABSENT ON NULL RETURNING jsonb); +ERROR: key value must be scalar, not array, composite, or json +SELECT JSON_OBJECT((ROW(1,2)) VALUE NULL::text ABSENT ON NULL RETURNING jsonb); +ERROR: key value must be scalar, not array, composite, or json +SELECT JSON_OBJECT((json '1') VALUE NULL::text ABSENT ON NULL RETURNING jsonb); +ERROR: key value must be scalar, not array, composite, or json +SELECT JSON_OBJECT((jsonb '1') VALUE NULL::text ABSENT ON NULL RETURNING jsonb); +ERROR: key value must be scalar, not array, composite, or json SELECT JSON_OBJECT(1: 1, '2': NULL, '3': 1, repeat('x', 1000): 1, 2: repeat('a', 100) WITH UNIQUE); ERROR: duplicate JSON object key value: "2" SELECT JSON_OBJECT(1: 1, '1': NULL WITH UNIQUE); @@ -607,6 +632,10 @@ $$ LANGUAGE sql IMMUTABLE; CREATE CAST (mood AS json) WITH FUNCTION mood_to_json(mood) AS IMPLICIT; SELECT JSON_OBJECT('happy'::mood: '123'::jsonb); ERROR: key value must be scalar, not array, composite, or json +SELECT JSON_OBJECT('happy'::mood: NULL ABSENT ON NULL RETURNING json); +ERROR: key value must be scalar, not array, composite, or json +SELECT JSON_OBJECT('happy'::mood: NULL ABSENT ON NULL RETURNING jsonb); +ERROR: key value must be scalar, not array, composite, or json DROP CAST (mood AS json); DROP FUNCTION mood_to_json; DROP TYPE mood; diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql index 9fa203c..030685b 100644 --- a/src/test/regress/sql/sqljson.sql +++ b/src/test/regress/sql/sqljson.sql @@ -138,6 +138,20 @@ SELECT JSON_OBJECT('a': '1', 'b': NULL, 'c': 2); SELECT JSON_OBJECT('a': '1', 'b': NULL, 'c': 2 NULL ON NULL); SELECT JSON_OBJECT('a': '1', 'b': NULL, 'c': 2 ABSENT ON NULL); +-- Keys must be valid even when ABSENT ON NULL omits their values. +SELECT JSON_OBJECT((NULL::text) VALUE NULL::text ABSENT ON NULL RETURNING json); +SELECT JSON_OBJECT((NULL::text) VALUE NULL::text ABSENT ON NULL WITH UNIQUE KEYS RETURNING json); +SELECT JSON_OBJECT((ARRAY[1,2]) VALUE NULL::text ABSENT ON NULL RETURNING json); +SELECT JSON_OBJECT((ROW(1,2)) VALUE NULL::text ABSENT ON NULL RETURNING json); +SELECT JSON_OBJECT((json '1') VALUE NULL::text ABSENT ON NULL RETURNING json); +SELECT JSON_OBJECT((jsonb '1') VALUE NULL::text ABSENT ON NULL RETURNING json); +SELECT JSON_OBJECT((NULL::text) VALUE NULL::text ABSENT ON NULL RETURNING jsonb); +SELECT JSON_OBJECT((NULL::text) VALUE NULL::text ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb); +SELECT JSON_OBJECT((ARRAY[1,2]) VALUE NULL::text ABSENT ON NULL RETURNING jsonb); +SELECT JSON_OBJECT((ROW(1,2)) VALUE NULL::text ABSENT ON NULL RETURNING jsonb); +SELECT JSON_OBJECT((json '1') VALUE NULL::text ABSENT ON NULL RETURNING jsonb); +SELECT JSON_OBJECT((jsonb '1') VALUE NULL::text ABSENT ON NULL RETURNING jsonb); + SELECT JSON_OBJECT(1: 1, '2': NULL, '3': 1, repeat('x', 1000): 1, 2: repeat('a', 100) WITH UNIQUE); SELECT JSON_OBJECT(1: 1, '1': NULL WITH UNIQUE); @@ -171,6 +185,8 @@ CREATE FUNCTION mood_to_json(mood) RETURNS json AS $$ $$ LANGUAGE sql IMMUTABLE; CREATE CAST (mood AS json) WITH FUNCTION mood_to_json(mood) AS IMPLICIT; SELECT JSON_OBJECT('happy'::mood: '123'::jsonb); +SELECT JSON_OBJECT('happy'::mood: NULL ABSENT ON NULL RETURNING json); +SELECT JSON_OBJECT('happy'::mood: NULL ABSENT ON NULL RETURNING jsonb); DROP CAST (mood AS json); DROP FUNCTION mood_to_json; DROP TYPE mood;