Skip to content

Instantly share code, notes, and snippets.

@masyukun
Last active January 11, 2017 16:30
Show Gist options
  • Save masyukun/4190b195afb02e940453c3fbb829dcc4 to your computer and use it in GitHub Desktop.
Save masyukun/4190b195afb02e940453c3fbb829dcc4 to your computer and use it in GitHub Desktop.
(:~
This module converts an XML document into a JSON document.
- Element ordering is preserved.
- Attributes and namespaces are ignored.
- Identical element names become JSON arrays.
Example usage:
let $uri := "somedoc.xml"
let $doc := fn:doc($uri)
return jsontools:jsonify($doc)
This version allows attribute array="true" to specify that an element should contain a JSON array.
This version returns a JSON null when the value of the text node is "null" and not specifically typed.
This version looks for an optional attribute on text nodes called type, with valid values of
- "boolean" or "number" = which will cause the text value to be cast to the native JSON equivalent type.
- "ignore" = which preserves any element marked such as XML text inside the JSON object.
This version supports a second function call with an optional top-level JSON array of objects.
This version supports creating a headless JSON object, by passing the option "headless"
- Example: jsontools:jsonify($doc/child::*, ("headless"))
@author Matthew Royal
@version 1.7
@since 1.0
@see https://gist.github.com/masyukun
@see Pigritia Impatiens Hubris
:)
xquery version "1.0-ml";
module namespace jsontools = "http://matthewroyal.com/marklogic/jsontools";
import module namespace json = "http://marklogic.com/xdmp/json" at "/MarkLogic/json/json.xqy";
(: Generate JSON object from map structure :)
declare function jsontools:jsonify($documents as node()+) {
if (fn:count($documents) le 1) then
xdmp:to-json(map:entry(fn:local-name($documents), jsontools:mapify($documents)))
else
xdmp:to-json(map:entry(fn:local-name($documents[1]), for $d in $documents return jsontools:mapify($d)))
};
(: Generate JSON object from map structure :)
declare function jsontools:jsonify($documents as node()+, $options) {
if (fn:exists($options) and $options instance of xs:string+) then
if (fn:count($options) gt 1) then
fn:error(xs:QName("INVALID-OPTION"), "Only one option is allowed: (headless|wraparray)")
else if ($options = "headless") then
let $headlessObj := map:map()
let $_ :=
for $d in $documents
let $local := fn:local-name($d)
let $e := map:get($headlessObj, $local )
return
if (fn:not(fn:exists($e))) then
map:put( $headlessObj, $local, jsontools:mapify($d) )
else
let $_ :=
if ($e instance of json:array) then ()
else
let $swap := $e
let $_ := map:put($headlessObj, $local, json:array())
return json:array-push(map:get($headlessObj, $local), $swap)
return json:array-push(map:get($headlessObj, $local), jsontools:mapify($d) )
return
xdmp:to-json(
$headlessObj
)
else if ($options = "wraparray") then
jsontools:jsonify($documents, fn:true())
else
fn:error(xs:QName("INVALID-OPTION"), "Unrecognized option")
else if (fn:exists($options) and $options instance of xs:boolean) then
(: Return multiple nodes as a JSON array without a key :)
let $array := json:array()
let $_ :=
for $d in $documents
return json:array-push( $array, jsontools:mapify($d) )
return $array
else
jsontools:jsonify($documents)
};
(: Generate nested map of XML document structure :)
declare function jsontools:mapify( $document as node() ) {
let $type := fn:lower-case($document/@type)
(: Get all the names of the child names :)
let $childNames :=
for $n in $document/child::node()
return
if (not(local-name($n))) then ()
else local-name($n)
(: Keep elements in original order (mostly -- except repeated element names, which become arrays) :)
let $o := json:object-define( fn:distinct-values(($childNames)) )
(: Configure the ones used more than once into arrays :)
let $_ :=
for $n in fn:distinct-values(($childNames))
let $single := fn:count($childNames[. eq $n]) eq 1
return if ($single) then () else map:put($o, $n, json:array())
(:: Configure ones explicitly marked with array="true" as arrays :)
let $_ :=
for $n in $document/child::node()
let $isArray := fn:lower-case($n/@array) eq "true"
return if ($isArray) then map:put($o, local-name($n), json:array()) else ()
(: Add ignored elements into the text component :)
let $ignoredNodeTexts :=
for $n in $document/child::node()
return
if ($type eq "ignore") then xdmp:quote($n)
else ()
(: Get the text node :)
let $text :=
fn:replace(
fn:normalize-space(
fn:string-join(($ignoredNodeTexts, $document/text())," ")
)
, "(^ +| +$)", ""
)
let $_ :=
if ($text) then ()
else
for $n in $document/child::node()
let $childtype := fn:lower-case($n/@type)
let $childtext := fn:normalize-space(fn:string-join($n/text()," "))
return
if ($type eq "ignore") then ()
else if (not(local-name($n))) then ()
(: Multiple identical names will be grouped into arrays, also items with array="true" :)
else if ( (fn:count($childNames[. eq local-name($n)]) gt 1) or (fn:lower-case($n/@array) eq "true") ) then
let $thisarray := map:get($o, local-name($n))
return
if ($childtext) then json:array-push($thisarray,
if ($childtype eq "number") then xs:decimal(fn:replace($childtext, "[^0-9\.\-]", ""))
else if ($childtype eq "boolean") then xs:boolean(fn:lower-case($childtext))
else if (not(exists($childtext)) or $childtext eq "null") then ()
else $childtext
)
else json:array-push($thisarray, jsontools:mapify($n))
(: Single items :)
else
if ($childtext) then map:put($o, local-name($n),
if ($childtype eq "number") then xs:decimal(fn:replace($childtext, "[^0-9\.\-]", ""))
else if ($childtype eq "boolean") then xs:boolean(fn:lower-case($childtext))
else if (not(exists($childtext)) or $childtext eq "null") then ()
else $childtext
)
else map:put($o, local-name($n), jsontools:mapify($n))
(: Return the map :)
return
if ($text) then
if ($type eq "number") then xs:decimal(fn:replace($text, "[^0-9\.\-]", ""))
else if ($type eq "boolean") then xs:boolean(fn:lower-case($text))
else if (not(exists($text)) or $text eq "null") then ()
else $text
else $o
};
@masyukun
Copy link
Author

masyukun commented Jan 10, 2017

Lots of changes in 1.5, especially allowing explicit creation of JSON arrays and value type (boolean, number, null)

@masyukun
Copy link
Author

masyukun commented Jan 11, 2017

Version 1.6 includes the type="ignore" attribute. Adding this attribute to an element will result in jsonify ignoring that element and its children, and instead convert its children into text node(s) attached to the parent.

@masyukun
Copy link
Author

Version 1.7 accepts string options

  • "headless" = Create a JSON object with no head. Example usage: jsontools:jsonify($doc/child::*, ("headless"))
  • "wraparray" = Return a JSON array instead of a JSON object
  • backwardly compatible: boolean option still works

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