MySQL

10 Powerful JSON Functions in MySQL 8 Every Developer Should Know

MySQL 8 dramatically improved JSON support with functions that eliminate the need for application-side parsing. Here are 10 JSON functions that will transform how you work with JSON columns in MySQL.

JusDB Team
January 2, 2022
9 min read
163 views

A few years ago, a common pattern in production codebases was fetching a JSON blob from MySQL, deserializing it in Python or Node.js, filtering the array in application memory, then writing the result back. That entire pipeline — sometimes spanning hundreds of lines — collapsed into a single SQL query once the team migrated to MySQL 8 and started using its native JSON functions. If you are still parsing JSON in application code after pulling it from MySQL, you are doing unnecessary work. MySQL 8 ships with a rich set of JSON functions that handle extraction, transformation, validation, aggregation, and even schema enforcement directly in the database engine.

TL;DR — 10 MySQL 8 JSON Functions Covered
  1. JSON_TABLE — Shred JSON arrays into relational rows you can JOIN
  2. JSON_OVERLAPS — Test array intersection for fast tag or label filtering
  3. JSON_VALUE — Extract a scalar with explicit type casting
  4. JSON_SCHEMA_VALID — Validate a document against a JSON Schema draft
  5. JSON_ARRAYAGG — Aggregate rows into a JSON array
  6. JSON_OBJECTAGG — Aggregate key-value pairs into a JSON object
  7. JSON_SEARCH — Locate a value anywhere inside a JSON document
  8. JSON_MERGE_PATCH — RFC 7396 patch semantics (null removes keys)
  9. JSON_MERGE_PRESERVE — Merge without losing duplicate keys
  10. JSON_PRETTY — Pretty-print JSON for readable debug output

JSON_TABLE — Transform JSON into Relational Rows

JSON_TABLE is the most powerful function in this list. It is a table-valued function that reads a JSON array and emits one relational row per element, making it possible to JOIN, GROUP BY, and filter JSON content as if it were a normal table.

Basic usage

sql
SELECT
  p.id,
  p.name,
  t.tag
FROM products p,
JSON_TABLE(
  p.tags,
  '$[*]' COLUMNS (
    tag VARCHAR(60) PATH '$'
  )
) AS t
WHERE t.tag = 'wireless';

This query unnests the tags JSON array stored in every product row and filters on a specific tag without any application-side loop.

Multi-column extraction with nested paths

sql
SELECT
  o.order_id,
  li.sku,
  li.qty,
  li.unit_price
FROM orders o,
JSON_TABLE(
  o.line_items,
  '$[*]' COLUMNS (
    sku        VARCHAR(40)    PATH '$.sku',
    qty        INT            PATH '$.quantity',
    unit_price DECIMAL(10,2)  PATH '$.price'
  )
) AS li
WHERE li.qty > 5;
Practical use case: Use JSON_TABLE to migrate denormalized JSON columns into proper relational tables. Run it in a CREATE TABLE ... SELECT statement and you get a normalized table in one shot, with no ETL script required.

JSON_OVERLAPS — Array Intersection for Filtering

Introduced in MySQL 8.0.17, JSON_OVERLAPS returns 1 if two JSON documents share at least one matching value. For arrays, this means "do any elements appear in both arrays?" — which is exactly what tag and label filtering needs.

sql
-- Find products tagged with at least one of: 'sale', 'clearance', 'discount'
SELECT id, name, tags
FROM products
WHERE JSON_OVERLAPS(tags, '["sale", "clearance", "discount"]');

Comparing with the older JSON_CONTAINS approach

sql
-- Old way: checks if tags contains ALL of these (AND logic)
SELECT id FROM products
WHERE JSON_CONTAINS(tags, '["sale"]');

-- New way: checks if tags contains ANY of these (OR logic)
SELECT id FROM products
WHERE JSON_OVERLAPS(tags, '["sale", "clearance"]');
Practical use case: Multi-select filter UIs — checkboxes where the user picks one or more tags and you want any matching record — map perfectly to JSON_OVERLAPS. Pass the user's selection as a JSON array parameter and you avoid building dynamic SQL.

JSON_VALUE — Extract with Type Casting

JSON_VALUE was added in MySQL 8.0.21 as a cleaner alternative to JSON_UNQUOTE(JSON_EXTRACT(...)). It accepts a path, a RETURNING clause for the output type, and optional ON EMPTY / ON ERROR clauses for safe defaults.

sql
-- Extract a typed scalar without the nested function calls
SELECT
  id,
  JSON_VALUE(metadata, '$.version'   RETURNING UNSIGNED)   AS version,
  JSON_VALUE(metadata, '$.created_at' RETURNING DATETIME)  AS created_at,
  JSON_VALUE(metadata, '$.score'      RETURNING DECIMAL(5,2) ON EMPTY NULL) AS score
FROM documents;

