0

I'm trying to use XSLT to change an XML to a JSON. It's actually a very simple schema but the XSLT I'm modify to use is more complicated than I need as I don't require pretty printing or arrays, besides the top level ones.

Source XML:

<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<r xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <GR CO="B" Date="2022-03-15" Zone="ASDF" Truck="1TR"></GR>
    <BS CO="A" Date="2022-03-14" Zone="ASDF" Truck="BT1"/>
    <GR CO="A" Date="2022-03-14" Zone="QWER" Truck="2TK"></GR>
</r>

All attributes would become a JSON string element following by a comma. Then append one at the end named Line with the value of the original element name. It would be nice to also sort by the Date attribute.

Pretty Print of Desired Output, but white space not needed:

{

    "Routes": [

        {
            "CO": "A",
            "Date": "2022-03-14",
            "Zone": "QWER",
            "Truck": "BT1",
            "Line": "BS"
        },
        {
            "CO": "A",
            "Date": "2022-03-14",
            "Zone": "ASDF",
            "Truck": "2TK",
            "Line": "GR"
        },
        {
            "CO": "B",
            "Date": "2022-03-15",
            "Zone": "ASDF",
            "Truck": "1TR",
            "Line": "GR"
        }

    ]
}

This is what I'm trying and I cannot make it work:

