SQL Server: Convert Between XML and JSON
Turn XML into JSON with nodes() and FOR JSON, and JSON into XML with OPENJSON and FOR XML PATH, keeping nested rows nested.
- Updated
- Requires
- SQL Server 2016+
- Markdown
Requires: FOR JSON and OPENJSON also need database compatibility level 130 or higher.
Problem
One system sends XML and the next one wants JSON, or the other way round, and the conversion has to happen in the database: in a stored procedure, a job, or a staging step. SQL Server has no function that converts one to the other directly.
Solution
Read the source into rows, then write the rows out in the other format. A correlated subquery with its own FOR JSON or FOR XML keeps child rows nested inside their parent.
XML to JSON
DECLARE @xml XML = N'
<orders>
<order id="1001">
<customer>Acme Ltd</customer>
<lines>
<line sku="A1" qty="2"/>
<line sku="B2" qty="1"/>
</lines>
</order>
</orders>';
SELECT
o.n.value('@id', 'INT') AS id,
o.n.value('(customer/text())[1]', 'NVARCHAR(200)') AS customer,
(
SELECT
l.n.value('@sku', 'NVARCHAR(50)') AS sku,
l.n.value('@qty', 'INT') AS qty
FROM o.n.nodes('lines/line') AS l(n)
FOR JSON PATH
) AS lines
FROM @xml.nodes('/orders/order') AS o(n)
FOR JSON PATH, ROOT('orders');
{"orders":[{"id":1001,"customer":"Acme Ltd","lines":[{"sku":"A1","qty":2},{"sku":"B2","qty":1}]}]}
Because the values are read with types, id and qty come out as JSON numbers, and every order’s lines is an array even when it has one line. A dot in a column alias, such as [customer.name], nests the value in an object.
JSON to XML
DECLARE @json NVARCHAR(MAX) = N'{
"orders": [
{ "id": 1001, "customer": "Acme Ltd",
"lines": [ { "sku": "A1", "qty": 2 }, { "sku": "B2", "qty": 1 } ] }
]
}';
SELECT
o.id AS [@id],
o.customer AS [customer],
(
SELECT l.sku AS [@sku], l.qty AS [@qty]
FROM OPENJSON(o.lines) WITH (sku NVARCHAR(50), qty INT) AS l
FOR XML PATH('line'), TYPE
) AS [lines]
FROM OPENJSON(@json, '$.orders')
WITH (
id INT,
customer NVARCHAR(200),
lines NVARCHAR(MAX) AS JSON
) AS o
FOR XML PATH('order'), ROOT('orders');
<orders><order id="1001"><customer>Acme Ltd</customer><lines><line sku="A1" qty="2"/><line sku="B2" qty="1"/></lines></order></orders>
In FOR XML PATH, an alias starting with @ becomes an attribute and any other alias becomes a child element. TYPE returns the inner result as XML, so it’s nested rather than escaped as text.
Notes
- You choose the shape. Unlike a generic converter, these queries only output the fields you name, with the names and types the receiving system expects. That is usually what an integration wants anyway.
- NULLs are left out.
FOR JSONomitsNULLvalues unless you addINCLUDE_NULL_VALUES, andFOR XML PATHomitsNULLelements unless you addELEMENTS XSINIL. An order with no lines has nolinesproperty at all. - Keep the XML as text inside JSON: to send a whole XML document as one JSON string, select
CAST(@xml AS NVARCHAR(MAX)).FOR JSONescapes the quotes for you. - Without
TYPE, a nestedFOR XMLcomes back as a string and its<and>are escaped to<and>in the outer document. - Unknown or changing shapes are hard to convert in SQL, because the columns have to be named in the query. For a one-off, or to see what a document looks like as JSON before writing the query, use the XML to JSON Converter, then JSON to OPENJSON to generate the query that reads it.
- For the XML side in more detail, see Turn XML Into Rows with nodes(). For the JSON side, see Pass a List of IDs Without Dynamic SQL.
Was this useful?
Thanks. What was the problem?