In my article, Warehousing JSON Formatted Data in SQL Server 2016, we had a look at available T-SQL options for converting JSON data into rows and columns for the purposes of populating a SQL Server based data warehouse. The increased popularity of JSON in modern web applications may create a requirement for data teams to expose some of their data to client applications (i.e. reporting tools, web services etc.) in a JSON format. In this article we take a look at how such a requirement can be implemented by data teams using SQL Server 2016 FOR JSON clause
SQL Server to JSON Supported Data Types
Like many of the features in SQL Server, there are terms and conditions to using them and JSON is no different. Thus, it is important that we take note of the supported data types. SQL Server data stored in the following data types cannot be converted into JSON:
A breakdown of supported data types is shown in Table1
SQL Server Data Type | JSON Data Type |
---|---|
char, nchar, varchar, nvarchar, date, datetime, datetime2, time, datetimeoffset, uniqueidentifier, money | string |
int, bigint, float, decimal, numeric | number |
Bit | Boolean |
varbinary, binary, image, timestamp, rowversion | BASE64-encoded string |
FOR JSON T-SQL Clause
Although SQL Server’s support for XML allowed for graphical representation of the data via an editor (shown in Figure 1), attempting to view JSON data via an editor may be frustrating as JSON data is shown as an unformatted single row.
It is therefore advisable that whilst you teach yourself JSON in SQL Server that you find yourself a JSON editor. For the purposes of this discussion, I will be using JSONFormatter from curiousconcept.com. As can be seen in Figure 3, the JSON output from Figure 2 is now properly formatted.
There are two ways that relational results can be converted into JSON, namely, the AUTO and PATH options.
Convert Results Using AUTO Mode
This is the simplest way to convert relational data into a JSON format as all that you have to do is to add FOR JSON AUTO clause at the end of your SELECT statement. In this mode, the structure of the JSON output is determined by a combination of the order of columns in your SELECT statement as well as the tables that are referenced by the SELECT statement. Figure 4 shows a T-SQL statement that converts the results from our fictitious Fruit Sales data mart into JSON.
1234567SELECTsales.[Item Nr],sales.[Transaction Date],sales.[Fruit],sales.[Quantity],sales.[Customer],sales.[MOP],sales.[Account Number]FROM [selectSIFISOBlogs].[DIM].[FruitSales] salesFOR JSON AUTOFigure 4 The results of the above scripts are shown in below in which a single array (represented by square bracket) was returned with rows held as objects (represented by curly braces).
Figure 5 The sample script provided in Figure 4 does not fully demonstrate the role of column and table ordering in FOR JSON AUTO clause as only a single table was used. Figure 6 shows the revised script with a join to another fictitious customer lookup dimension that stores additional information regarding customers that have purchased fruits.
12345678910SELECTsales.[Item Nr],sales.[Transaction Date],sales.[Fruit],sales.[Quantity],sales.[Customer],sales.[MOP],sales.[Account Number],cust.[Name],cust.[DOB],cust.[Gender]FROM [selectSIFISOBlogs].[DIM].[FruitSales] salesLEFT JOIN [selectSIFISOBlogs].[DIM].[Customer] custON sales.[Customer] = cust.[Customer]FOR JSON AUTOFigure 6 The execution of the above script results into an output that is shown in Figure 7. You will now notice that another child array (with its own objects) labelled cust appears in the output. The child array represents the information retrieved from the customer dimension.
Figure 7 However, when we change the column order of the SELECT statement such that it begins with a column from the customer dimension as shown in Figure 8, we get a different output than the one in Figure 7 in that the child array is now based off the FruitSales dimension.
123456789SELECTcust.[Name],sales.[Item Nr],sales.[Transaction Date],sales.[Fruit],sales.[Quantity],sales.[Customer],sales.[MOP],sales.[Account Number] ,cust.[DOB],cust.[Gender]FROM [selectSIFISOBlogs].[DIM].[FruitSales] salesLEFT JOIN [selectSIFISOBlogs].[DIM].[Customer] custON sales.[Customer] = cust.[Customer]FOR JSON AUTOFigure 8
Figure 9 Therefore, although using the AUTO mode is convenient, it often returns an inconsistent output whose ordering is subject to change based on the column ordering in the SELECT statement. To ensure that your JSON results are consistent, you will have to make use of the PATH mode.
- Convert Results Using PATH Mode
The PATH mode can be used in two ways:
Without a dot syntax
With a dot syntax
When you are using it without a dot syntax, it works similar to the AUTO mode in that it will generate a JSON output based on the ordering of columns in your SELECT statement.
12345678910SELECTsales.[Item Nr],sales.[Transaction Date],sales.[Fruit],sales.[Quantity],sales.[Customer],sales.[MOP],sales.[Account Number],cust.[Name],cust.[DOB],cust.[Gender]FROM [selectSIFISOBlogs].[DIM].[FruitSales] salesLEFT JOIN [selectSIFISOBlogs].[DIM].[Customer] custON sales.[Customer] = cust.[Customer]FOR JSON PATHFigure 10 However, as it can be seen in Figure 11, the PATH mode doesn’t automatically group information from joined tables into child arrays. In fact, all columns from the two tables are shown in the same root level.
Figure 11 To organise the JSON output into child arrays, you will have to use the dot syntax as shown in Figure 12. The label before the dot represents the name of the object – in this case we have two objects named Sales and Cust.
12345678910111213SELECTsales.[Item Nr] AS [Sales.Item Nr],sales.[Transaction Date] AS [Sales.Transaction Date],sales.[Fruit] AS [Sales.Fruit],sales.[Quantity] AS [Sales.Quantity],sales.[Customer] AS [Sales.Customer],sales.[MOP] AS [Sales.MOP],sales.[Account Number] AS [Sales.Account Number],cust.[Name] AS [Cust.Name],cust.[DOB] AS [Cust.DOB],cust.[Gender] AS [Cust.Gender]FROM [selectSIFISOBlogs].[DIM].[FruitSales] salesLEFT JOIN [selectSIFISOBlogs].[DIM].[Customer] custON sales.[Customer] = cust.[Customer]FOR JSON PATHFigure 12 The execution results of Figure 12 are shown below.
Figure 13 You can still organise your JSON PATH output into child arrays by converting your script into a nested join as shown in Figure 14.
12345678910111213SELECT(SELECT cust.[Name],cust.[DOB],cust.[Gender]FROM [selectSIFISOBlogs].[DIM].[Customer] custWHERE cust.[Customer] = sales.[Customer]FOR JSON PATH) cust,sales.[Item Nr],sales.[Transaction Date],sales.[Fruit],sales.[Quantity],sales.[Customer],sales.[MOP],sales.[Account Number]FROM [selectSIFISOBlogs].[DIM].[FruitSales] salesFOR JSON PATHFigure 14
Figure 15 Options FOR JSON Clause
Both AUTO and PATH modes allow you to specify additional options such as ROOT, INCLUDE_NULL_VALUES and WITHOUT_ARRAY_WRAPPER.
ROOT
The ROOT option is used assign a label to the top-level array. Figure 16 shows the application of the ROOT option.
123456789101112SELECTsales.[Item Nr] AS [Sales.Item Nr],sales.[Transaction Date] AS [Sales.Transaction Date],sales.[Fruit] AS [Sales.Fruit],sales.[Quantity] AS [Sales.Quantity],sales.[Customer] AS [Sales.Customer],sales.[MOP] AS [Sales.MOP],sales.[Account Number] AS [Sales.Account Number],cust.[Name] AS [Cust.Name],cust.[DOB] AS [Cust.DOB],cust.[Gender] AS [Cust.Gender]FROM [selectSIFISOBlogs].[DIM].[FruitSales] salesLEFT JOIN [selectSIFISOBlogs].[DIM].[Customer] custON sales.[Customer] = cust.[Customer]FOR JSON PATH, ROOT ('TOP_LEVEL')Figure 16 As it can be seen in Figure 17, our main array is now titled TOP_LEVEL.
Figure 17 INCLUDE_NULL_VALUES
The default behaviour when using FOR JSON clause is that NULL values will not be included in your JSON output. This can be overridden by specifying the INCLUDE_NULL_VALUES option. To illustrate this point I have added a dummy fruit transaction in my SELECT statement as shown in Figure 18. You will notice that the last four values in my dummy transaction are NULL.
123456789SELECTsales.[Item Nr],sales.[Transaction Date],sales.[Fruit],sales.[Quantity],sales.[Customer],sales.[MOP],sales.[Account Number]FROM [selectSIFISOBlogs].[DIM].[FruitSales] salesUNIONSELECT 12,20990101,'Dummy Fruit',NULL,NULL,NULL,NULLFOR JSON AUTOFigure 18 The output of this statement is shown in Figure 19 in which the last 4 values that were NULL are not included in the JSON output.
Figure 19 To override this behaviour, you just need to include INCLUDE_NULL_VALUES option as shown in Figure 20.
123456789SELECTsales.[Item Nr],sales.[Transaction Date],sales.[Fruit],sales.[Quantity],sales.[Customer],sales.[MOP],sales.[Account Number]FROM [selectSIFISOBlogs].[DIM].[FruitSales] salesUNIONSELECT 12,20990101,'Dummy Fruit',NULL,NULL,NULL,NULLFOR JSON AUTO, INCLUDE_NULL_VALUESFigure 20 As it can now be seen in Figure 21, after including the INCLUDE_NULL_VALUES option, our JSON output includes NULL values.
Figure 21 WITHOUT_ARRAY_WRAPPER
By default every FOR JSON clause returns JSON data wrapped around square brackets – also known as, an array. There are instances whereby you want the square brackets excluded from the output because you may want to concatenate two or more JSON data. Figure 22 shows the application of the WITHOUT_ARRAY_WRAPPER option.
1234567SELECTsales.[Item Nr],sales.[Transaction Date],sales.[Fruit],sales.[Quantity],sales.[Customer],sales.[MOP],sales.[Account Number]FROM [selectSIFISOBlogs].[DIM].[FruitSales] salesFOR JSON AUTO, WITHOUT_ARRAY_WRAPPERFigure 22 The results of the above scripts are shown below.
Figure 23
Finally, you would have noticed in Figure 22 that I used a TOP clause to limit my selection into a single row. This is because when you use the WITHOUT_ARRAY_WRAPPER option against a dataset that has more than one row, you will run into JSON validation error (shown in below) caused by the missing square brackets. Thus, be careful in the way you use the WITHOUT_ARRAY_WRAPPER option as it may lead to unintended validation errors.
Figure 24 Downloads
Sample selectSIFISOBlogs database
Reference
Latest posts by Sifiso Ndlovu (see all)