Как правильно экранировать завершающие обратные слэши при преобразовании XML в JSON с помощью SQL Server?
Я создал рекурсивную скалярную функцию SQL Server, которая преобразует XML данные в JSON строку. Функция хорошо работает в большинстве случаев, включая вложенные элементы и обработку массивов (с использованием атрибута json:Array).
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\\\\".
Для этого ввода:
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>
';
Я получаю этот вывод:
{"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
Проблема возникает из-за того, что вы применяете функцию REPLACE после STRING_ESCAPE, что приводит к двойному экранированию обратных слэшей. Функция STRING_ESCAPE уже правильно экранирует обратные слэши для JSON, поэтому дополнительная функция REPLACE не нужна и создает проблемы.
Вот как исправить вашу функцию:
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('"' + 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');
-- Заменяем только прямые слэши, оставляем обратные слэши как их экранирует STRING_ESCAPE
SET @m = REPLACE(@m, '\/', '/');
RETURN @m;
END;
Основные изменения:
- Удалена замена обратных слэшей: Устранена
REPLACE(STRING_ESCAPE(...), '\', '\\'), так как функцияSTRING_ESCAPEуже правильно обрабатывает экранирование обратных слэшей - Сохранена замена прямых слэшей: Оставлена
REPLACE(@m, '\/', '/')для обработки прямых слэшей, которые функцияSTRING_ESCAPEможет экранировать ненужно
Содержание
- Понимание проблемы
- Поведение функции STRING_ESCAPE
- Реализация решения
- Тестирование исправления
- Альтернативные подходы
- Лучшие практики преобразования XML в JSON
Понимание проблемы
Проблема заключается в двойном экранировании обратных слэшей при преобразовании 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-строк.
Реализация решения
Исправленная функция удаляет ненужную замену обратных слэшей, сохраняя при этом обработку прямых слэшей:
-- До (проблемный вариант):
STRING_ESCAPE(x.a.value('text()[1]', 'nvarchar(max)'), 'json'), '\', '\\')
-- После (исправленный вариант):
STRING_ESCAPE(x.a.value('text()[1]', 'nvarchar(max)'), 'json')
Это гарантирует, что:
- Функция
STRING_ESCAPEвыполняет все необходимые экранирования JSON один раз - Только прямые слэши обрабатываются вручную при необходимости
- Обратные слэши остаются правильно экранированными как
\\в конечном JSON
Тестирование исправления
С вашими тестовыми данными:
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>';
Исправленная функция теперь будет производить:
{"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. Пользовательская функция экранирования
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 для точного контроля:
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:
- Используйте
STRING_ESCAPEодин раз: Применяйте его непосредственно к текстовым значениям без дополнительного экранирования - Внимательно обрабатывайте особые случаи: Заменяйте символы вручную только в тех случаях, когда
STRING_ESCAPEне обрабатывает их должным образом - Тестируйте граничные случаи: Всегда тестируйте с завершающими обратными слэшами, кавычками и специальными символами
- Проверяйте вывод JSON: Используйте
ISJSON()для проверки, что ваши результаты являются допустимым JSON - Учитывайте производительность: Для больших наборов данных тестируйте разные подходы, чтобы найти наиболее эффективный метод
Ключевое понимание заключается в том, что функция STRING_ESCAPE предназначена для обработки всех необходимых экранирований JSON, поэтому дополнительное ручное экранирование обычно не требуется и может вызывать проблемы, такие как проблема двойного экранирования, с которой вы столкнулись.