Converting XML to JSON with XSL – Part 1

A little while ago I was doing some playing around with Extjs and I found that the majority of the Ajax calls where expecting JSON back rather than XML. At this point I thought to myself ‘right then someone must have a converter out there in pure XSL. To my great surprise no one seems to either written one or made it public. So, based on this finding I think this will make an excellent, if long, first blog post.

The core idea for the converter is to use only the basic functionality provided in XSL. This means no library/namespace extensions. This will provide for the transformer to be portable across all major web platforms, such as JAVA, PHP, CFM and of course .NET.

For the purpose of this post I will use a simple XML file with some basic content as the example. I will lay out the creation of the transformer as I wrote it. I will use the JSON Badgerfish convention and later in the post show how to deviate from it and why. At the end of the post I will cover the introduction of a config.xml file to drive the transformations. The actual server side transformation will be covered lastly and will be in VB.NET and C#.

Source XML

The source xml I will use will be the following xml snippet.

<root>
   <book id=”780102497″>
     <title>Building a Better Web Site</title>
     <pricing releaseDate=”2007-07-25″>
          <sellPrice>120.00</sellPrice>
          <purchasePrice>95.49</purchasePrice>
          <sales>
               <item count=”52″ value=”120″ dtm=”2008-08-01″ />
               <item count=”104″ value=”180″ dtm=”2007-08-01″ />
          </sales>
     </pricing>
     <description>If you’re a developer working with XML, you know there’s a lot to know about XML, and the XML space is evolving almost moment by moment. But you don’t need to commit every XML syntax, API, or XSLT transformation to memory; you only need to know where to find it. Use the < & > values to define your xml nodes.</description> 
   </book>
<root>

The above source in JSON under the BadgerFish convention would look like the following:

var myJSON=
[{"root":{"book":{"id":"780102497","title":{"$":"Building a Better Web Site"},"pricing":{"releaseDate":"2007-07-25","sellPrice":{"$":120.00},"purchasePrice":{"$":95.49},"sales":{"item":[{"count":"52","value":120,"dtm":"2008-08-01"},{"count":"104","value":"180","dtm":"2007-08-01"}]}},”description”:{“$”:”If you’re a developer working with XML, you know there’s a lot to know about XML, and the XML space is evolving almost moment by moment. But you don’t need to commit every XML syntax, API, or XSLT transformation to memory; you only need to know where to find it. Use the < & > values to define your xml nodes.”}}}}]

With the above example we are able to to move to an element value such as the sellPrice with myJSON[0]['root']['book']['pricing']['sellPrice']['$'].  Similarly in xpath we would could have \root\book[1]\pricing\sellPrice. You can also get to the same value through javascript with myJSON[0].root.book.pricing.sellPrice.$

Step 1: Singleton or Array?

The first step in creating our JSON is to create a iterative template.  The template matches on * and has a mode to inforce recurssion, better safe than sorry I always say.  The first thing we have to figure out is if the current node is a singleton or array object structure.  In order to do this we will use counts on the preceding-sibling and following-sibling axis tests where node name is the same as current.

<xsl:template match=”/”>
   <xsl:variable name=”initial_JSON”>
      <xsl:apply-templates select=”current()/child::*” mode=”build”>
   </xsl:variable>
