PostgreSQL Error 2203F: SQL JSON Scalar Required
PostgreSQL error 2203F: sql_json_scalar_required is raised when a SQL/JSON function that expects a scalar return value (string, number, boolean, or null) receives a composite JSON value such as an array or object instead. This error is most commonly encountered with the JSON_VALUE() function and other SQL/JSON path functions introduced in PostgreSQL 12 and later. Understanding the distinction between scalar and non-scalar JSON values is key to resolving this error quickly.
Top 3 Causes
1. Using JSON_VALUE() on a JSON Array or Object
JSON_VALUE() is strictly designed to return a single scalar value per the SQL standard. When the JSON path expression resolves to an array or object, PostgreSQL raises 2203F.
-- This raises ERROR 2203F
SELECT JSON_VALUE('{"items": [10, 20, 30]}', '$.items');
-- ERROR: 2203F: SQL/JSON scalar required
-- Fix: target a scalar element within the array
SELECT JSON_VALUE('{"items": [10, 20, 30]}', '$.items[0]');
-- Result: 10
-- Fix: use JSON_QUERY() to return arrays or objects
SELECT JSON_QUERY('{"items": [10, 20, 30]}', '$.items');
-- Result: [10, 20, 30]
-- Fix: suppress the error with ON ERROR clause
SELECT JSON_VALUE('{"items": [10, 20, 30]}', '$.items' NULL ON ERROR);
-- Result: NULL
2. JSON Path Points to a Nested Object Instead of a Leaf Value
When working with deeply nested JSON, it is easy to write a path that resolves to an intermediate object rather than a leaf scalar value.
-- Nested JSON data
SELECT JSON_VALUE(
'{"user": {"name": "Alice", "city": "Seoul"}}',
'$.user' -- points to an object, not a scalar
);
-- ERROR: 2203F: SQL/JSON scalar required
-- Fix: extend the path to the actual scalar leaf
SELECT JSON_VALUE(
'{"user": {"name": "Alice", "city": "Seoul"}}',
'$.user.name'
);
-- Result: Alice
-- Practical table-based example
CREATE TABLE customers (id SERIAL PRIMARY KEY, info JSONB);
INSERT INTO customers (info) VALUES
('{"name": "Alice", "contact": {"email": "alice@example.com", "phone": "010-1234-5678"}}');
SELECT
id,
JSON_VALUE(info::json, '$.name') AS name,
JSON_VALUE(info::json, '$.contact.email') AS email
FROM customers;
3. Inconsistent JSON Field Types Across Rows
In real-world data, a field may sometimes contain a scalar and sometimes an array depending on how the data was ingested. Applying JSON_VALUE() blindly across all rows will fail on any row where the field is not scalar.
-- Mixed-type data
CREATE TABLE events (id SERIAL PRIMARY KEY, payload JSONB);
INSERT INTO events (payload) VALUES
('{"type": "click"}'), -- scalar string
('{"type": ["click", "hover"]}'); -- array
-- This fails on the second row
SELECT JSON_VALUE(payload::json, '$.type') FROM events;
-- ERROR: 2203F: SQL/JSON scalar required
-- Fix: use jsonb_typeof() to branch by type
SELECT
id,
CASE
WHEN jsonb_typeof(payload->'type') = 'array'
THEN payload->'type'->>0
ELSE payload->>'type'
END AS event_type
FROM events;
-- Alternative: use jsonb_path_query_first() which handles both gracefully
SELECT
id,
jsonb_path_query_first(payload, '$.type[0]') AS event_type
FROM events;
Quick Fix Solutions
| Situation | Recommended Fix |
|---|---|
| Need scalar only | Extend JSON path to leaf node |
| Need array or object | Switch to JSON_QUERY()
|
| Mixed types in data | Use jsonb_typeof() + CASE
|
| Graceful error handling | Add NULL ON ERROR clause |
-- Universal safe pattern for production code
SELECT JSON_VALUE(
payload::json,
'$.some.field'
DEFAULT 'N/A' ON ERROR
) AS safe_value
FROM events;
Prevention Tips
1. Enforce JSON structure with CHECK constraints
Use jsonb_typeof() in CHECK constraints to guarantee field types at insert time, eliminating type ambiguity at query time.
ALTER TABLE events
ADD CONSTRAINT chk_type_is_scalar
CHECK (jsonb_typeof(payload->'type') IN ('string', 'number', 'boolean', 'null'));
2. Always include ON ERROR in production JSON_VALUE() calls
Never let an unexpected JSON structure crash a production query. The ON ERROR clause lets you define a safe fallback behavior, making your queries resilient to data variability.
-- Robust production pattern
SELECT
id,
JSON_VALUE(payload::json, '$.type' NULL ON ERROR) AS type,
JSON_VALUE(payload::json, '$.user.name' NULL ON ERROR) AS user_name
FROM events;
Related Errors
-
2203Esql_json_array_not_found— The inverse: an array was expected but a scalar or object was returned. -
2203Wsql_json_object_not_found— An object was expected but a scalar or array was returned. -
22032invalid_json_text— The JSON string itself is malformed and fails parsing before path evaluation.
📖 Want a more detailed guide?
Check out the full in-depth version (Korean) on oraerror.com — includes detailed analysis, additional SQL examples, and prevention tips.
Top comments (0)