Working with JSON in SQL Server: OPENJSON, JSON_VALUE and FOR JSON
Modern applications send and receive JSON everywhere, and SQL Server has built-in functions to read, write and query it, available since SQL Server 2016 and in Azure SQL. Used well, they let you store flexible data and exchange it with APIs without extra code. Used badly, they create slow, hard-to-query tables. Here is the practical toolkit.

Storing JSON
Traditionally JSON is stored in an NVARCHAR column, with a check constraint to keep it valid:
CREATE TABLE dbo.EmployeeSettings (
EmployeeId INT PRIMARY KEY,
Settings NVARCHAR(MAX) NOT NULL
CONSTRAINT CK_EmployeeSettings_IsJson CHECK (ISJSON(Settings) = 1)
);
INSERT INTO dbo.EmployeeSettings VALUES
(1, N'{"theme":"dark","language":"en","notifications":{"email":true,"sms":false},"roles":["payroll","hr"]}');Azure SQL Database and newer SQL Server versions also offer a native json data type that stores JSON more efficiently. Check what your version supports; the functions below work with both.
Read a single value: JSON_VALUE
SELECT
EmployeeId,
JSON_VALUE(Settings, '$.theme') AS Theme,
JSON_VALUE(Settings, '$.notifications.email') AS EmailOn
FROM dbo.EmployeeSettings;JSON_VALUE returns a scalar as text (up to 4,000 characters). For objects or arrays it returns NULL in the default lax mode; use JSON_QUERY for those.
Read an object or array: JSON_QUERY
SELECT JSON_QUERY(Settings, '$.roles') AS Roles -- returns ["payroll","hr"]
FROM dbo.EmployeeSettings;Turn JSON into rows: OPENJSON
OPENJSON is the workhorse: it turns a JSON array into a table you can join and filter. With a WITH clause you define typed columns:
DECLARE @json NVARCHAR(MAX) = N'[
{"code":"E100","name":"Asha","dept":10},
{"code":"E101","name":"Ravi","dept":20}
]';
SELECT *
FROM OPENJSON(@json)
WITH (
EmployeeCode VARCHAR(20) '$.code',
FullName NVARCHAR(100) '$.name',
DepartmentId INT '$.dept'
);This is a clean way to pass a whole list from an application to a stored procedure in one parameter and insert it with a single INSERT ... SELECT. OPENJSON requires database compatibility level 130 or higher.
To expand an array stored inside a column, combine it with CROSS APPLY:
SELECT s.EmployeeId, r.value AS Role
FROM dbo.EmployeeSettings s
CROSS APPLY OPENJSON(s.Settings, '$.roles') r;Change JSON: JSON_MODIFY
UPDATE dbo.EmployeeSettings
SET Settings = JSON_MODIFY(Settings, '$.theme', 'light')
WHERE EmployeeId = 1;
-- Append to an array
UPDATE dbo.EmployeeSettings
SET Settings = JSON_MODIFY(Settings, 'append $.roles', 'admin')
WHERE EmployeeId = 1;Return results as JSON: FOR JSON
SELECT EmployeeId AS id, FullName AS name, DepartmentId AS [department.id]
FROM dbo.Employee
WHERE IsActive = 1
FOR JSON PATH, ROOT('employees');FOR JSON PATH uses dotted column aliases to build nested objects. FOR JSON AUTO builds the shape from the tables in the query.
Index a JSON property you filter on
Filtering on JSON_VALUE in a WHERE clause scans every row and parses JSON each time. If you filter on a property often, expose it as a computed column and index it:
ALTER TABLE dbo.EmployeeSettings
ADD Language AS JSON_VALUE(Settings, '$.language');
CREATE INDEX IX_EmployeeSettings_Language
ON dbo.EmployeeSettings (Language);Queries using WHERE JSON_VALUE(Settings, '$.language') = 'en' can then use the index.
When not to use JSON
- Data you filter, join or aggregate on regularly belongs in proper columns with proper types.
- Data that needs constraints, such as foreign keys, belongs in tables.
- JSON is a good fit for optional settings, audit payloads, API request copies and attributes that vary by record.
Related reading:
Comments
Post a Comment