Safe extraction with error handling

sql
-- Return 0 if the path doesn't exist, NULL on a type conversion error
SELECT
  JSON_VALUE(
    payload,
    '$.retry_count'
    RETURNING UNSIGNED
    DEFAULT 0  ON EMPTY
    DEFAULT 0  ON ERROR
  ) AS retry_count
FROM event_queue;
Practical use case: JSON_VALUE with RETURNING is essential when you create generated columns for indexing. The explicit type makes the generated column deterministic and index-eligible.

JSON_SCHEMA_VALID — Validate JSON Structure

JSON_SCHEMA_VALID, available since MySQL 8.0.17, checks a JSON document against a JSON Schema (Draft 4) definition and returns 1 if valid, 0 if not. This moves schema enforcement into the database, where it belongs.

sql
-- Inline validation in a query
SELECT
  id,
  JSON_SCHEMA_VALID(
    '{
      "type": "object",
      "required": ["sku", "price", "quantity"],
      "properties": {
        "sku":      { "type": "string"  },
        "price":    { "type": "number", "minimum": 0 },
        "quantity": { "type": "integer", "minimum": 0 }
      }
    }',
    line_item
  ) AS is_valid
FROM order_lines;

Using it in a CHECK constraint

sql
CREATE TABLE order_lines (
  id        INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  line_item JSON NOT NULL,
  CONSTRAINT chk_line_item_schema CHECK (
    JSON_SCHEMA_VALID(
      '{
        "type": "object",
        "required": ["sku", "price"],
        "properties": {
          "sku":   { "type": "string" },
          "price": { "type": "number", "minimum": 0 }
        }
      }',
      line_item
    )
  )
);
Practical use case: Audit existing rows for schema drift. Run WHERE JSON_SCHEMA_VALID(...) = 0 to find every malformed document that slipped in before the constraint was added, then repair them in a migration.

JSON_ARRAYAGG and JSON_OBJECTAGG — Aggregate into JSON

These two aggregate functions invert the JSON_TABLE pattern: instead of expanding JSON into rows, they collapse rows into JSON. They are indispensable for building API responses directly in SQL and avoiding N+1 query patterns.

JSON_ARRAYAGG

sql
-- Build one JSON array of order IDs per customer
SELECT
  customer_id,
  JSON_ARRAYAGG(order_id ORDER BY created_at DESC) AS recent_orders
FROM orders
GROUP BY customer_id;

JSON_OBJECTAGG

sql
-- Pivot configuration key-value rows into a single JSON object per user
SELECT
  user_id,
  JSON_OBJECTAGG(config_key, config_value) AS preferences
FROM user_config
GROUP BY user_id;

Nesting both for a full document

sql
-- Return each order with its line items as a nested JSON array
SELECT
  o.id,
  o.status,
  JSON_ARRAYAGG(
    JSON_OBJECT(
      'sku',   li.sku,
      'qty',   li.quantity,
      'price', li.unit_price
    )
    ORDER BY li.id
  ) AS line_items
FROM orders o
JOIN order_lines li ON li.order_id = o.id
GROUP BY o.id, o.status;
Practical use case: Replace multiple round-trips or application-side grouping logic with a single SQL query that returns a ready-to-serialize JSON document. This is particularly effective in read-heavy API endpoints where latency matters.

JSON_SEARCH — Find Values Across JSON Documents

JSON_SEARCH locates a string value anywhere inside a JSON document and returns the path (or paths) where it was found. It supports 'one' (stop at first match) and 'all' (return every matching path) modes.

sql
-- Find which path inside the 'contacts' column holds a specific email
SELECT
  id,
  JSON_SEARCH(contacts, 'all', 'alice@example.com') AS paths_found
FROM users
WHERE JSON_SEARCH(contacts, 'one', 'alice@example.com') IS NOT NULL;

Wildcard search with LIKE patterns

sql
-- Find all paths in metadata that contain a value starting with 'error'
SELECT
  id,
  JSON_SEARCH(metadata, 'all', 'error%', NULL, '$') AS error_paths
FROM application_logs
WHERE JSON_SEARCH(metadata, 'one', 'error%', NULL, '$') IS NOT NULL;
Practical use case: Use JSON_SEARCH during data audits to locate PII scattered across semi-structured JSON columns — email addresses, phone numbers, or national ID patterns — before a compliance review.

JSON_MERGE_PATCH vs JSON_MERGE_PRESERVE

MySQL provides two merge semantics. Understanding the difference prevents silent data loss when updating JSON documents.

JSON_MERGE_PATCH (RFC 7396 — destructive on conflicts)

sql
-- Patch: keys in the second document overwrite the first; null removes the key
SELECT JSON_MERGE_PATCH(
  '{"color": "red",  "size": "M",  "stock": 10}',
  '{"color": "blue", "stock": null}'
);
-- Result: {"color": "blue", "size": "M"}
-- 'stock' is removed because its patch value was null
sql
-- Practical update pattern: apply a partial patch to a stored document
UPDATE products
SET attributes = JSON_MERGE_PATCH(attributes, '{"color": "navy", "weight_kg": 0.45}')
WHERE id = 1042;

JSON_MERGE_PRESERVE (non-destructive — keeps all values)

sql
-- Preserve: conflicting values are combined into an array
SELECT JSON_MERGE_PRESERVE(
  '{"tags": ["sale"],  "color": "red"}',
  '{"tags": ["new"],   "color": "blue"}'
);
-- Result: {"tags": ["sale", "new"], "color": ["red", "blue"]}
Practical use case: Use JSON_MERGE_PATCH when building a PATCH API endpoint where the client sends a sparse update document. Use JSON_MERGE_PRESERVE when combining event payloads from multiple sources where every value must be retained.

JSON_PRETTY — Human-Readable Output for Debugging

JSON_PRETTY formats a compact JSON string with indentation and newlines. It has no effect on stored data — it is purely a display aid for development, logging, and admin queries.

sql
-- Compare raw vs formatted output
SELECT
  metadata,
  JSON_PRETTY(metadata) AS metadata_formatted
FROM products
WHERE id = 77;
sql
-- Use in a debug procedure to inspect large JSON columns readably
SELECT JSON_PRETTY(payload)
FROM webhook_events
WHERE id = 4891;

-- Sample output:
-- {
--   "event": "order.created",
--   "timestamp": "2025-11-14T09:22:01Z",
--   "data": {
--     "order_id": 98234,
--     "customer_id": 5501,
--     "total": 149.99
--   }
-- }
Practical use case: Wrap JSON_PRETTY in a stored procedure used by your on-call rotation for incident triage. Readable JSON output in a terminal significantly speeds up debugging compared to scrolling through minified blobs.

Indexing JSON Columns with Generated Columns

MySQL cannot index a JSON column directly, but it can index a generated column derived from a JSON path. This is the standard pattern for making JSON-heavy queries index-eligible and avoiding full table scans.

Creating an indexed generated column

sql
ALTER TABLE events
  ADD COLUMN event_type VARCHAR(80)
    GENERATED ALWAYS AS (JSON_VALUE(payload, '$.type' RETURNING CHAR(80)))
    STORED,
  ADD INDEX idx_event_type (event_type);
sql
-- This query now uses idx_event_type instead of a full table scan
SELECT id, created_at
FROM events
WHERE event_type = 'checkout.completed';

Functional index shorthand (MySQL 8.0.13+)

sql
-- Equivalent approach without altering the schema: functional index
ALTER TABLE events
  ADD INDEX idx_payload_type ((CAST(payload->>'$.type' AS CHAR(80))));

Composite index combining JSON and relational columns

sql
ALTER TABLE events
  ADD COLUMN tenant_id_gen INT UNSIGNED
    GENERATED ALWAYS AS (JSON_VALUE(payload, '$.tenant_id' RETURNING UNSIGNED))
    VIRTUAL,
  ADD INDEX idx_tenant_event (tenant_id_gen, event_type, created_at);
Practical use case: Before adding a generated column, run EXPLAIN on your slowest JSON queries to identify which paths appear in WHERE clauses most frequently. Target those paths first — a single well-chosen generated column index can eliminate the majority of full table scans in a JSON-heavy workload.
Key Takeaways
  • JSON_TABLE replaces application-side JSON parsing loops with a single SQL clause, and doubles as a migration tool for normalizing JSON columns.
  • JSON_OVERLAPS is the correct function for "any of these tags" filtering; JSON_CONTAINS tests for all values, not any.
  • JSON_VALUE with RETURNING provides typed extraction and is the building block for index-eligible generated columns.
  • JSON_SCHEMA_VALID in a CHECK constraint enforces document structure at the storage engine level, before any application code runs.
  • JSON_ARRAYAGG and JSON_OBJECTAGG let you construct complete API response documents in SQL, eliminating N+1 query patterns.
  • JSON_MERGE_PATCH follows RFC 7396 — a null value in the patch deletes the key in the target. Use JSON_MERGE_PRESERVE when you need non-destructive merging.
  • JSON columns cannot be indexed directly. Always use generated columns (stored or virtual) with explicit type casting to make JSON paths index-eligible.
  • Combine EXPLAIN with JSON_PRETTY in your debugging workflow — readable output and execution plan analysis together cut incident resolution time significantly.

Working with JusDB on MySQL JSON Workloads

JusDB optimizes MySQL JSON column performance for engineering teams — indexing strategies with generated columns, query rewrites using JSON_TABLE, and schema design guidance for semi-structured data.

Explore JusDB MySQL Management →  |  Talk to a DBA

Related reading:

Share this article