Blog Post

Azure SQL Blog
5 MIN READ

Announcing JSON Enhancements in Azure SQL Database, Azure SQL Managed Instance and SQL Server 2022

uc-msft's avatar
uc-msft
Icon for Microsoft rankMicrosoft
May 24, 2022

Azure SQL Database, Azure SQL Managed Instance and SQL Server currently support JSON text processing in T-SQL batches. This functionality allows you to store and manipulate JSON documents in the database. The JSON functions - ISJSON, JSON_VALUE, JSON_QUERY, JSON_MODIFY allows you to validate, query and modify JSON documents. The FOR JSON operator allows you to transform SQL data into a JSON string. The OPENJSON operator allows you to transform JSON string into a relational dataset.

 

Today, we are announcing the public preview of JSON enhancements in Azure SQL Database, Azure SQL Managed Instance and SQL Server 2022 CTP 2.0. This preview contains an enhancement to ISJSON function and three new JSON functions – JSON_PATH_EXISTS, JSON_OBJECT and JSON_ARRAY. Currently, the ISJSON function allows you to test if a string value contains a valid JSON object or array. The new optional json_type_constraint parameter in ISJSON function can now be used to test conformance of JSON documents to the IETF RFC 8259 specification. This capability allows you to test for strings that contain a JSON value, scalar, object, or array. This functionality is like the IS JSON predicate in the ANSI SQL standard. The new JSON_PATH_EXISTS function allows you to test for the existence of a specific SQL/JSON path expression in a JSON document. This functionality is like the JSON_EXISTS predicate in the ANSI SQL standard. The new ANSI SQL compatible JSON value constructors - JSON_OBJECT and JSON_ARRAY functions allow you to construct JSON object or array from SQL data.

JSON functionality overview

Now, let us look at how the new JSON functionality can be used. The ISJSON function currently tests if a string is a valid JSON object or array. This function is typically used in a CHECK constraint on a column. Consider a SQL table that stores order details from an order processing system as a JSON string or document. The schema of the table is shown below:

 

CREATE TABLE new_orders (
 order_id int IDENTITY NOT NULL PRIMARY KEY,
 order_details nvarchar(4000) NOT NULL CHECK (ISJSON (order_details) = 1)
);

 

The ISJSON condition in the CHECK constraint ensures that the input string contains a valid JSON object or array. However, if the application developer wants to enforce the additional requirement that the JSON string should be a JSON array then it is not possible to use ISJSON directly since it tests for both JSON object and array. The new json_type_constraint parameter can now be used to ensure that the application only inserts the order details as a JSON array. The table definition can be modified as shown below:

 

CREATE TABLE new_orders (
 order_id int IDENTITY NOT NULL PRIMARY KEY,
 order_details nvarchar(4000) NOT NULL CHECK (ISJSON (order_details, ARRAY) = 1)
);

 

The new JSON_PATH_EXISTS function can be used to augment the CHECK constraint to test for existence of specific SQL/JSON paths in the JSON documents. The table definition can be modified as shown below to test for specific JSON path:

 

CREATE TABLE new_orders (
 order_id int IDENTITY NOT NULL PRIMARY KEY,
 order_details nvarchar(4000) NOT NULL
  CHECK (ISJSON (order_details, ARRAY) = 1 AND
         JSON_PATH_EXISTS(order_details, '$.OrderNumber') = 1)
);

 

Currently, the FOR JSON operator can be used to transform relational data into a JSON string. However, there are some limitations with the FOR JSON operator. For example, producing a nested JSON document from a query may require sub-queries or parameterizing the key name in a JSON object requires dynamic SQL or constructing JSON array with scalars is not possible. The ANSI SQL compatible JSON_OBJECT & JSON_ARRAY functions allow you to construct complex JSON documents using scalar functions in a query or procedural code. A sample query that constructs a JSON document with a JSON object containing a nested JSON array is shown below:

 

