JSON in SQL Server
Read, shred, build and change JSON with T-SQL: ISJSON, JSON_VALUE versus JSON_QUERY, lax and strict paths, OPENJSON with and without a schema, FOR JSON PATH, JSON_MODIFY, and JSON_OBJECT and JSON_ARRAY.
nvarchar) and has functions to check it, read values out of it, turn it into rows and build it from rows. This lesson uses one order as it might arrive from a web shop:
DECLARE @j nvarchar(max) = N'{ "orderRef": "WEB-1001", "customer": { "name": "Asha Patel", "city": "Leeds" }, "lines": [ { "productId": 6, "qty": 10 }, { "productId": 8, "qty": 2 } ], "giftWrap": true, "note": null}'; SELECT ISJSON(@j) AS Valid, JSON_VALUE(@j, '$.orderRef') AS OrderRef, JSON_VALUE(@j, '$.customer.city') AS City, JSON_VALUE(@j, '$.lines[1].qty') AS SecondQty, JSON_VALUE(@j, '$.giftWrap') AS GiftWrap;Valid OrderRef City SecondQty GiftWrap
----- -------- ----- --------- --------
1 WEB-1001 Leeds 2 true$, the whole document. .name steps into a property and [n] into an array element, counting from 0. JSON_VALUE always returns nvarchar(4000) text, so true comes back as the text “true” and numbers need a CAST before you do sums with them.
JSON_VALUE or JSON_QUERY
| Function | Returns | Given an object or array | Given a scalar |
|---|---|---|---|
JSON_VALUE | |||
JSON_QUERY |
DECLARE @j nvarchar(max) = N'{"customer":{"name":"Asha Patel","city":"Leeds"},"lines":[{"productId":6,"qty":10},{"productId":8,"qty":2}]}'; SELECT JSON_VALUE(@j, '$.customer') AS ValueOfObject, JSON_QUERY(@j, '$.customer') AS QueryOfObject, JSON_QUERY(@j, '$.customer.name') AS QueryOfScalar, JSON_QUERY(@j, '$.lines[0]') AS FirstLine;ValueOfObject QueryOfObject QueryOfScalar FirstLine
------------- ------------------------------------ ------------- ------------------------
NULL {"name":"Asha Patel","city":"Leeds"} NULL {"productId":6,"qty":10}lax and strict
strict at the start of the path and the same problems raise an error:
DECLARE @j nvarchar(max) = N'{"orderRef":"WEB-1001","customer":{"name":"Asha Patel"}}'; SELECT JSON_VALUE(@j, '$.orderref') AS LaxTypo; -- paths are case-sensitiveSELECT JSON_VALUE(@j, 'strict $.orderref') AS StrictTypo;LaxTypo ------- NULL Msg 13608, Level 16, State 2, Line 4 Property cannot be found on the specified JSON path.
OPENJSON: JSON into rows
OPENJSON is a table-valued function. On its own it returns one row per property or array element, with its key, value and type (0 null, 1 string, 2 number, 3 true/false, 4 array, 5 object):
DECLARE @j nvarchar(max) = N'{"orderRef":"WEB-1001","customer":{"name":"Asha Patel"},"lines":[{"productId":6,"qty":10}],"giftWrap":true,"note":null}'; SELECT [key], value, typeFROM OPENJSON(@j);key value type
-------- -------------------------- ----
orderRef WEB-1001 1
customer {"name":"Asha Patel"} 5
lines [{"productId":6,"qty":10}] 4
giftWrap true 3
note NULL 0WITH clause and you choose the columns, their types and the path each one comes from. Point OPENJSON at the array and every element becomes a typed row, ready to join to real tables:
DECLARE @j nvarchar(max) = N'{"orderRef":"WEB-1001","lines":[{"productId":6,"qty":10},{"productId":8,"qty":2}]}'; SELECT l.ProductId, p.Name, l.Qty, p.Price, l.Qty * p.Price AS LineValueFROM OPENJSON(@j, '$.lines') WITH (ProductId int '$.productId', Qty int '$.qty') AS lJOIN dbo.Products AS p ON p.ProductId = l.ProductId;ProductId Name Qty Price LineValue
--------- ----------------- --- ----- ---------
6 Timber 2x4 2.4 m 10 6.80 68.00
8 Wood screws (200) 2 11.90 23.80INSERT … SELECT … FROM OPENJSON(…).
JSON in a column
CHECK (ISJSON(col) = 1) constraint keeps bad text out:
CREATE TABLE #WebEvents ( EventId int IDENTITY PRIMARY KEY, Payload nvarchar(max) NOT NULL CHECK (ISJSON(Payload) = 1));INSERT INTO #WebEvents (Payload) VALUES (N'{"type":"search","terms":["drill","cordless"]}'), (N'{"type":"view","productId":2}'), (N'{"type":"search","terms":["paint"]}'); SELECT e.EventId, t.value AS TermFROM #WebEvents AS eCROSS APPLY OPENJSON(e.Payload, '$.terms') AS tWHERE JSON_VALUE(e.Payload, '$.type') = 'search'ORDER BY e.EventId, t.[key];EventId Term
------- --------
1 drill
1 cordless
3 paintFOR JSON: rows into JSON
FOR JSON PATH to a query and the result becomes one JSON array, one object per row. Dots in the column aliases make nested objects, and a subquery with its own FOR JSON makes a nested array:
SELECT ( SELECT o.OrderId AS id, o.OrderDate AS [date], c.FirstName AS [customer.name], c.City AS [customer.city], (SELECT oi.ProductId AS productId, oi.Quantity AS qty FROM dbo.OrderItems AS oi WHERE oi.OrderId = o.OrderId ORDER BY oi.ProductId FOR JSON PATH) AS lines FROM dbo.Orders AS o JOIN dbo.Customers AS c ON c.CustomerId = o.CustomerId WHERE o.OrderId IN (1, 4) ORDER BY o.OrderId FOR JSON PATH) AS Json;Json
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
[{"id":1,"date":"2024-02-03","customer":{"name":"Asha","city":"Leeds"},"lines":[{"productId":1,"qty":1},{"productId":3,"qty":2}]},{"id":4,"date":"2024-03-15","customer":{"name":"Dev","city":"Bradford"},"lines":[{"productId":6,"qty":40},{"productId":7,"qty":6}]}]INCLUDE_NULL_VALUES, and WITHOUT_ARRAY_WRAPPER drops the outer [ ] when you know there is one row. Wrapping the query in SELECT (…) AS Json gives the result a normal column name.
Changing JSON: JSON_MODIFY
JSON_MODIFY(json, path, newValue) returns a copy of the document with one value changed. append adds to an array, and NULL removes a property in lax mode:
DECLARE @j nvarchar(max) = N'{"orderRef":"WEB-1001","status":"new","tags":["web"]}'; SET @j = JSON_MODIFY(@j, '$.status', 'paid');SET @j = JSON_MODIFY(@j, 'append $.tags', 'priority');SET @j = JSON_MODIFY(@j, '$.paidOn', '2024-07-01');SET @j = JSON_MODIFY(@j, '$.orderRef', NULL); SELECT @j AS Changed;Changed
-----------------------------------------------------------------
{"status":"paid","tags":["web","priority"],"paidOn":"2024-07-01"}JSON_OBJECT and JSON_ARRAY (SQL Server 2022)
true, and a nested JSON_ARRAY stays an array:
SELECT JSON_OBJECT('id': ProductId, 'name': Name, 'inStock': CAST(CASE WHEN Stock > 0 THEN 1 ELSE 0 END AS bit), 'sizes': JSON_ARRAY(Unit, 'trade pack')) AS DocFROM dbo.ProductsWHERE ProductId IN (1, 9)ORDER BY ProductId;Doc
-------------------------------------------------------------------------------
{"id":1,"name":"Claw hammer","inStock":true,"sizes":["each","trade pack"]}
{"id":9,"name":"Wall plugs (100)","inStock":false,"sizes":["box","trade pack"]}{"delivery":[{"productId":4,"qty":50},{"productId":5,"qty":40},{"productId":9,"qty":25}]}, show each delivered product’s name, its current Stock, the delivered qty and the stock after the delivery.
Show a hint
Show the solution
DECLARE @d nvarchar(max) = N'{"delivery":[{"productId":4,"qty":50},{"productId":5,"qty":40},{"productId":9,"qty":25}]}'; SELECT p.Name, p.Stock, d.Qty, p.Stock + d.Qty AS NewStockFROM OPENJSON(@d, '$.delivery') WITH (ProductId int '$.productId', Qty int '$.qty') AS dJOIN dbo.Products AS p ON p.ProductId = d.ProductIdORDER BY p.ProductId;Name Stock Qty NewStock ---------------- ----- --- -------- Cement 25 kg 300 50 350 Sand 25 kg 220 40 260 Wall plugs (100) 0 25 25
Remember
JSON is stored as nvarchar; ISJSON checks it and a CHECK constraint can enforce it. Paths start at $, use .name and [index], count from 0 and are case-sensitive. JSON_VALUE reads scalars (as nvarchar), JSON_QUERY reads objects and arrays; the wrong one gives NULL. Lax paths return NULL for problems; strict paths raise errors. OPENJSON turns JSON into rows; WITH gives typed columns; use CROSS APPLY for JSON in a column. FOR JSON PATH builds JSON from rows; dotted aliases nest objects and subqueries nest arrays. JSON_MODIFY changes, appends to and removes from a document. JSON_OBJECT and JSON_ARRAY (2022) build small documents with real JSON types.