XSLT3.0 XML转JSON:根节点丢失、属性转子节点及键名小写问题
XML转JSON的XSLT 3.0实现优化
需求
- 将所有JSON键名转为小写
- 若节点存在首个属性(属性名称不固定),需将其转换为JSON子节点
输入示例XML
<FOO id="1"> <BAR xy="2"> <SPAM>N</SPAM> </BAR> </FOO>
期望输出JSON
{ "foo" : { "id" : "1", "bar" : { "xy" : "2", "spam" : "N" } } }
当前代码及问题
当前使用的XSLT代码
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://www.w3.org/2005/xpath-functions" exclude-result-prefixes="#all" expand-text="yes" version="3.0"> <xsl:output method="xml" indent='true' omit-xml-declaration='yes'/> <xsl:template match="dummy"> <xsl:variable name="json-xml"> <xsl:apply-templates/> </xsl:variable> <xsl:value-of select="xml-to-json($json-xml, map { 'indent' : true() })"/> </xsl:template> <!--no children--> <xsl:template match="*[not(*)]"> <string key="{lower-case(local-name())}">{.}</string> </xsl:template> <xsl:template match="*[*]"> <xsl:param name="key" as="xs:boolean" select="true()"/> <map> <xsl:if test="$key"> <xsl:attribute name="key" select="lower-case(local-name())"/> </xsl:if> <xsl:for-each-group select="*" group-by="node-name()"> <xsl:choose> <xsl:when test="current-group()[2]"> <array key="{lower-case(local-name())}"> <xsl:apply-templates select="current-group()"> <xsl:with-param name="key" select="false()"/> </xsl:apply-templates> </array> </xsl:when> <xsl:otherwise> <xsl:apply-templates select="current-group()"> <xsl:with-param name="key" select="true()"/> </xsl:apply-templates> </xsl:otherwise> </xsl:choose> </xsl:for-each-group> </map> </xsl:template> </xsl:stylesheet>
遇到的问题
- 当将匹配模板改为
/触发转换时,结果丢失根节点foo,输出为:
{ "bar" : { "spam" : "N" } }
- 尚未实现将节点首个属性转为JSON子节点的逻辑
- 当前每个模板单独处理键名小写,希望能统一处理,避免重复代码
解决方案代码
以下是修复问题并优化后的XSLT代码:
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://www.w3.org/2005/xpath-functions" exclude-result-prefixes="#all" expand-text="yes" version="3.0"> <xsl:output method="text" indent="true"/> <!-- 根节点模板:生成包含根元素的JSON结构,避免丢失根节点 --> <xsl:template match="/"> <xsl:variable name="json-xml"> <map> <xsl:apply-templates select="*"/> </map> </xsl:variable> <xsl:value-of select="xml-to-json($json-xml, map { 'indent' : true() })"/> </xsl:template> <!-- 处理无子元素的节点 --> <xsl:template match="*[not(*)]"> <string key="{get-lowercase-name(.)}">{.}</string> <!-- 处理首个属性 --> <xsl:if test="@*[1]"> <string key="{lower-case(local-name(@*[1]))}">{@*[1]}</string> </xsl:if> </xsl:template> <!-- 处理有子元素的节点 --> <xsl:template match="*[*]"> <xsl:param name="key" as="xs:boolean" select="true()"/> <map> <xsl:if test="$key"> <xsl:attribute name="key" select="get-lowercase-name(.)"/> </xsl:if> <!-- 先处理首个属性 --> <xsl:if test="@*[1]"> <string key="{lower-case(local-name(@*[1]))}">{@*[1]}</string> </xsl:if> <!-- 处理子元素 --> <xsl:for-each-group select="*" group-by="node-name()"> <xsl:choose> <xsl:when test="current-group()[2]"> <array key="{get-lowercase-name(current-group()[1])}"> <xsl:apply-templates select="current-group()"> <xsl:with-param name="key" select="false()"/> </xsl:apply-templates> </array> </xsl:when> <xsl:otherwise> <xsl:apply-templates select="current-group()"> <xsl:with-param name="key" select="true()"/> </xsl:apply-templates> </xsl:otherwise> </xsl:choose> </xsl:for-each-group> </map> </xsl:template> <!-- 统一处理节点名小写的函数 --> <xsl:function name="get-lowercase-name" as="xs:string"> <xsl:param name="node" as="node()"/> <xsl:sequence select="lower-case(local-name($node))"/> </xsl:function> </xsl:stylesheet>
关键优化点说明
- 修复根节点丢失问题:根模板匹配
/后,手动生成一个外层map,将根元素作为子节点放入其中,确保根节点的键值对被保留。 - 实现属性转换逻辑:在两个元素模板中,添加
<xsl:if test="@*[1]">逻辑,将首个属性转为对应的string类型键值对,键名同样转为小写。 - 统一处理键名小写:定义
get-lowercase-name函数,所有需要转换节点名为小写的地方统一调用该函数,避免重复编写lower-case(local-name())代码,提升可维护性。
内容的提问来源于stack exchange,提问作者yan-hic
相关产品推荐
相关产品推荐