</xsl:template>
<xsl:template match=”*” mode=”build”>
   <!– name of current node –>
   <xsl:variable name=”nName” select=”name()”> 
   <!– count of preceding and following nodes with the same name as the current node –>
   <xsl:variable name=”iPreceding” select=”count(preceding-sibling::*[name()=#nName])”/>
   <xsl:variable name=”iFollowing” select=”count(following-sibling::*[name()=#nName])”/>
</xsl:template>

Based on $iPreceding and $iFollowing we will no if there are any other nodes with the same name.  If there are then we no that the current node must be encased within an array structure, [ {object instance 1}, {object instance 2}, {e.t.c. for all matching nodes on name...} ].  In order to do this we must now introduce a case statement to decide on how to process the current node.

 <xsl:template match=”*” mode=”build”>
    …preamble, see above
   <xsl:case>
      <xsl:when test=”$iPreceding = 0 and $iFollowing &gt; 0″>
         <!– this is the first instance of a node that has matching siblings below –>
      </xsl:when>
      <xsl:when test=”$iPreceding = 0 and $iFollowing = 0″>
         <!– this node has no siblings so it should be rendered as a singleton –>
      </xsl:when>
   <xsl:case>
</xsl:template>

I will deal with converting a singleton node to JSON as logically this is the same process as an array full of singleton objects.

Step 2: Converting a Node to JSON

 There are only ever 2 basic components to an xml node/element.  When dealing with a node there are only two things we have to deal with, properties and content/text.  To be clear I am looking at xml as XML not HTML. In other words the following is invalid:

<root>
   <mydata>
      some little bit of text
      <span style=”font-weight:bold”>that needs to be bolded</span>
      so that the user can see it clearly
   </mydata>
</root> 

Although the above may be valid in the HTML world, in the XML world it is NOT.  The content contained within the mydata node should be wrapped within a CDATA tag and escaped, but I digress.

As I was saying, we have two issues to deal with attributes and inner text of the element.  Lets deal with the properties first. In order to deal with the properties we need to create a further template with a matching xpath of @*.  

<xsl:template match=”@*” mode=”attributes”>
   <xsl:value-of select=”concat($encaseObject, name(), $encaseObject, $cln, $encaseString, text(), $encaseString)
   <xsl:if test=”position() != last()”>,</xsl:if>
</xsl:template> 

Notice in the above code snippet I have introduced a couple of variables which are defined as global parameters in the XSL file. The $encaseObject and $encaseString are set to ” and the $cln variable is set to :.  If we where to enumerate the item node in the source xml it would result in:

{“count”:”52″, “value”:”120″, “dtm”:”2008-08-01″}

The final test for last position tells us to add a comma or not to the output to seperate each attribute as a JSON property. Now we need to process the text contained within the node.  For arguments sake lets assume that the item node contains the text.  We create another template to process the text as required.

<xsl:template match=”*” mode=”elements”>
   <xsl:value-of select=”concat($encaseObject, $txtPrefix, $txtSuffix, $encaseObject, $cln, $encaseString, text(), $encaseString)
</xsl:template>

Again notice that I have introduced some further stylehsheet params, $txtPrefix and $txtSuffix .  These variables contain $ and blank respectively.  This meets the BadgerFish convention for JSON.

Now we need to place the calls to the attributes and elements templates in the master build template within the singleton case test.  The results are as follows:

…preamble
<xsl:when test=”$iPreceding = 0 and $iFollowing = 0″>
   <xsl:variable name=”properties”>
      <xsl:apply-templates select=”@” mode=”properties”/>
      <xsl:if test=”count(@*) != 0″ and string-length(text()) !=0″>,</xsl:if>
      <xsl:apply-templates select=”*” mode=”elements”/>
   </xsl:template>
   <xsl:value-of select=”concat($encaseObject, $nName, $encaseObject, $cln, ‘{‘, $properties)”/>
      <xsl:if test=”child::*”>
         <xsl:if test=”string-length($properties) != 0″>,</xsl:if>
         <xsl:apply-templates select=”current()/*” mode=”build”/>
      </xsl:if>
   <xsl:text>}</xsl:text>
   <xsl:if test=”following-sibling::*”>,</xsl:if>
</xsl:when>

Notice in the above example I have added a couple of tests and an iterative call to the template.  First within the properties variable I test if there where any properties on the node and it has content add in a comma to the output.

Finally return the results with the concat statement as “node name“:{$properties. Finally, if there are children of the current node and then call the same template iteratively but before hand add in a another comma to seperate the child object.  Then to close things out we add in a closing } bracket.  We then do an if to see if there are any more following siblings to the current node and if so add in a comma.

 Step 3: Dealing With Multiple Instances/Arrays

As mentioned earlier there are only two scenarios to deal with when parsing the XML to JSON, Singletons and Arrays.  In the previous step I discussed how to deal with a singleton.  The process of dealing with a ‘collection’ of nodes is almost exactly the same.  Below is the XSL:

…preamble
<xsl:when test=”$iPreceding = 0 and $iFollowing &gt; 0″>
   <xsl:value-of select=”concat($encaseObject, $nName, $encaseObject, $cln, ‘[')"/>
   <xsl:for-each select="../*[name() = $nName]“>
       <xsl:variable name=”properties”>
         <xsl:apply-templates select=”@” mode=”properties”/>
         <xsl:if test=”count(@*) != 0″ and string-length(text()) !=0″>,</xsl:if>
         <xsl:apply-templates select=”*” mode=”elements”/>
       </xsl:template>
       <xsl:value-of select=”concat($encaseObject, $nName, $encaseObject, $cln, ‘{‘, $properties)”/>
      <xsl:if test=”child::*”>
         <xsl:if test=”string-length($properties) != 0″>,</xsl:if>
         <xsl:apply-templates select=”current()/*” mode=”build”/>
      </xsl:if>
      <xsl:text>}</xsl:text>
      <xsl:if test=”position() != last()”>,</xsl:if>
   </xsl:for-each>
   <xsl:text>]</xsl:text>
   <xsl:if test=”following-sibling::*”>,</xsl:if>
</xsl:when>

As can be seen above the same basic logic is being used as the singleton but rather we are using an inner for-each to enumerate all of the nodes matching the name() xpath pattern.  Outside of the loop we encase it with [ followed by ] then again the test for node end of xml and add in a comma.

Step 4: Putting it all Together

 Finally we need to put togther the whole thing into a coherent xsl template which I have done below:

<?xml version=”1.0″ encoding=”utf-8″?>
<xsl:stylesheet version=”1.0″ xmlns:xsl=”http://www.w3.org/1999/XSL/Transform&#8221; xmlns:fn=”http://www.w3.org/2005/02/xpath-functions”&gt;
<xsl:output method=”text”/>
<xsl:param name=”encaseObject”>”</xsl:param>
<xsl:param name=”encaseString”>”</xsl:param>
<xsl:param name=”attPrefix”></xsl:param>
<xsl:param name=”attSuffix”></xsl:param>
<xsl:param name=”txtPrefix”>$</xsl:param>
<xsl:param name=”txtSuffix”></xsl:param>
<xsl:template match=”/”>
   <xsl:variable name=”initial_JSON”>
      <xsl:apply-templates select=”current()/child::*” mode=”build”>
   </xsl:variable>
   <xsl:value-of select=”concat(‘[',$initial_JSON,']‘)/>
</xsl:template>
<xsl:template match=”*” mode=”build”>
   <!– name of current node –>
   <xsl:variable name=”nName” select=”name()”>
   <!– count of preceding and following nodes with the same name as the current node –>
   <xsl:variable name=”iPreceding” select=”count(preceding-sibling::*[name()=#nName])”/>
   <xsl:variable name=”iFollowing” select=”count(following-sibling::*[name()=#nName])”/>
   <xsl:case>
      <xsl:when test=”$iPreceding = 0 and $iFollowing &gt; 0″>
         <xsl:value-of select=”concat($encaseObject, $nName, $encaseObject, $cln, ‘[')"/>
         <xsl:for-each select="../*[name() = $nName]“>
             <xsl:variable name=”properties”>
               <xsl:apply-templates select=”@” mode=”properties”/>
               <xsl:if test=”count(@*) != 0″ and string-length(text()) !=0″>,</xsl:if>
               <xsl:apply-templates select=”*” mode=”elements”/>
             </xsl:template>
             <xsl:value-of select=”concat($encaseObject, $nName, $encaseObject, $cln, ‘{‘, $properties)”/>
            <xsl:if test=”child::*”>
               <xsl:if test=”string-length($properties) != 0″>,</xsl:if>
               <xsl:apply-templates select=”current()/*” mode=”build”/>
            </xsl:if>
            <xsl:text>}</xsl:text>
            <xsl:if test=”position() != last()”>,</xsl:if>
         </xsl:for-each>
         <xsl:text>]</xsl:text>
         <xsl:if test=”following-sibling::*”>,</xsl:if>
      </xsl:when>
      <xsl:when test=”$iPreceding = 0 and $iFollowing = 0″>
         <xsl:variable name=”properties”>
            <xsl:apply-templates select=”@” mode=”properties”/>
            <xsl:if test=”count(@*) != 0″ and string-length(text()) !=0″>,</xsl:if>
            <xsl:apply-templates select=”*” mode=”elements”/>
         </xsl:template>
         <xsl:value-of select=”concat($encaseObject, $nName, $encaseObject, $cln, ‘{‘, $properties)”/>
            <xsl:if test=”child::*”>
               <xsl:if test=”string-length($properties) != 0″>,</xsl:if>
               <xsl:apply-templates select=”current()/*” mode=”build”/>
            </xsl:if>
         <xsl:text>}</xsl:text>
         <xsl:if test=”following-sibling::*”>,</xsl:if>
      </xsl:when>
   <xsl:case>
</xsl:template>
<xsl:template match=”@*” mode=”attributes”>
   <xsl:value-of select=”concat($encaseObject, name(), $encaseObject, $cln, $encaseString, text(), $encaseString)
   <xsl:if test=”position() != last()”>,</xsl:if>
</xsl:template> 
<xsl:template match=”*” mode=”elements”>
   <xsl:value-of select=”concat($encaseObject, $txtPrefix, $txtSuffix, $encaseObject, $cln, $encaseString, text(), $encaseString)
</xsl:template>
</xsl:stylesheet>

Finally

As you can see it is not that difficult to convert XML to basic JSON and in the next installment of this post I will take things a little further.  I will show how to deal with escaping characters and introduce a config.xml file to derive not only the style of implementation but also how to escape functions and datatype elements from XML direct to JSON.  Who wants a date as a string I may ask and next I will show you how to do it.

Until then…
Keith Chadwick

About these ads

One response to “Converting XML to JSON with XSL – Part 1

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s