<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="text" encoding="utf-8"/>


    <xsl:template match="/r">
        <xsl:text>{"Routes": [</xsl:text>
        <xsl:apply-templates select="." mode="detect">
        <xsl:sort select="./@Date" />
        </xsl:apply-templates>
        <xsl:text>}</xsl:text>
    </xsl:template>

    <xsl:template match="@*" priority="1">
        <xsl:text>Found Attrib</xsl:text>
    </xsl:template>

    <xsl:template match="*" priority="2" mode="detect">
        <xsl:text>{</xsl:text>
        <xsl:choose>
            <xsl:when test="name(preceding-sibling::*[1]) = name(current()) and name(following-sibling::*[1]) != name(current())">
                <xsl:apply-templates select="." mode="obj-content" />
                <xsl:text>]</xsl:text>
                <xsl:if test="count(following-sibling::*[name() != name(current())]) &gt; 0">, </xsl:if>
            </xsl:when>
            <xsl:when test="name(preceding-sibling::*[1]) = name(current())">
                    <xsl:apply-templates select="." mode="obj-content" />
                    <xsl:if test="name(following-sibling::*) = name(current())">, </xsl:if>
            </xsl:when>
            <xsl:when test="following-sibling::*[1][name() = name(current())]">
                <xsl:text>"</xsl:text><xsl:value-of select="name()"/><xsl:text>" : [</xsl:text>
                    <xsl:apply-templates select="." mode="obj-content" /><xsl:text>, </xsl:text> 
            </xsl:when>
            <xsl:when test="count(./child::*) > 0 or count(@*) > 0">
                <xsl:text>"</xsl:text><xsl:value-of select="name()"/>" : <xsl:apply-templates select="." mode="obj-content" />
                <xsl:if test="count(following-sibling::*) &gt; 0">, </xsl:if>
            </xsl:when>
            <xsl:when test="count(./child::*) = 0">
                <xsl:text>"</xsl:text><xsl:value-of select="name()"/>" : "<xsl:apply-templates select="."/><xsl:text>"</xsl:text>
                <xsl:if test="count(following-sibling::*) &gt; 0">, </xsl:if>
            </xsl:when>
        </xsl:choose>
        <xsl:text>"Line": "</xsl:text><xsl:value-of select="local-name()"/><xsl:text>"
    </xsl:text>

    </xsl:template>

    <xsl:template match="*" mode="obj-content">
        <xsl:text>{</xsl:text>
            <xsl:apply-templates select="@*" mode="attr" />
            <xsl:if test="count(@*) &gt; 0 and (count(child::*) &gt; 0 or text())">, </xsl:if>
            <xsl:apply-templates select="./*" mode="detect" />
            <xsl:if test="count(child::*) = 0 and text() and not(@*)">
                <xsl:text>"</xsl:text><xsl:value-of select="name()"/>" : "<xsl:value-of select="text()"/><xsl:text>"</xsl:text>
            </xsl:if>
            <xsl:if test="count(child::*) = 0 and text() and @*">
                <xsl:text>"text" : "</xsl:text><xsl:value-of select="text()"/><xsl:text>"</xsl:text>
            </xsl:if>
        <xsl:text>}</xsl:text>
        <xsl:if test="position() &lt; last()">, </xsl:if>
    </xsl:template>
    
    <xsl:template match="@*" mode="attr">
        <xsl:text>"</xsl:text><xsl:value-of select="name()"/>" : "<xsl:value-of select="."/><xsl:text>"</xsl:text>
        <xsl:if test="position() &lt; last()">,</xsl:if>
    </xsl:template>

    <xsl:template match="node/@TEXT | text()" name="removeBreaks">
        <xsl:param name="pText" select="normalize-space(.)"/>
        <xsl:choose>
            <xsl:when test="not(contains($pText, '&#xA;'))"><xsl:copy-of select="$pText"/></xsl:when>
            <xsl:otherwise>
                <xsl:value-of select="concat(substring-before($pText, '&#xD;&#xA;'), ' ')"/>
                <xsl:call-template name="removeBreaks">
                    <xsl:with-param name="pText" select="substring-after($pText, '&#xD;&#xA;')"/>
                </xsl:call-template>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>
    
</xsl:stylesheet>

1 Answer 1

2

First, you don't say what version of XSLT you want to use, so here is an XSLT 3.0 solution:

<xsl:stylesheet version="3.0"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="json" indent="yes"/>
    <xsl:strip-space elements="*"/>
    <xsl:template match="/r">
        <xsl:map>
            <xsl:variable name="content" as="map(*)*">
                <xsl:apply-templates>
                   <xsl:sort select="xs:date(@Date)"/>
                </xsl:apply-templates>
            </xsl:variable>
            <xsl:map-entry key="'Routes'" select="array{$content}"/>
        </xsl:map>
    </xsl:template>
    <xsl:template match="r/*">
        <xsl:sequence select="map{
            'CO': string(@CO), 
            'Date': string(@Date), 
            'Zone': string(@Zone), 
            'Truck': string(@Truck), 
            'Line':local-name()}"/>
    </xsl:template>
</xsl:stylesheet>

With Saxon this produces the output

{ "Routes": [
    { "CO": "A","Zone": "ASDF","Date": "2022-03-14","Line": "BS","Truck": "BT1" },
    { "CO": "A","Zone": "QWER","Date": "2022-03-14","Line": "GR","Truck": "2TK" },
    { "CO": "B","Zone": "ASDF","Date": "2022-03-15","Line": "GR","Truck": "1TR" }
  ] }

It's hardly any more difficult with XSLT 1.0 and it's much easier than you have made it (I really haven't been able to follow your complex logic at all).

Try this:

<xsl:stylesheet version="1.0"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="text"/>
    <xsl:strip-space elements="*"/>
    <xsl:template match="/r">
        { "Routes": [
           <xsl:for-each select="*">
            <xsl:sort select="@Date"/>
            <xsl:if test="position() != 1">, </xsl:if>
            {"CO": "<xsl:value-of select="@CO"/>", 
            "Date": "<xsl:value-of select="@Date"/>",
            "Zone": "<xsl:value-of select="@Zone"/>",
            "Truck": "<xsl:value-of select="@Truck"/>", 
            "Line": "<xsl:value-of select="local-name()"/>"}            
           </xsl:for-each>
       ]}
    </xsl:template>

</xsl:stylesheet>

the output is:

    { "Routes": [
       
        {"CO": "A", 
        "Date": "2022-03-14",
        "Zone": "ASDF",
        "Truck": "BT1", 
        "Line": "BS"}           
       , 
        {"CO": "A", 
        "Date": "2022-03-14",
        "Zone": "QWER",
        "Truck": "2TK", 
        "Line": "GR"}           
       , 
        {"CO": "B", 
        "Date": "2022-03-15",
        "Zone": "ASDF",
        "Truck": "1TR", 
        "Line": "GR"}
]}          
       
Sign up to request clarification or add additional context in comments.

1 Comment

That's perfect, Michael. The complex logic came from a generic XML to JSON that tries to check if an element repeats, is the last element, among other things. It was over done and your solution is more targeted. Thank you for the help.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.