I would like to use XSLT to transform some XML into JSON.
The XML looks like the following:
<DATA_DS>
<G_1>
<ORGANIZATION_NAME>My Company 1</ORGANIZATION_NAME>
<ORGANIZATIONID>901</ORGANIZATIONID>
<ITEMNUMBER>20001</ITEMNUMBER>
<ITEMDESCRIPTION>Item Description 1</ITEMDESCRIPTION>
</G_1>
<G_1>
<ORGANIZATION_NAME>My Company 1</ORGANIZATION_NAME>
<ORGANIZATIONID>901</ORGANIZATIONID>
<ITEMNUMBER>20002</ITEMNUMBER>
<ITEMDESCRIPTION>Item Description 2</ITEMDESCRIPTION>
</G_1>
<G_1>
<ORGANIZATION_NAME>My Company 1</ORGANIZATION_NAME>
<ORGANIZATIONID>901</ORGANIZATIONID>
<ITEMNUMBER>20003</ITEMNUMBER>
<ITEMDESCRIPTION>Item Description 3</ITEMDESCRIPTION>
</G_1>
</DATA_DS>
I expect the JSON to look like the following:
[
{
"Item_Number":"20001",
"Item_Description":"Item Description 1"
},
{
"Item_Number":"20002",
"Item_Description":"Item Description 2"
},
{
"Item_Number":"20003",
"Item_Description":"Item Description 3"
}
]
What is the recommended way to do this?
I am considering two approaches:
Try using the fn:xml-to-json function, as defined at https://www.w3.org/TR/xpath-functions-31/#func-xml-to-json. But as I understand, the input XML must follow a specific format defined at: https://www.w3.org/TR/xpath-functions-31/schema-for-json.xsd. And I also need the field names in the output JSON to be specifically "Item_Number" and "Item_Description".
Manually code the bracket and brace characters, "[", "]", "{", and "}", along with the field names "Item_Number" and "Item_Description". Then use a standard function to list the values and ensure that any special characters are handled properly. For example, the "&" character should appear normally in the JSON output.
What is the recommended way to do this, or is there a better way that I have not considered?
I would take the first approach, but start with transforming the given input to the XML format expected by the xml-to-json() function. This could be something like:
XSLT 3.0
<xsl:stylesheet version="3.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns="http://www.w3.org/2005/xpath-functions">
<xsl:output method="text" encoding="UTF-8"/>
<xsl:template match="/G_1">
<!-- CONVERT INPUT TO XML FOR JSON -->
<xsl:variable name="xml">
<array>
<xsl:for-each-group select="*" group-starting-with="ORGANIZATION_NAME">
<map>
<string key="Item_Number">
<xsl:value-of select="current-group()[self::ITEMNUMBER]"/>
</string>
<string key="Item_Description">
<xsl:value-of select="current-group()[self::ITEMDESCRIPTION]"/>
</string>
</map>
</xsl:for-each-group>
</array>
</xsl:variable>
<!-- OUTPUT -->
<xsl:value-of select="xml-to-json($xml)"/>
</xsl:template>
</xsl:stylesheet>
Demo: https://xsltfiddle.liberty-development.net/bFWR5DQ
For simple mappings like that you can also directly construct XPath 3.1 arrays and maps i.e. in this case an array of maps:
<xsl:template match="DATA_DS">
<xsl:sequence select="array { G_1 ! map { 'Item_Number' : string(ITEMNUMBER), 'Item_Description' : string(ITEMDESCRIPTION) } }"/>
</xsl:template>
Then serialize as JSON with <xsl:output method="json" indent="yes"/>: https://xsltfiddle.liberty-development.net/ejivdGS
The main disadvantage is that maps have no order so you can't control the order of the items in a map, for instance for that example and the used Saxon version Item_Description is output before Item_Number.
But in general transforming to the format for xml-to-json provides more flexibility and also allows you to control the order as the function preserves the order in the XML representation of JSON.
This is the result of taking the solution posted by michael.hor257k and applying it to my revised input XML:
<xsl:stylesheet version="3.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns="http://www.w3.org/2005/xpath-functions">
<xsl:output method="text" encoding="UTF-8"/>
<xsl:template match="/DATA_DS">
<!-- CONVERT INPUT TO XML FOR JSON -->
<xsl:variable name="xml">
<array>
<xsl:for-each select="G_1">
<map>
<string key="Item_Number">
<xsl:value-of select="ITEMNUMBER"/>
</string>
<string key="Item_Description">
<xsl:value-of select="ITEMDESCRIPTION"/>
</string>
</map>
</xsl:for-each>
</array>
</xsl:variable>
<!-- OUTPUT -->
<xsl:value-of select="xml-to-json($xml)"/>
</xsl:template>
</xsl:stylesheet>
Related
I am trying to convert XML to json using XSLT 3.0, lowercasing all keys and moving the first attribute, if any, as a JSON child. So given following (dummy) input XML:
<FOO id="1">
<BAR xy="2">
<SPAM>N</SPAM>
</BAR>
</FOO>
I am expecting
{
"foo" : {
"id" : "1",
"bar" : {
"xy" : "2",
"spam" : "N"
}
}
}
Using this 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>
I get (note the dummy pattern to skip json conversion),
<map xmlns="http://www.w3.org/2005/xpath-functions" key="foo">
<map key="bar">
<string key="spam">N</string>
</map>
</map>
Looks good to me but when I invoke the JSON conversion (by replacing dummy with /), I get:
{ "bar" :
{ "spam" : "N" } }
-> the foo node is gone.
I haven't figured out how to "move" the first attribute (arbitrary, could have any name) as a child node - if someone knows, appreciate a little snippet.
Lastly, not a big-deal but I am lowercasing keys in each template. Is it possible to do the transformation at once, either before in the source XML, or after templating, in the Result XML (before jsonifying) ?
See - https://xsltfiddle.liberty-development.net/jyfAiDC/2
(thanks btw to #Martin Honnen for this very useful tool !!)
You can use
<xsl:template match="/">
<xsl:variable name="json-xml">
<map>
<xsl:apply-templates/>
</map>
</xsl:variable>
<xsl:value-of select="xml-to-json($json-xml, map { 'indent' : true() })"/>
</xsl:template>
perhaps to get closer to what you might want. But I haven't understood what you want to do with attributes in the XML.
The key attribute only make sense for an element that represents an entry in a map, so there's no point including it in an element unless that element has a parent named map. You want another layer of map in your structure.
Thanks to Martin & Michael for the map wrapper tip. Agreed though it is an unnecessary level in tree.
For rendering a XML attribute as a child node - assuming there is one only -,
I added the following after the first test (conditional map attribute) in the template:
<xsl:if test="#*[1]">
<string key="{name(#*[1])}">{#*[1]}</string>
</xsl:if>
Lastly, for converting to lowercase all intermediary key attributes in one-go instead of individually in multiple templates, it would require I think parsing the result tree before passing on to the xml-to-json function.
Not worth it... but it would be a nice featured option in XSLT 4.0 (?) i.e. a new xml-to-json option force-key-case = lower/upper
I am trying to parse nested JSON to CSV, using XSLT transformation.
In this particular case each child object counting from "datasheet", e.g. "result-sheet" and "balance-sheet", should end up in one CSV file (output) each. Currently I am however just elaborating getting out "result-sheet" only.
I noticed that the content of arrays are getting merged togehter.
Data:
<data>
{
"datasheets": {
"result-sheet": {"bank": [1,3], "credit": [2,6]},
"balance-sheet": {"loans": [4,5], "inventory": [9,0]}
}
}
</data>
XSL:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:map="http://www.w3.org/2005/xpath-functions/map"
xmlns:array="http://www.w3.org/2005/xpath-functions/array"
>
<xsl:output method="text" indent="yes"/>
<xsl:mode on-no-match="shallow-skip"/>
<!-- Parse JSON to XML -->
<xsl:template match="data">
<xsl:apply-templates select="json-to-xml(.)"/>
</xsl:template>
<xsl:template match="*">
<h2>Result sheet</h2>
<xsl:text>
</xsl:text>
<xsl:value-of select="*/(string-join(head(*)/*/#key, ','), *!string-join(*, ','))" separator="
"/>
</xsl:template>
</xsl:stylesheet>
Result:
Result sheet
bank,credit
13,26
45,90
Wanted result:
bank,credit
1, 2,
3, 6
I don't quite understand which data you want to have in each line, the following templates creates a line using for-each-pair on each pair of fn:number elements in the two fn:array children of the fn:map with the #key being result-sheet:
<xsl:template match="*:map[#key = 'result-sheet']">
<xsl:value-of select="for-each-pair(*:array[1]/*:number, *:array[2]/*:number, function($n1, $n2) { $n1 || ', ' || $n2 })"
separator="
"/>
</xsl:template>
I want to convert JSON to XML using XSLT. But not able to achieve the expected output.
Below is the JSON request:
{
"Store": [
{
"Book": "Cartoons",
"ID": "ABC"
}
]
}
The XSLT which I tried:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:math="http://www.w3.org/2005/xpath-functions/math"
xmlns:xd="http://www.oxygenxml.com/ns/doc/xsl"
xmlns:emp="http://www.semanticalllc.com/ns/employees#"
xmlns:h="http://www.w3.org/1999/xhtml"
xmlns:fn="http://www.w3.org/2005/xpath-functions"
xmlns:j="http://www.w3.org/2005/xpath-functions"
exclude-result-prefixes="xs math xd h emp"
version="3.0"
expand-text="yes">
<xsl:template match="/">
<xsl:copy>
<xsl:apply-templates select="json-to-xml(.)/*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="*[#key]" xpath-default-namespace="http://www.w3.org/2005/xpath-functions">
<xsl:element name="{#key}">
<xsl:apply-templates/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
But I'm getting empty Response.
You need to pass the JSON as a parameter or read it from a file, the input to your XSLT is either XML or you can start with a named template:
<?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"
exclude-result-prefixes="#all"
version="3.0">
<xsl:param name="json" as="xs:string" expand-text="no">{
"Store": [
{
"Book": "Cartoons",
"ID": "ABC"
}
]
}</xsl:param>
<xsl:output indent="yes"/>
<xsl:template match="/" name="xsl:initial-template">
<xsl:sequence select="json-to-xml($json)"/>
</xsl:template>
</xsl:stylesheet>
https://xsltfiddle.liberty-development.net/93wniTJ
The result of the function for your sample is
<map xmlns="http://www.w3.org/2005/xpath-functions">
<array key="Store">
<map>
<string key="Book">Cartoons</string>
<string key="ID">ABC</string>
</map>
</array>
</map>
but of course you can run it through further templates to transform it to a different format.
The pattern match="/" matches a document node (the root of an XML tree). It won't match your input if the input is JSON.
XSLT 3.0 isn't actually that good at processing JSON using template rules: it can be done, but it isn't very convenient. It's usually more convenient to use functions. You can supply the JSON input as the value of an xsl:param and process it in code from your xsl:initial-template; or if you're feeling more ambitious you could actually initiate the XSLT processing by invoking a public xsl:function that takes the JSON input as a parameter.
The traditional match="/" entry to a stylesheet only works for the traditional use of XSLT to process an XML input document.
I am processing various XML files with XSLT. In one XML I found a wrapped JSON list:
<list><![CDATA[[
{
"title": "Title 1",
"value": "Value 1",
"order": 1
},
{
"title": "Title 2",
"value": "Value 2",
"order": 2
}
]]]>
</list>
My problem is that I need to iterate over the list. For example:
<xsl:variable name="listVar">
<!-- do something with list -->
</xsl:variable>
<xsl:for-each select="$listVar">
<!-- do something with objects in list e.g. -->
<xsl:value-of select="title"/>
<xsl:value-of select="value"/>
</xsl:for-each>
How to do this with XSLT? I use XSLT 3.0 and Saxon engine, version 9.8 HE.
Considered solutions:
1.
Use parse-json function:
But then I cannot iterate over the result because of XPathException: "Required item type of the context item for the child axis is node(); supplied value (.) has item type array(function(*))" or "Maps cannot be atomized". I found that there are
functions that probably I should take into account like map:get, map:entry, but I've failed to use them in my case so far.
2.
Addidiotnal transform before the one mentioned above:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="3.0">
<xsl:output method="xml" encoding="UTF-8" indent="no"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="list">
<list>
<xsl:copy-of select="json-to-xml(.)"/>
</list>
</xsl:template>
</xsl:stylesheet>
And then:
<xsl:variable name="listVar" select="list/array/map"/>
But it does not work - probably due to added namespace
<list>
<array xmlns="http://www.w3.org/2005/xpath-functions">
<map>
...
Your JSON structure when parsed with parse-json gives you on the XSLT/XPath side an array of maps and the most straightforward way to process the individual array items is with the ?* lookup operator, then you can use for-each or even apply-templates:
<?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:math="http://www.w3.org/2005/xpath-functions/math"
exclude-result-prefixes="xs math"
version="3.0">
<xsl:template match="list">
<xsl:apply-templates select="parse-json(.)?*"/>
</xsl:template>
<xsl:template match=".[. instance of map(xs:string, item())]">
<xsl:value-of select="?title, ?value"/>
</xsl:template>
</xsl:stylesheet>
where to access the map values you can again use ?foo as shown above.
As for working with the XML returned by json-to-xml, it returns elements in the XPath function namespace so to select them, as with any other elements in a namespace you need to make sure you set up a namespace with e.g. xpath-default-namespace for the section you want to process elements from that namespace or you can use the namespace wild card e.g. *:array/*:map.
I'm trying to convert some existing XML files to a JSON structure using XSL. Depending on their values, I need to put some elements into an JSON Array. To Produce a JSON String with correct syntax, I need to dynamically decide whether to put a comma behind (or in front) an object, since JSON doesn't allow trailing commas in arrays.
The XML files look somewhat like the following:
<list>
<type_a>
<active>0</active>
<data_a>don't put me to JSON...</data_a>
</type_a>
<type_b>
<active>1</active>
<data_b>put me to JSON</data_b>
</type_b>
<type_c>
<active>1</active>
<data_c>me too...</data_c>
</type_c>
</list>
The XSL to convert this looks like the following:
<xsl:template match="/list">
[
<xsl:if test="type_a/active != 0">
{ "type": "type_a",
"data": <xsl:value-of select="type_a/data_a" /> }
</xsl:if>
<xsl:if test="type_b/active != 0">
{ "type": "type_b",
"data": <xsl:value-of select="type_b/data_b" /> }
</xsl:if>
<xsl:if test="type_c/active != 0">
{ "type": "type_c",
"data": <xsl:value-of select="type_c/data_c" /> }
</xsl:if>
]
</xsl:template>
The Problem is, that I need to put commas between the different { } objects, but not after the last active one. The only two solutions I see are, either to check if any of the preceding objects where active, before putting an object (<xsl-if test="type_a/active != 0 or type_b/active != 0">, </xsl-if>; in front of the third object) or to transfer the XML into some, less odd, intermediate XML first. Particular the first option would be extremely ugly, since in reality I have to check for far more than 3 object types. The XML Format is produced by some legacy application and can't be changed.
Should I expect any further trouble because of using XSL to transfer the XML Structure to some none XML output?
One way is to check the preceding sibling to see if it is exists and is active and if so insert a ,. I have edited your template to reflect this, but I would write a function to just pass the node to, to make you life easier instead of repeating the template for each sibling.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:func="http://example.com/function"
exclude-result-prefixes="xs"
version="2.0">
<xsl:output encoding="UTF-8" indent="yes" method="xml"/>
<xsl:template match="/list">
[
<xsl:sequence select="func:processing(type_a)"/>
<xsl:sequence select="func:processing(type_b)"/>
<xsl:sequence select="func:processing(type_c)"/>
]
</xsl:template>
<xsl:function name="func:processing">
<xsl:param name="type"/>
<xsl:for-each select="$type">
<xsl:if test="active != 0">
{ "type": <xsl:value-of select="local-name()" />,
"data": <xsl:value-of select="data_a" /> }
<xsl:if test="following-sibling::*[1] and following-sibling::*[1]/active!=0">
,
</xsl:if>
</xsl:if>
</xsl:for-each>
</xsl:function>
</xsl:stylesheet>