Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I keep FOR JSON PATH from escaping query results?

I'm trying to write a fairly complicated SQL Query that produces JSON as the result. All is working great except for some hardcoded arrays I need to have deeper in the hierarchy that I must use UNION ALL to create. I've produced a query that shows my problem here (no data is required - I'm running this in Azure SQL Database):

SELECT
    'Hi' AS Greeting,
    (
        SELECT
            CASE WHEN DatePart(second, GetDate()) % 2 = 1 THEN
                'qwerty'
            ELSE
                'asdf'
            END AS Stuff
        FOR JSON PATH
    ) AS StuffArray,
    (
        CASE WHEN DatePart(second, GetDate()) % 2 = 1 THEN
        (
            SELECT 'qwerty' AS Stuff
            FOR JSON PATH
        )
        ELSE
        (
            SELECT 'asdf' AS Stuff
            FOR JSON PATH
        )
        END
    ) AS QuotedStuffArray,
    (
        CASE WHEN DatePart(second, GetDate()) % 2 = 1 THEN
        (
            SELECT * FROM
            (
                SELECT 'qwerty' AS Stuff
                UNION ALL
                SELECT 'zxcvb' AS Stuff
            ) AS SubSelect
            FOR JSON PATH
        )
        ELSE
        (
            SELECT 'asdf' AS Stuff
            FOR JSON PATH
        )
        END
    ) AS WhyItMatters,
    (
        SELECT * FROM
        (
            SELECT 'qwerty' AS Stuff
            UNION ALL
            SELECT 'zxcvb' AS Stuff
        ) AS SubSelect
        FOR JSON PATH
    ) AS ButThisIsFine
FOR JSON PATH

This outputs this JSON:

[
    {
        "Greeting": "Hi",
        "StuffArray": [
            {
                "Stuff": "qwerty"
            }
        ],
        "QuotedStuffArray": "[{\"Stuff\":\"qwerty\"}]",
        "WhyItMatters": "[{\"Stuff\":\"qwerty\"},{\"Stuff\":\"zxcvb\"}]",
        "ButThisIsFine": [
            {
                "Stuff": "qwerty"
            },
            {
                "Stuff": "zxcvb"
            }
        ]
    }
]

In this query, you'll see four different objects in the hierarchy beyond the base object: StuffArray, QuotedStuffArray, WhyItMatters, and ButThisIsFine. The StuffArray object is exactly what I want all of my objects to look like - pure JSON without anything escaped. However, when I begin to put my SELECT syntax inside of my CASE statements, my results begin to be quoted, as shown by the QuotedStuffArray object. So for the first two objects, this is fine. But I have a problem where I sometimes need to do a conditional UNION of two hardcoded values which forces me to put my SELECT into the CASE statement as shown by the WhyItMatters object. The ButThisIsFine object produces the output formated like I want the WhyItMatters object to be formatted but it removes the conditional UNION, which I need.

How can I get this last WhyItMatters object to produce pure JSON without escaped quotes just like the ButThisIsFine object while keeping in that conditional UNION statement?

like image 407
Jaxidian Avatar asked Aug 28 '17 20:08

Jaxidian


People also ask

Is it OK to store JSON in SQL database?

You can store JSON documents in SQL Server or SQL Database and query JSON data as in a NoSQL database.

What characters should be escaped in JSON?

In JSON the only characters you must escape are \, ", and control codes.

How do I get SQL output in JSON format?

Format query results as JSON, or export data from SQL Server as JSON, by adding the FOR JSON clause to a SELECT statement. Use the FOR JSON clause to simplify client applications by delegating the formatting of JSON output from the app to SQL Server.

How do I remove an escape character from a JSON string in Java?

On the receiving end, if you really want to, you could just do myJsonString = myJsonString. replaceAll("\\",""); But do note that those escape characters in no way make the JSON invalid or otherwise semantically different -- the '/' character can be optionally escaped with '\' in JSON. Save this answer.


2 Answers

There's some fascinating behavior going on in the optimizer for this query, and I'm not sure if it's a bug. The following query will not add escaping:

SELECT
    'Hi' AS Greeting,
    (
        CASE WHEN 1 = 1 THEN (
            SELECT * FROM (
                SELECT 'qwerty' AS [Stuff]
                UNION ALL
                SELECT 'zxcvb' AS [Stuff]
            ) _
            FOR JSON PATH
        ) ELSE (
            SELECT 'asdf' AS [Stuff]
            FOR JSON PATH
        )
        END
    ) AS WhyItMatters
FOR JSON PATH

The CASE can be optimized away, and it is optimized away, and the end result is nicely nested JSON. But if we remove the ability to optimize things away, it degenerates into pasting in an escaped string:

SELECT
    'Hi' AS Greeting,
    (
        CASE WHEN RAND() = 1 THEN (
            SELECT * FROM (
                SELECT 'qwerty' AS [Stuff]
                UNION ALL
                SELECT 'zxcvb' AS [Stuff]
            ) _
            FOR JSON PATH
        ) ELSE (
            SELECT 'asdf' AS [Stuff]
            FOR JSON PATH
        )
        END
    ) AS WhyItMatters
FOR JSON PATH

It seems illogical that one query would result in processing typed JSON and the other would not, but there you go. JSON is not an actual type in T-SQL (unlike XML), so we can't CAST or CONVERT, but JSON_QUERY will do roughly the same thing:

SELECT
    'Hi' AS Greeting,
    JSON_QUERY(
        CASE WHEN RAND() = 1 THEN (
            SELECT * FROM (
                SELECT 'qwerty' AS [Stuff]
                UNION ALL
                SELECT 'zxcvb' AS [Stuff]
            ) _
            FOR JSON PATH
        ) ELSE (
            SELECT 'asdf' AS [Stuff]
            FOR JSON PATH
        )
        END
    ) AS WhyItMatters
FOR JSON PATH

Note that this also works if the argument is already JSON (in the constant case), so it's safe to add regardless.

like image 119
Jeroen Mostert Avatar answered Nov 11 '22 18:11

Jeroen Mostert


I have found one possible solution but I really don't like it. I'm posting what I have in hopes that somebody has a better solution.

Using a WHERE statement on every branch of my UNION with either the affirmative or exact negative of my CASE statement can prevent the "strigifying" of my results.

For example, this query:

SELECT
    'Hi' AS Greeting,
    (
        SELECT * FROM
        (
            SELECT 'asdf' AS Stuff WHERE DatePart(second, GetDate()) % 2 = 0
            UNION ALL
            SELECT 'qwerty' AS Stuff WHERE DatePart(second, GetDate()) % 2 = 1
            UNION ALL
            SELECT 'zxcvb' AS Stuff WHERE DatePart(second, GetDate()) % 2 = 1
        ) AS SubSelect
        FOR JSON PATH
    ) AS Try1
FOR JSON PATH

provides these results:

[
    {
        "Greeting": "Hi",
        "Try1": [
            {
                "Stuff": "qwerty"
            },
            {
                "Stuff": "zxcvb"
            }
        ]
    }
]

If nothing better can be found, I can move forward with this. But this seems like a hacky way to control this.

like image 42
Jaxidian Avatar answered Nov 11 '22 18:11

Jaxidian