Introduction
JSON_TABLE is a SQL-standard function introduced in PostgreSQL 17 that transforms JSON data into a relational table format. It replaces complex combinations of jsonb_array_elements, jsonb_to_record, and lateral joins with a single, declarative expression. This is one of the most powerful additions for working with JSON data.
Key Concepts
- JSON_TABLE(): A SQL-standard function that maps a JSON document to a virtual relational table with typed columns.
- COLUMNS clause: Defines the output columns, their types, and the JSON path that populates each one.
- NESTED PATH: Allows flattening nested arrays within the same JSON_TABLE call, producing one row per nested element.
- Error handling clauses:
DEFAULT ... ON ERRORandNULL ON ERRORcontrol behavior when a JSON path fails or a type cast is invalid.
Real World Context
Before JSON_TABLE, extracting structured data from complex JSON required verbose lateral joins with jsonb_array_elements and manual type casting. Reporting tools and ETL pipelines benefit enormously from JSON_TABLE because it produces clean, typed tabular output from nested documents in a single query. Data analysts working with JSON API responses can now write straightforward SELECT statements.
Deep Dive
Basic JSON_TABLE Usage
JSON_TABLE takes a JSON expression, a row path, and a COLUMNS definition:
sqlSELECT jt.* FROM JSON_TABLE( '[{"id": 1, "name": "Alice", "age": 30}, {"id": 2, "name": "Bob", "age": 25}]'::jsonb, '$[*]' COLUMNS ( id INTEGER PATH '$.id', name TEXT PATH '$.name', age INTEGER PATH '$.age' ) ) AS jt;
This produces a clean relational result with typed columns. The '$[*]' path iterates over each array element, and each COLUMNS entry extracts a specific field.
Using JSON_TABLE with Table Data
In practice, you use JSON_TABLE with data stored in a table column:
sqlCREATE TABLE api_responses ( id SERIAL PRIMARY KEY, payload JSONB NOT NULL ); INSERT INTO api_responses (payload) VALUES ('{"orders": [{"oid": 101, "total": 59.99, "status": "shipped"}, {"oid": 102, "total": 124.50, "status": "pending"}]}'); SELECT r.id AS response_id, jt.* FROM api_responses r, JSON_TABLE( r.payload, '$.orders[*]' COLUMNS ( order_id INTEGER PATH '$.oid', total NUMERIC PATH '$.total', status TEXT PATH '$.status' ) ) AS jt;
This flattens the nested orders array into relational rows joined with the parent response.
Nested Paths for Multi-Level Flattening
The NESTED PATH clause handles arrays within arrays:
sqlSELECT jt.* FROM JSON_TABLE( '{"customer": "Alice", "orders": [ {"id": 1, "items": [{"product": "Laptop", "qty": 1}, {"product": "Mouse", "qty": 2}]}, {"id": 2, "items": [{"product": "Keyboard", "qty": 1}]} ]}'::jsonb, '$.orders[*]' COLUMNS ( order_id INTEGER PATH '$.id', NESTED PATH '$.items[*]' COLUMNS ( product TEXT PATH '$.product', quantity INTEGER PATH '$.qty' ) ) ) AS jt;
This produces one row per item, with the order_id repeated for each item in that order. Without JSON_TABLE, this would require multiple lateral joins.
Error Handling
JSON_TABLE supports error handling clauses:
sqlSELECT jt.* FROM JSON_TABLE( '[{"id": 1, "price": "29.99"}, {"id": 2, "price": "invalid"}]'::jsonb, '$[*]' COLUMNS ( id INTEGER PATH '$.id', price NUMERIC PATH '$.price' DEFAULT 0 ON ERROR ) ) AS jt;
The DEFAULT 0 ON ERROR clause returns 0 instead of failing when "invalid" cannot be cast to NUMERIC.
Common Pitfalls
- Forgetting the alias —
JSON_TABLErequires an alias (e.g.,AS jt). Omitting it causes a syntax error. - Using wrong path for nested arrays — The row path must point to the array level you want to iterate.
'$'instead of'$[*]'returns a single row with the whole document. - Implicit json-to-jsonb conversion overhead —
JSON_TABLEaccepts bothjsonandjsonb, butjsoninput is implicitly cast tojsonbon every call. For best performance, store data asjsonbto avoid repeated conversion.
Best Practices
- Use JSON_TABLE for complex flattening — When you need typed columns from nested JSON arrays,
JSON_TABLEis cleaner than lateral joins withjsonb_array_elements. - Add error handling for external data — When processing untrusted JSON (API responses, user uploads), use
DEFAULT ... ON ERRORorNULL ON ERRORto handle malformed values gracefully. - Create views for reusable transformations — Wrap
JSON_TABLEqueries in views so reporting tools can treat JSON data as regular tables.
Summary
JSON_TABLEconverts JSON documents into typed relational rows in a single expression.- The COLUMNS clause maps JSON paths to output columns with specific SQL types.
- NESTED PATH handles multi-level array flattening without manual lateral joins.
- Error handling clauses (
DEFAULT ... ON ERROR,NULL ON ERROR) make it safe for untrusted data. - Available in PostgreSQL 17+ and replaces verbose
jsonb_array_elementspatterns.
Code Examples
-- Flatten orders with line items in a single query
SELECT jt.*
FROM orders o,
JSON_TABLE(
o.data,
'$.line_items[*]'
COLUMNS (
product_name TEXT PATH '$.name',
quantity INTEGER PATH '$.qty',
unit_price NUMERIC PATH '$.price',
NESTED PATH '$.discounts[*]'
COLUMNS (
discount_code TEXT PATH '$.code',
discount_pct NUMERIC PATH '$.percent'
)
)
) AS jt
WHERE o.status = 'pending';