I was interested in finding (or if necessary developing) an XSLT equivalent for JSON.
As I have not found any, I was considering the possible query language to use for matching JSON paths so as to apply templates (from JavaScript) when there was a match (probably just checking an array of matching patterns in order, and stopping at the first template which matches, though allowing for the equivalent of xsl:apply-templates to keep templates going for children).
I am aware of JSONPath, JSONQuery, and RQL as JSON query languages (though I was not fully clear on whether RQL supported absolute and relative paths). Any suggestions on factors to consider and relative advantages of each toward such a usage.
7
XML : XSLT :: JSON : x. What is x ?
The most facile answer would be x = JavaScript. Though you could make a case for this, it feels unsatisfying. Even though XSLT is technically Turing complete, there is a poor correspondence between the declarative style of XSLT and the more imperative or functional styles seen in JavaScript.
There are a few standalone JSON query languages, like JSONPath, JSONiq, and RQL which might stand in for the middle ground of XML : XPath :: JSON : y (or possibly, XQuery rather than XPath). And every JSON-focused document database has a JSON-related query language.
But the reality is, despite there being a few contenders for the full XSLT position, such as SpahQL, there are no generally accepted, broadly supported JSON equivalents to XSLT.
Why?
With all the JSON in the world, why isn’t there a (more direct) analog to XSLT? Because many developers view XSLT as a failed experiment. Any search engine will lead to to quotes like “XSLT is a failure wrapped in pain.” Others have argued that if it were just better formatted, it would be more popular. But interest in XSLT has generally diminished over the years. Many tools that support it support only version 1.0, which is a 1999 specification. Fifteen year old specs? There is a much newer 2.0 spec, and if people were enthusiastic about XSLT, it would be supported. It isn’t.
By and large developers have chosen to process and transform XML documents with code, not transformation templates. It is therefore unsurprising that when working with JSON, they would also by and large choose to do that in their native language, rather than adding an additional “foreign” transformation system.
3
While Jonathan largely talks about the nature of XSLT as a language in his answer, I think there’s another angle to consider.
The purpose of XSLT was to transform XML documents into some other document (XML, HTML, SGML, PDF, etc). In this way, XSLT is frequently used, effectively, as a template language.
There is a vast array of template libraries out there, even if you restrict yourself to JavaScript libraries (which you shouldn’t need to, since the JS in JSON only refers to the genesis of the notation and should not be taken to imply that JSON is only for JavaScript). This template engine selector gives and indication of the variety of JS options there are out there.
The latter half of your questions talks more about query languages and the XML version of these would be XPath (not XSLT). As you noted, there are a variety of options there and I don’t anything to add to that list. This area is relatively new, so I’d suggest you pick one and just go with it.
3
I recently created a library, json-transforms, exactly for this purpose:
https://github.com/ColinEberhardt/json-transforms
It uses a combination of JSPath, a DSL modelled on XPath, and a recursive pattern matching approach, inspired directly by XSLT.
Here’s a quick example. Given the following JSON object:
const json = {
"automobiles": [
{ "maker": "Nissan", "model": "Teana", "year": 2011 },
{ "maker": "Honda", "model": "Jazz", "year": 2010 },
{ "maker": "Honda", "model": "Civic", "year": 2007 },
{ "maker": "Toyota", "model": "Yaris", "year": 2008 },
{ "maker": "Honda", "model": "Accord", "year": 2011 }
]
};
Here’s a transformation:
const jsont = require('json-transforms');
const rules = [
jsont.pathRule(
'.automobiles{.maker === "Honda"}', d => ({
Honda: d.runner()
})
),
jsont.pathRule(
'.{.maker}', d => ({
model: d.match.model,
year: d.match.year
})
),
jsont.identity
];
const transformed = jsont.transform(json, rules);
Which output the following:
{
"Honda": [
{ "model": "Jazz", "year": 2010 },
{ "model": "Civic", "year": 2007 },
{ "model": "Accord", "year": 2011 }
]
}
This transform is composed of three rules. The first matches any automobile which is made by Honda, emitting an object with a Honda
property, then recursively matching. The second rule matches any object with a maker
property, outputting the model
and year
properties. The final is the identity transform that recursively matches.
1
Here are a few examples of what you can do with my (small[jslt.min.js]) JSLT — JavaScript Lightweight Transforms:
https://jsfiddle.net/YSharpLanguage/c7usrpsL/10
([jslt.min.js] weighs ~ 3.1kb minified)
that is, just one function,
function Per ( subject ) { ... }
… which actually mimics XSLT (1.0)’s processing model.
(cf. the “transform” and “template” inner functions, in Per’s body)
So, in essence, it’s simply just all baked into that single function Per ( subject ) { ... }
which forks its evaluation on the type of its (also) unique argument, to implement, either:
1) Array subject
nodeset creation / filtering / flattening / grouping / ordering / etc, if subject is an array, where the resulting nodeset (an Array as well) is extended with, and bound to methods named accordingly (only the returned Array instance of the call to Per ( subjectArray )
is extended; i.e., Array.prototype is left untouched)
i.e., Per :: Array -->
Array
(the resulting Array‘s extension methods having self-explanatory names such as, groupBy, orderBy, flattenBy, etc — cf. the usage in the examples)
2) String subject
string interpolation, if subject is a string
(“Per” then returns an object with a method map ( source )
, which is bound to the subject template string)
i.e., Per :: String -->
{ map :: (AnyValue -->
String) }
e.g.,
Per("Hi honey, my name is {last}. {first}, {last}.").map({ "first": "James", "last": "Bond" })
yields:
"Hi honey, my name is Bond. James, Bond."
while either of
Per("Those '{*}' are our 10 digits.").map([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ])
or
Per("Those '{*}' are our 10 digits.").map(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
yields the same:
"Those '0123456789' are our 10 digits."
but only
Per("Those '{*}' are our 10 digits.").map([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ], ", ")
yields
"Those '0, 1, 2, 3, 4, 5, 6, 7, 8, 9' are our 10 digits."
3) Transform subject
XSLT look-alike transformation, if the subject is a hash with a conventionally-defined “$” member providing the array of rewriting rules (and same as in (2), “Per” then returns an object with a method map ( source )
bound to the subject transform — where
“ruleName” in Per ( subjectTransform [ , ruleName ])
is optional and provides functionality similar to <xsl:call-template name=”templateName”>…)
i.e., Per :: (Transform [ , ruleName :: String ]) -->
{ map :: (AnyValue -->
AnyValue) }
with
Transform :: { $ :: Array of rewriting rules[rw.r.] }
([rw.r.] predicate and template function pairs)
e.g., given (… another contrived example)
// (A "Member" must have first and last names, and a gender)
function Member(obj) {
return obj.first && obj.last && obj.sex;
}
var a_transform = { $: [
//...
[ [ Member ], // (alike <xsl:template match="...">...)
function(member) {
return {
li: Per("{first} {last}").map(member) +
" " +
Per(this).map({ gender: member.sex })
};
}
],
[ [ function(info) { return info.gender; } ], // (alike <xsl:template match="...">...)
function(info) { return Per("(gender: {gender})").map(info); }
],
[ [ "betterGenderString" ], // (alike <xsl:template name="betterGenderString">...)
function(info) {
info.pronoun = info.pronoun || "his/her";
return Per("({pronoun} gender is {gender})").map(info);
}
]
//...
] };
then
Per(a_transform).map({ "first": "John", "last": "Smith", "sex": "Male" })
yields:
{ "li": "John Smith (gender: Male)" }
while… (much alike <xsl:call-template name="betterGenderString">...
)
"James Bond... " +
Per(a_transform, "betterGenderString").map({ "pronoun": "his", "gender": "Male" })
yields:
"James Bond... (his gender is Male)"
and
"Someone... " +
Per(a_transform, "betterGenderString").map({ "gender": "Male or Female" })
yields:
"Someone... (his/her gender is Male or Female)"
4) Otherwise
the identity function, in all other cases
i.e., Per :: T -->
T
( i.e., Per === function ( value ) { return value ; }
)
Note
in (3) above, a JavaScript’s “this” in the body of a template function is thus bound to the container / owner Transform and its set of rules (as defined by the $: [ … ] array) — therefore, making the expression “Per(this)”, in that context, a functionally-close equivalent to XSLT’s
<xsl:apply-templates select="..."/>
‘HTH,
3
I don’t think you will ever get an JSON variant for JSON per se. There exist several templating engines such as Python’s Jinja2, JavaScripts Nunjucks, the Groovy MarkupTemplateEngine, and many others that should be well suited for what you want. .NET has T4 and JSON serialization/deserialization support so you have that also.
Since the derserialized JSON data would be basically a dictionary or map structure, that would just get past to your templating engine and you would iterate over the desired nodes there. The JSON data then gets transformed by the template.