SELECT o.OrderNumber,
	JSON_OBJECT('Date':o.OrderTime, 'Price':o.Price, 'Quantity':o.Quantity, 
		    'AccountDetails':JSON_OBJECT('AccountNumber':o.AccountNumber, 
                        'PhoneNumbers':JSON_ARRAY(a.Phone1, a.Phone2, a.Phone3))) AS OrderDetails
  FROM Orders AS o
  JOIN Accounts AS a
    ON a.AccountNumber = o.AccountNumber;

 

This sample query generates a result set with 2 columns: one containing the value of the OrderNumber column and another column containing a string formatted as a JSON document. using the new JSON_OBJECT and JSON_ARRAY functions. The JSON document produced from a sample is shown below:

 

{"Date":"2022-05-24T08:01:00","Price":59.99,"Quantity":1,"AccountDetails":{"AccountNumber":"AW29825","PhoneNumbers":["(123)456-7890","(123)567-8901"]}}

 

The script below contains an end-to-end sample of the new JSON functionality:

 

USE tempdb;
-- Sample schema:
DROP TABLE IF EXISTS Accounts;
CREATE TABLE Accounts (
	AccountNumber varchar(10) NOT NULL PRIMARY KEY,
	Phone1 varchar(20) NULL,
	Phone2 varchar(20) NULL,
	Phone3 varchar(20) NULL
);
INSERT INTO Accounts (AccountNumber, Phone1, Phone2, Phone3)
VALUES('AW29825', '(123)456-7890', '(123)567-8901', NULL),
	('AW73565', '(234)0987-654', NULL, NULL);
DROP TABLE IF EXISTS Orders;
CREATE TABLE Orders (
	OrderNumber varchar(10) NOT NULL PRIMARY KEY,
	OrderTime datetime2 NOT NULL,
	AccountNumber varchar(10) NOT NULL,
	Price decimal(10, 2) NOT NULL,
	Quantity int NOT NULL
);

-- Input JSON document example:
DECLARE @json nvarchar(1000) = N'
[
    {
        "OrderNumber": "S043659",
        "Date":"2022-05-24T08:01:00",
        "AccountNumber":"AW29825",
        "Price":59.99,
        "Quantity":1
    },
    {
        "OrderNumber": "S043661",
        "Date":"2022-05-20T12:20:00",
        "AccountNumber":"AW73565",
        "Price":24.99,
        "Quantity":3
    }
]';
-- Test for valid JSON array and a specific SQL/JSON path:
SELECT ISJSON(@json, ARRAY) AS IsValidJSONArray, JSON_PATH_EXISTS(@json, '$[0].OrderNumber') AS OrderNumberExists;

-- Transform JSON string into relational data using OPENJSON operator:
INSERT INTO Orders (OrderNumber, OrderTime, AccountNumber, Price, Quantity)
  SELECT T.OrderNumber, T.OrderTime, T.AccountNumber, T.Price, T.Quantity
  FROM OPENJSON(@json)
		WITH(
			OrderNumber varchar(10) '$.OrderNumber',
			OrderTime datetime2 '$.Date',
			AccountNumber varchar(10) '$.AccountNumber',
			Price decimal(10, 2) '$.Price',
			Quantity int '$.Quantity'
			) AS T;

SELECT * FROM Orders;

-- Transform relational data into a JSON string using JSON_OBJECT function:
SELECT o.OrderNumber, JSON_OBJECT('Date':o.OrderTime, 'AccountNumber':o.AccountNumber, 'Price':o.Price, 'Quantity':o.Quantity) AS OrderDetails
  FROM Orders AS o;

-- Transform relational data into a JSON string:
-- Approach using the new JSON_OBJECT & JSON_ARRAY functions:
SELECT o.OrderNumber,
		JSON_OBJECT('Date':o.OrderTime, 'Price':o.Price, 'Quantity':o.Quantity, 
			'AccountDetails':JSON_OBJECT('AccountNumber':o.AccountNumber, 'PhoneNumbers':JSON_ARRAY(a.Phone1, a.Phone2, a.Phone3))) AS OrderDetails
  FROM Orders AS o
  JOIN Accounts AS a
    ON a.AccountNumber = o.AccountNumber;

