Skip to content

Instantly share code, notes, and snippets.

@joewiz
Last active June 6, 2021 00:33
Show Gist options
  • Save joewiz/d986da715facaad633db to your computer and use it in GitHub Desktop.
Save joewiz/d986da715facaad633db to your computer and use it in GitHub Desktop.
An implementation of XQuery 3.1's fn:json-to-xml and fn:xml-to-json functions for eXist
xquery version "3.1";
(:~
: An implementation of XQuery 3.1's fn:json-to-xml and fn:xml-to-json functions for eXist, which does not support them natively as of 4.3.0.
:
: @author Joe Wicentowski
: @version 0.4
: @see http://www.w3.org/TR/xpath-functions-31/#json
:)
module namespace jx = "http://joewiz.org/ns/xquery/json-xml";
(:~
: Parses a string supplied in the form of a JSON text, returning the results in the form of an XML document node.
:
: @param $json-text A string supplied in the form of a JSON text
: @return The results in the form of an XML document node
: @see https://www.w3.org/TR/xpath-functions-31/#func-json-to-xml
:)
declare function jx:json-to-xml($json-text as xs:string) as document-node()? {
jx:json-to-xml($json-text, map {})
};
(:~
: Parses a string supplied in the form of a JSON text, returning the results in the form of an XML document node.
:
: @param $json-text A string supplied in the form of a JSON text
: @param $options Used to control the way in which the parsing takes place
: @return The results in the form of an XML document node
: @see https://www.w3.org/TR/xpath-functions-31/#func-json-to-xml
:)
declare function jx:json-to-xml($json-text as xs:string, $options as map(*)) as document-node()? {
let $json := parse-json($json-text, $options)
return
document { jx:json-to-xml-recurse($json) }
};
(:~
: Converts an XML tree, whose format corresponds to the XML representation of JSON defined in the XPath and XQuery 3.1 Functions & Operators specification, into a string conforming to the JSON grammar.
:
: @param $input An XML tree, whose format corresponds to the XML representation of JSON
: @return A string conforming to the JSON grammar
: @see https://www.w3.org/TR/xpath-functions-31/#func-xml-to-json
:)
declare function jx:xml-to-json($input as node()?) as xs:string? {
jx:xml-to-json($input, map {} )
};
(:~
: Converts an XML tree, whose format corresponds to the XML representation of JSON defined in the XPath and XQuery 3.1 Functions & Operators specification, into a string conforming to the JSON grammar.
:
: @param $input An XML tree, whose format corresponds to the XML representation of JSON
: @param $options Options for controlling the way in which the conversion takes place
: @return A string conforming to the JSON grammar
: @see https://www.w3.org/TR/xpath-functions-31/#func-xml-to-json
:)
declare function jx:xml-to-json($input as node()?, $options as map(*)) as xs:string? {
let $json := jx:xml-to-json-recurse($input)
let $serialization-parameters := map { "method": "json", "indent": $options?indent }
return
serialize($json, $serialization-parameters)
};
(:~
: A utility function that recurses through a parsed JSON text, returning the results in the form of XML nodes.
:
: @param $json A parsed JSON text
: @return The results in the form of an XML document node
:)
declare %private function jx:json-to-xml-recurse($json as item()*) as item()+ {
let $data-type := jx:json-data-type($json)
return
element { QName("http://www.w3.org/2005/xpath-functions", $data-type) } {
if ($data-type eq "array") then
for $array-member in $json?*
let $array-member-data-type := jx:json-data-type($array-member)
return
element {$array-member-data-type} {
if ($array-member-data-type = ("array", "map")) then
jx:json-to-xml-recurse($array-member)/node()
else
$array-member
}
else if ($data-type eq "map") then
map:for-each(
$json,
function($object-name, $object-value) {
let $object-value-data-type := jx:json-data-type($object-value)
return
element { QName("http://www.w3.org/2005/xpath-functions", $object-value-data-type) } {
attribute key {$object-name},
if ($object-value-data-type = ("array", "map")) then
jx:json-to-xml-recurse($object-value)/node()
else
$object-value
}
}
)
else
$json
}
};
(:~
: A utility function for getting the data type of JSON data
:)
declare %private function jx:json-data-type($json as item()?) {
if ($json instance of array(*)) then 'array'
else if ($json instance of map(*)) then 'map'
else if ($json instance of xs:string) then 'string'
else if ($json instance of xs:double) then 'number'
else if ($json instance of xs:boolean) then 'boolean'
else if (empty($json)) then 'null'
else error(xs:QName('ERR'), 'Not a known data type for json data')
};
declare %private function jx:xml-to-json-recurse($input as node()*) as item()* {
for $node in $input
return
typeswitch ($node)
case element(fn:map) return
if ($node/@key) then
map { $node/@key: map:merge( jx:xml-to-json-recurse($node/node()) ) }
else
map:merge( jx:xml-to-json-recurse($node/node()) )
case element(fn:array) return
if ($node/@key) then
map { $node/@key: array { jx:xml-to-json-recurse($node/node()) } }
else
array { jx:xml-to-json-recurse($node/node()) }
case element(fn:string) return
if ($node/@key) then
map { $node/@key: $node/string() }
else
$node/string()
case element(fn:number) return
if ($node/@key) then
map { $node/@key: $node cast as xs:double }
else
$node cast as xs:double
case element(fn:boolean) return
if ($node/@key) then
map { $node/@key: $node cast as xs:boolean }
else
$node cast as xs:boolean
case element(fn:null) return
if ($node/@key) then
map { $node/@key: () }
else
'null'
case document-node() return
jx:xml-to-json-recurse($node/node())
(: Comments, processing instructions, and whitespace text node children of map and array are ignored :)
case text() return
if (normalize-space($node) eq '') then
()
else
$node
case comment() | processing-instruction() return
()
case element() return
error(xs:QName('FOJS0006'), 'Invalid XML representation of JSON')
default return
error(xs:QName('ERR'), 'Does not match known node types for xml-to-json data')
};
@adamretter
Copy link

Thanks Joe

@benibela
Copy link

benibela commented Sep 11, 2020

This does not check for the "escape" option? Do you plan to add it?

@joewiz
Copy link
Author

joewiz commented Sep 11, 2020

@benibela Now that eXist has native support for fn:json-to-xml() and fn:xml-to-json(), I don't use this library anymore. You're welcome to fork this and add to it though. If you do, please add a link here, and I bet people would find it useful!

@benibela
Copy link

I have changed it a little to pass more qt3 tests https://gist.github.com/benibela/0bd905319eb159353f0620ffb3ab65a5

Unfortunately xml-to-json/json-to-xml need to do things that parse-json/serialize cannot do, like preserving nulls. So this is kind of a deadend

@joewiz
Copy link
Author

joewiz commented Sep 24, 2020

@benibela Thanks for the link to your fork. Could you give an example of how this approach fail to preserve nulls?

@benibela
Copy link

benibela commented Sep 24, 2020

Because null is parsed as empty-sequence, and then it disappears in the iteration, [null, null] turn into []

Although on second thought, it can actually work with null, but it needs to iterate differently

I have updated it. Only failing 56 of 168 tests now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment