Skip to main content

Overview

When syncing data from your backend source database to PowerSync, JSON columns (whether from MongoDB documents, Postgres JSONB columns, or other JSON data types) are stored as TEXT in SQLite. See the type mapping guide for more details. This guide shows you how to effectively query and filter JSON data using SQLite’s powerful JSON functions on the client.

Understanding JSON Storage in PowerSync

Your backend source database might store structured data as JSON in various ways:
  • MongoDB: Nested documents and arrays
  • Postgres: JSONB, JSON, array, or custom types
  • MySQL: JSON columns
  • SQL Server: JSON columns
Regardless of the source, PowerSync syncs these JSON structures to SQLite as TEXT columns. On the client side, you can query this data using SQLite’s built-in JSON functions without needing to parse it yourself. Learn more about how PowerSync handles JSON, arrays, and custom types.

Example Data Structure

Let’s use a task management system where tasks have nested metadata:
In SQLite, the assignees, tags, and metadata columns are stored as JSON strings. For details on how different backend types map to SQLite, see database types and mapping.

JSON Extraction Basics

Standard json_extract() Function

Extract values from JSON using path expressions:
Path syntax:
  • $ - root element
  • . - object member access
  • [index] - array element access

Shorthand: The -> and ->> Operators

SQLite provides convenient shorthand operators for JSON extraction:
The difference between -> and ->>:
  • -> returns JSON (preserves type information, quotes strings)
  • ->> extracts the value unquoted (strings as TEXT, numbers/booleans as their native types)
When to use which:
  • Use ->> when extracting final values for display or comparison
  • Use -> when extracting intermediate JSON for further processing
  • ->> preserves data types (numbers stay numbers, not strings)

Nested Path Access

Access deeply nested values:

Querying Arrays with json_each()

Flattening Simple Arrays

For the tags array, use json_each() to create one row per element:
What’s happening:
  • json_each(t.tags) creates a virtual table with one row per tag
  • tag.value contains each individual tag string
  • You can filter, join, or aggregate these expanded rows

Querying Nested Objects in Arrays

For complex objects like assignees:
Key points:
  • Each assignee.value is a JSON object representing one assignee
  • Use ->> to extract text values for comparison
  • Use -> when you need numeric values for calculations

Real-World Query Examples

Example 1: Finding Tasks by Assignee

Use case: Show all tasks assigned to a specific user.

Example 2: Calculating Total Hours by Role

Use case: Aggregate hours across all tasks grouped by role.

Example 3: Tasks with Specific Tags

Use case: Find tasks tagged with multiple specific tags.
Or using a simpler approach for single tags:

Example 4: Filtering by Array Contents

Use case: Find tasks that depend on a specific task ID.

Example 5: Checking for Array Membership

Use case: Check if a task has any dependencies.

Working with Comma or Delimiter-Separated Values

Sometimes JSON strings contain delimiter-separated values (like "NYC;LAX;MIA"). Here’s how to query them efficiently:
Why this pattern works:
  • Wraps the value: ";user_001;user_002;user_003;"
  • Searches for ;user_001; ensuring exact delimiter-bounded match
  • Prevents false matches (won’t match “user_0011” when searching for “user_001”)
Avoid LIKE for delimited strings:

Advanced Techniques

Using CTEs for Cleaner Queries

Common Table Expressions make complex JSON queries more readable:

Combining Multiple JSON Arrays

Query across multiple nested arrays:
Cartesian product warning: When using multiple json_each() calls, you create a Cartesian product. A task with 3 assignees and 4 tags creates 12 rows. Use DISTINCT when needed and filter early to minimize row expansion.

Checking for Key Existence

Verify if a JSON key exists:

Performance Optimization

Important Performance Considerations
  1. Index JSON columns for better performance: If you frequently query JSON fields, add indexes to the JSON string columns in your AppSchema:
  1. Minimize json_each() usage: Each json_each() call expands rows. For a table with 10,000 tasks averaging 5 assignees each, you’re processing 50,000 rows.
  2. Use EXISTS for membership checks: More efficient than joining:
  1. Cache extracted values in CTEs: Extract once, use multiple times:

Useful JSON Functions

Beyond extraction, SQLite offers many JSON utilities:

Common Gotchas

Watch out for these common issues:
  1. NULL vs missing keys: json_extract() returns NULL for non-existent paths. Always check for NULL:
  1. Type mismatches:
  1. Array index bounds: Out-of-bounds array access returns NULL, not an error:
  1. Quotes in JSON strings: Use ->> to get unquoted text, not ->:
  1. Performance on large arrays: json_each() on arrays with thousands of elements can be slow. Consider data restructuring for such cases.

Summary

Querying JSON data in SQLite effectively requires:
  • Understanding that JSON is stored as strings but queryable with built-in functions
  • Using json_extract() or the shorthand -> and ->> operators
  • Leveraging json_each() to flatten arrays for filtering and aggregation
  • Being mindful of type conversions and NULL handling
  • Optimizing queries by filtering early and considering denormalization for critical paths
With these techniques, you can query complex nested data structures synced from your backend while maintaining good performance on mobile and edge devices. For complete SQLite JSON function reference, see the SQLite JSON documentation.