-- One approach with the existing FOR JSON operator:
SELECT o.OrderNumber,
	(SELECT o.OrderTime as Date, o.Price, o.Quantity,
		(SELECT a.AccountNumber, JSON_QUERY(CONCAT('[',
			CONCAT_WS(',', QUOTENAME(a.Phone1, '"'),			
				QUOTENAME(a.Phone2, '"'),
				QUOTENAME(a.Phone3, '"')), ']')) AS PhoneNumbers
		    FOR JSON PATH) AS AccountDetails
	    FOR JSON PATH, WITHOUT_ARRAY_WRAPPER) AS OrderDetails
  FROM Orders AS o
  JOIN Accounts AS a
    ON a.AccountNumber = o.AccountNumber;

 

In summary, you have seen how to use the ISJSON enhancement and the new JSON functions JSON_PATH_EXISTS, JSON_OBJECT and JSON_ARRAY.  You can try these enhancements in SQL Server 2022 CTP 2.0. These enhancements will come soon to Azure SQL Database and Azure SQL Managed Instance. Let us know what you think about these JSON enhancements and other JSON capabilities you would like to see in the database engine.

Updated May 25, 2022
Version 5.0
  • NicolaBiada's avatar
    NicolaBiada
    Copper Contributor

    This is a great improvement, but I have a question: why I cannot use an implicit mapping using field names of both sides?

    Something like:

    INSERT INTO Person (id, name, surname, age, dateOfBirth)
     SELECT id, firstName, lastName, age, dateOfBirth 
     FROM OPENJSON(@json)
     WITH (id int,
           firstName nvarchar(50), lastName nvarchar(50), 
           age int, dateOfBirth datetime2)

    but without the unnecessary "WITH".

    And I need an option to skip or to throw an exception in case a field cannot be mapped.

    Something like:

    INSERT INTO Person (id, name, surname, age, dateOfBirth)
     SELECT id, firstName, lastName, age, dateOfBirth 
     FROM OPENJSON(@json, SKIP)
  • LenniLobel's avatar
    LenniLobel
    Copper Contributor

    Thanks for this.

     

    Can you please explain the SCALAR and VALUE type constraints added to the ISJSON function?

     

    I'm at a loss to see the use case for the new type constraint SCALAR on the ISJSON function. You don't show a sample, and I'm unable to figure out what kind of JSON string returns 1 (true) for type constraint SCALAR.

     

    I also don't see the value (no pun intended) of the new type constraint VALUE. It seems like ISJSON with the type constraint VALUE returns 1 (true) whether it's an array or an object, which is what ISJSON returns when no type constraint is specified at all (as per previous versions of SQL Server).

     

    Thanks again!

  • NicolaBiada Thanks for your feedback. Referencing JSON properties using a virtual column reference is on the list of improvements we are planning. This improvement will apply to OPENJSON operator too and remove need for WITH clause for most scenarios. As for skipping, we will probably make OPENJSON return NULL since this is current behavior in general for lax mode of SQL/JSON path expressions that does not map to a JSON item in a document.

  • LenniLobel Great question. The ISJSON function without the new type_constraint parameter test documents that conform to the RFC 4627: The application/json Media Type for JavaScript Object Notation (JSON) (rfc-editor.org) specification. In this specification a JSON document is defined as a JSON object or array.

    JSON-text = object / array

    Using ISJSON with the VALUE constraint tests for RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format (rfc-editor.org), where a JSON document is defined as:

    JSON-text = ws value ws
    value = false / null / true / object / array / number / string

    As for JSON scalar, it is either a number or string. Below is an example to illustrate the new SCALAR constraint:

    SELECT T.doc, ISJSON(T.doc, SCALAR) AS is_json_scalar, ISJSON(T.doc, VALUE) AS is_json_value
    FROM (VALUES('abc'), ('"test string"'), ('1.0'), ('1'), ('1E2'), ('false'), ('true'), ('null'), ('TRUE'), ('NULL'), ('"false"')) as T(doc);

    I am also pasting the table from my spec below:

     

    Input value with JSON string

    ISJSON expression

    Result

    Test string

    ISJSON( input_value )

    0

    Test string

    ISJSON( input_value, VALUE )

    0

    Test string

    ISJSON( input_value, ARRAY )

    0

    Test string

    ISJSON( input_value, OBJECT )

    0

    Test string

    ISJSON( input_value, SCALAR )

    0

    "Test string"

    ISJSON( input_value )

    0

    "Test string"

    ISJSON( input_value, VALUE )

    1

    "Test string"

    ISJSON( input_value, ARRAY )

    0

    "Test string"

    ISJSON( input_value, OBJECT )

    0

    "Test string"

    ISJSON( input_value, SCALAR )

    1

    true

    ISJSON( input_value )

    0

    true

    ISJSON( input_value, VALUE )

    1

    true

    ISJSON( input_value, SCALAR )

    0

    TRUE

    ISJSON( input_value )

    0

    TRUE

    ISJSON( input_value, VALUE )

    0

    TRUE

    ISJSON( input_value, SCALAR )

    0

    null

    ISJSON( input_value )

    0

    null

    ISJSON( input_value, VALUE )

    1

    null

    ISJSON( input_value, SCALAR )

    0

    Null

    ISJSON( input_value )

    0

    Null

    ISJSON( input_value, VALUE )

    0

    Null

    ISJSON( input_value, SCALAR )

    0

    123

    ISJSON( input_value )

    0

    123

    ISJSON( input_value, VALUE )

    1

    123

    ISJSON( input_value, SCALAR )

    1

    234.5

    ISJSON( input_value )

    0

    234.5

    ISJSON( input_value, VALUE )

    1

    234.5

    ISJSON( input_value, SCALAR )

    1

    6E7

    ISJSON( input_value )

    0

    6E7

    ISJSON( input_value, VALUE )

    1

    6E7

    ISJSON( input_value, SCALAR )

    1

    8E-2

    ISJSON( input_value )

    0

    8E-2

    ISJSON( input_value, VALUE )

    1

    8E-2

    ISJSON( input_value, SCALAR )

    1

    {}

    ISJSON( input_value )

    1

    {}

    ISJSON( input_value, VALUE )

    1

    {}

    ISJSON( input_value, OBJECT )

    1

    {}

    ISJSON( input_value, SCALAR )

    0

    {"k":"v"}

    ISJSON( input_value )

    1

    {"k":"v"}

    ISJSON( input_value, VALUE )

    1

    {"k":"v"}

    ISJSON( input_value, OBJECT )

    1

    {"k":"v"}

    ISJSON( input_value, SCALAR )

    0

    []

    ISJSON( input_value )

    1

    []

    ISJSON( input_value, VALUE )

    1

    []

    ISJSON( input_value, ARRAY )

    1

    []

    ISJSON( input_value, OBJECT )

    0

    []

    ISJSON( input_value, SCALAR )

    0

    [ 1, "v", {}, true, null ]

    ISJSON( input_value )

    1

    [ 1, "v", {}, true, null ]

    ISJSON( input_value, VALUE )

    1

    [ 1, "v", {}, true, null ]

    ISJSON( input_value, ARRAY )

    1

    [ 1, "v", {}, true, null ]

    ISJSON( input_value, OBJECT )

    0

    [ 1, "v", {}, true, null ]

    ISJSON( input_value, SCALAR )

    0

     

    To summarize, use ISJSON without type constraint to test documents for RFC 4627 and ISJSON with VALUE constraint for RFC 8259. Rest of the constraints are useful if you want to validate input before using JSON_OBJECT or JSON_ARRAY to construct new JSON documents or perform other conditional logic. Hope this helps clarify the behavior & syntax.

     

    Another tip, if you use Visual Studio Code and use JSON as the file type it will validate the file based on RFC 8259.

  • LuisGonzalezLT's avatar
    LuisGonzalezLT
    Copper Contributor

    "These enhancements will come soon to Azure SQL Database"

    Not available as of 08/01/2022. Any ETA?