НейроАгент

Исправление двойных экранированных обратных слэшей в XML в JSON в SQL Server

Узнайте, как исправлять двойные экранированные обратные слэши при преобразовании XML в JSON с помощью функции STRING_ESCAPE в SQL Server. Полное руководство с примерами кода и лучшими практиками.

Как правильно экранировать завершающие обратные слэши при преобразовании XML в JSON с помощью SQL Server?

Я создал рекурсивную скалярную функцию SQL Server, которая преобразует XML данные в JSON строку. Функция хорошо работает в большинстве случаев, включая вложенные элементы и обработку массивов (с использованием атрибута json:Array).

sql
CREATE OR ALTER FUNCTION dbo.XmlToJson(@XmlData xml)  
RETURNS nvarchar(max)
WITH RETURNS NULL ON NULL INPUT
AS  
BEGIN  
    DECLARE @m nvarchar(max);

    WITH XMLNAMESPACES (N'http://james.newtonking.com/projects/json' AS json)
    SELECT @m = '{' + STRING_AGG(
  '"' + STRING_ESCAPE(name, 'json') + '":' + value,
  ','
) + '}'
    FROM 
        (SELECT
             v.name,
             CONCAT(CASE WHEN COUNT(*) > 1 OR MAX(isArray) = 1 THEN '[' END,
                    STRING_AGG(ISNULL('"' + REPLACE(STRING_ESCAPE(x.a.value('text()[1]', 'nvarchar(max)'), 'json'), '\', '\\') + '"', dbo.XmlToJson(x.a.query('./*'))), ','),
                    CASE WHEN COUNT(*) > 1 OR MAX(isArray) = 1 THEN ']' END
                   ) AS value
         FROM @XmlData.nodes('./*') x(a)
         CROSS APPLY 
             (SELECT
                  x.a.value('local-name(.)', 'nvarchar(4000)') AS name,
                  x.a.value('xs:int(xs:boolean(@json:Array))', 'int') AS isArray) v
         GROUP BY
             v.name) grouped;

    SET @m = ISNULL(@m, 'null');
    SET @m = REPLACE(@m, '\/', '/');

    RETURN @m;
END;

Однако я столкнулся с проблемой экранирования обратных слэшей в текстовом содержимом. Конкретно, когда текстовое содержимое XML элемента заканчивается обратным слэшем, моя текущая логика приводит к дополнительному экранированию обратного слэша в итоговом JSON выводе.

Желаемый вывод для пути вроде C:\Books\Book1\Book1.pdf\ должен быть "C:\\Books\\Book1\\Book1.pdf\\". Мой текущий вывод produces "C:\\\\Books\\\\Book1\\\\Book1.pdf\\\\".

Для этого ввода:

sql
DECLARE @xml xml = N'<root>
    <Book>Book1</Book>
    <TransactionId  xmlns:json="http://james.newtonking.com/projects/json" json:Array="true">abc123</TransactionId>
    <Publisher>Amazon</Publisher>
    <Edition  xmlns:json="http://james.newtonking.com/projects/json" json:Array="true">
        <Name>Ed1</Name>
        <Color>Red</Color>
        <Price>100</Price>
        <file>C:\Books\Book1\Book1.pdf\</file>
    </Edition>
    <PublisherId>1</PublisherId>
    <UserId>1234</UserId>
    <Release />
</root>
';

Я получаю этот вывод:

json
{"Book":"Book1","Edition":[{"Color":"Red","file":"C:\\Books\\Book1\\Book1.pdf\\","Name":"Ed1","Price":"100"}],"Publisher":"Amazon","PublisherId":"1","Release":null,"TransactionId":["abc123"],"UserId":"1234"}

Проблема, кажется, возникает из-за конфликта между STRING_ESCAPE и ручным REPLACE, который я использую для обработки общих обратных слэшей внутри строки, и как это взаимодействует, когда символ находится в самом конце текста.

Я создал DB fiddle для справки: https://dbfiddle.uk/rUlklVK8

Однако я не могу воспроизвести ту же проблему, с которой я столкнулся на моем SQL Server.

Подробности:
Microsoft SQL Server 2019 (RTM-CU22-GDR) (KB5029378) - 15.0.4326.1 (X64)
Copyright © 2019 Microsoft Corporation
Developer Edition (64-bit) on Windows Server 2019 Standard 10.0 (Build 17763: ) (Hypervisor)

НейроАгент

Проблема возникает из-за того, что вы применяете функцию REPLACE после STRING_ESCAPE, что приводит к двойному экранированию обратных слэшей. Функция STRING_ESCAPE уже правильно экранирует обратные слэши для JSON, поэтому дополнительная функция REPLACE не нужна и создает проблемы.

Вот как исправить вашу функцию:

sql
CREATE OR ALTER FUNCTION dbo.XmlToJson(@XmlData xml)  
RETURNS nvarchar(max)
WITH RETURNS NULL ON NULL INPUT
AS  
BEGIN  
    DECLARE @m nvarchar(max);

    WITH XMLNAMESPACES (N'http://james.newtonking.com/projects/json' AS json)
    SELECT @m = '{' + STRING_AGG(
  '&quot;' + STRING_ESCAPE(name, 'json') + '&quot;:' + value,
  ','
) + '}'
    FROM 
        (SELECT
             v.name,
             CONCAT(CASE WHEN COUNT(*) > 1 OR MAX(isArray) = 1 THEN '[' END,
                    STRING_AGG(ISNULL('&quot;' + STRING_ESCAPE(x.a.value('text()[1]', 'nvarchar(max)'), 'json') + '&quot;', dbo.XmlToJson(x.a.query('./*'))), ','),
                    CASE WHEN COUNT(*) > 1 OR MAX(isArray) = 1 THEN ']' END
                   ) AS value
         FROM @XmlData.nodes('./*') x(a)
         CROSS APPLY 
             (SELECT
                  x.a.value('local-name(.)', 'nvarchar(4000)') AS name,
                  x.a.value('xs:int(xs:boolean(@json:Array))', 'int') AS isArray) v
         GROUP BY
             v.name) grouped;

    SET @m = ISNULL(@m, 'null');
    -- Заменяем только прямые слэши, оставляем обратные слэши как их экранирует STRING_ESCAPE
    SET @m = REPLACE(@m, '\/', '/');

    RETURN @m;
END;

Основные изменения:

  1. Удалена замена обратных слэшей: Устранена REPLACE(STRING_ESCAPE(...), '\', '\\'), так как функция STRING_ESCAPE уже правильно обрабатывает экранирование обратных слэшей
  2. Сохранена замена прямых слэшей: Оставлена REPLACE(@m, '\/', '/') для обработки прямых слэшей, которые функция STRING_ESCAPE может экранировать ненужно

Содержание

Понимание проблемы

Проблема заключается в двойном экранировании обратных слэшей при преобразовании XML в JSON. Когда функция STRING_ESCAPE обрабатывает строку, содержащую обратные слэши, она правильно экранирует их в соответствии со стандартами JSON. Однако последующая функция REPLACE рассматривала эти уже экранированные обратные слэши как неэкранированные и экранировала их снова.

Например:

  • Исходное содержимое: C:\Books\Book1\Book1.pdf\
  • После STRING_ESCAPE: C:\\Books\\Book1\\Book1.pdf\\
  • После вашей REPLACE: C:\\\\Books\\\\Book1\\\\Book1.pdf\\\\ (двойное экранирование)

Это создает недопустимый JSON и не соответствует ожидаемому формату вывода.

Поведение функции STRING_ESCAPE

Согласно документации Microsoft, функция STRING_ESCAPE “экранирует специальные символы в тексте и возвращает текст с экранированными символами”. В частности для JSON:

  • Обратные слэши (\) экранируются как \\
  • Двойные кавычки (") экранируются как \"
  • Прямые слэши (/) экранируются как \/ (хотя это необязательно в JSON)
  • Управляющие символы правильно экранируются

Исследования из SQL Authority подтверждают, что функция STRING_ESCAPE обрабатывает все необходимые экранирования для JSON-строк.

Реализация решения

Исправленная функция удаляет ненужную замену обратных слэшей, сохраняя при этом обработку прямых слэшей:

sql
-- До (проблемный вариант):
STRING_ESCAPE(x.a.value('text()[1]', 'nvarchar(max)'), 'json'), '\', '\\')

-- После (исправленный вариант):
STRING_ESCAPE(x.a.value('text()[1]', 'nvarchar(max)'), 'json')

Это гарантирует, что:

  1. Функция STRING_ESCAPE выполняет все необходимые экранирования JSON один раз
  2. Только прямые слэши обрабатываются вручную при необходимости
  3. Обратные слэши остаются правильно экранированными как \\ в конечном JSON

Тестирование исправления

С вашими тестовыми данными:

sql
DECLARE @xml xml = N'<root>
    <Book>Book1</Book>
    <TransactionId  xmlns:json="http://james.newtonking.com/projects/json" json:Array="true">abc123</TransactionId>
    <Publisher>Amazon</Publisher>
    <Edition  xmlns:json="http://james.newtonking.com/projects/json" json:Array="true">
        <Name>Ed1</Name>
        <Color>Red</Color>
        <Price>100</Price>
        <file>C:\Books\Book1\Book1.pdf\</file>
    </Edition>
    <PublisherId>1</PublisherId>
    <UserId>1234</UserId>
    <Release />
</root>';

Исправленная функция теперь будет производить:

json
{"Book":"Book1","Edition":[{"Color":"Red","file":"C:\\Books\\Book1\\Book1.pdf\\","Name":"Ed1","Price":"100"}],"Publisher":"Amazon","PublisherId":"1","Release":null,"TransactionId":["abc123"],"UserId":"1234"}

Обратите внимание, что путь к файлу теперь отображается как "C:\\Books\\Book1\\Book1.pdf\\" с правильным однократным экранированием вместо двойного экранирования.

Альтернативные подходы

Если вам нужен больший контроль над процессом экранирования, рассмотрите эти альтернативы:

1. Пользовательская функция экранирования

sql
CREATE FUNCTION dbo.CustomJsonEscape(@String nvarchar(max))
RETURNS nvarchar(max)
AS
BEGIN
    -- Сначала применяем STRING_ESCAPE
    DECLARE @Escaped nvarchar(max) = STRING_ESCAPE(@String, 'json');
    
    -- Обрабатываем любые особые случаи, не охваченные STRING_ESCAPE
    -- Например, если нужно сохранить определенные escape-последовательности
    
    RETURN @Escaped;
END

2. Постобработка с помощью JSON_MODIFY

Если вы работаете с SQL Server 2016+, вы можете использовать JSON_MODIFY для точного контроля:

sql
SELECT JSON_MODIFY(
    '{"file":"C:\Books\Book1\Book1.pdf\"}',
    '$.file', 
    STRING_ESCAPE('C:\Books\Book1\Book1.pdf\', 'json')
) AS JsonResult;

Лучшие практики преобразования XML в JSON

При преобразовании XML в JSON в SQL Server:

  1. Используйте STRING_ESCAPE один раз: Применяйте его непосредственно к текстовым значениям без дополнительного экранирования
  2. Внимательно обрабатывайте особые случаи: Заменяйте символы вручную только в тех случаях, когда STRING_ESCAPE не обрабатывает их должным образом
  3. Тестируйте граничные случаи: Всегда тестируйте с завершающими обратными слэшами, кавычками и специальными символами
  4. Проверяйте вывод JSON: Используйте ISJSON() для проверки, что ваши результаты являются допустимым JSON
  5. Учитывайте производительность: Для больших наборов данных тестируйте разные подходы, чтобы найти наиболее эффективный метод

Ключевое понимание заключается в том, что функция STRING_ESCAPE предназначена для обработки всех необходимых экранирований JSON, поэтому дополнительное ручное экранирование обычно не требуется и может вызывать проблемы, такие как проблема двойного экранирования, с которой вы столкнулись.

Источники

  1. Microsoft Learn - STRING_ESCAPE (Transact-SQL)
  2. SQL Authority - SQL SERVER - STRING_ESCAPE() для JSON
  3. Database Administrators Stack Exchange - Правильное экранирование символов в строке для JSON
  4. Stack Overflow - STRING_ESCAPE json forward slash