Introduction #
This script is a Rhino-compatible JavaScript library that converts JSON data structures to XML format, designed to work with Rhino 1.7R5 and later. It provides a jsonToXml function that accepts JSON input (objects, arrays, or primitives) and extensive options for customizing the output XML, including root element naming, array item tags, attribute prefixes, pretty printing with configurable indentation, and the ability to wrap simple values in custom tags. The script handles XML escaping for special characters, sanitizes element names to comply with XML standards, separates JSON properties with attribute prefixes (default @) into actual XML attributes, and converts nested objects into child XML elements with configurable attribute node names.
Core function #
jsonToXml(json, options) → string`</mark>Parameters:
json(Object|Array|string|number|boolean|null) – Input data to convertoptions(Object, optional) – Conversion options
Returns: (string) XML string representation
Supported Input Types #
| Input Type | Example | Output |
|---|---|---|
| Simple object | {name: "Keith Armstrong", age: 35} |
<person><name>Keith Armstrong</name><age>35</age></person> |
| Array of primitives | ["red", "green", "blue"] |
<palette><color>red</color><color>green</color>...</palette> |
| Nested objects | {address: {city: "SF"}} |
<root><address><city>SF</city></address></root> |
| Arrays of objects | [{id: 1}, {id: 2}] |
<orders><order><id>1</id></order>...</orders> |
| Primitive values | "Hello" |
<message>Hello</message> |
| Empty arrays/null | {items: [], count: null} |
<dataset><items></items><count></count></dataset> |
Configuration Options #
| Option | Type | Default | Description |
|---|---|---|---|
rootName |
string | 'root' |
Name for the root XML element |
itemName |
string | 'item' |
Name for array item elements |
attrPrefix |
string | '@' |
Prefix for JSON keys to treat as XML attributes |
attrNodeName |
string | 'attr' |
Element name for nested object properties |
attrKeyName |
string | 'attr-name' |
Attribute name for the key in nested property elements |
prettyPrint |
boolean | false |
Enable indentation and line breaks |
indentChar |
string | ' ' |
Indentation string (e.g., ' ') |
wrapSimpleValues |
string | 'value' |
Tag name to wrap simple values. If empty, simple values are not wrapped |
attrForAllKeys |
boolean | false |
Apply attrNodeName and attrKeyName to all top-level JSON keys |
Features #
- XML Escaping Automatically escapes special characters (&, <, >, ", ') in values.
- Attribute Support JSON keys prefixed with @ (configurable) become XML attributes:
{"@id": "123", name: "Test"} →
Test - Nested Object Handling Nested objects are converted to child elements, with their properties as
value elements by default. - Element Name Sanitization Invalid XML characters in names are replaced with _, and names starting with numbers/special chars are prefixed with _.
- Pretty Printing Optional formatted output with configurable indentation.
- Rhino Compatibility Designed to work with Rhino 1.7R5+, using print() for output.
XML Escaping #
Automatically escapes special characters in values:
&→&<→<>→>"→"'→'
Attribute Support #
JSON keys prefixed with @ (configurable via attrPrefix) become XML attributes:
// Input
{ "@id": "P1001", "@category": "electronics", name: "Smartphone X" }
// Output with attrPrefix: '@'
<product id="P1001" category="electronics">
<name>Smartphone X</name>
</product>Nested Object Handling #
Nested objects are converted to child elements. Their properties become:
<attr attr-name="key">value</attr>unless configured otherwise via attrNodeName and attrKeyName.
Element Name Sanitization #
- Invalid XML characters ([^a-zA-Z0-9_:.-]) replaced with _
- Names starting with numbers or special chars prefixed with _
Pretty Printing #
Optional formatted output with configurable indentation:
<root>
<name>Value</name>
<nested>
<item>Content</item>
</nested>
</root>Rhino compatibility #
Designed to work with Rhino 1.7R5+.
Examples Included #
The file contains 14 usage examples demonstrating:
- Simple and complex objects
- Arrays (primitives & objects)
- XML attributes via @ prefix
- Custom indentation
- Empty arrays and null values
- Special character escaping
- Custom attribute element names (attrNodeName, attrKeyName)
- Mixed content
Example 1: Simple Object #
JSON: {
"name": "Sarah Johnson",
"age": 28,
"city": "New York"
}
XML: <person><name>Sarah Johnson</name><age>28</age><city>New York</city></person>Example 2: Array of Primitives #
JSON: [
"red",
"green",
"blue"
]
XML: <palette>
<color>red</color>
<color>green</color>
<color>blue</color>
</palette>Example 3: Nested Objects #
JSON: {
"name": "TechCorp",
"founded": 2010,
"address": {
"street": "123 Innovation Blvd",
"city": "San Francisco",
"zip": "94105"
}
}
XML: <company>
<name>TechCorp</name>
<founded>2010</founded>
<address>
<attr attr-name="street">123 Innovation Blvd</attr>
<attr attr-name="city">San Francisco</attr>
<attr attr-name="zip">94105</attr>
</address>
</company>Example 4: Object with Attributes #
JSON: {
"@id": "P1001",
"@category": "electronics",
"name": "Smartphone X",
"price": 599.99,
"inStock": true
}
XML: <product id="P1001" category="electronics">
<name>Smartphone X</name>
<price>599.99</price>
<inStock>true</inStock>
</product>Example 5: Complex Structure #
JSON: {
"@name": "Central Library",
"books": [
{
"@isbn": "978-0134853987",
"title": "The Pragmatic Programmer",
"author": "Andrew Hunt",
"year": 1999
},
{
"@isbn": "978-0596517748",
"title": "JavaScript: The Good Parts",
"author": "Douglas Crockford",
"year": 2008
}
]
}
XML: <library name="Central Library">
<books>
<book isbn="978-0134853987">
<title>The Pragmatic Programmer</title>
<author>Andrew Hunt</author>
<year>1999</year>
</book>
<book isbn="978-0596517748">
<title>JavaScript: The Good Parts</title>
<author>Douglas Crockford</author>
<year>2008</year>
</book>
</books>
</library>Example 6: XML Escaping #
JSON: {
"title": "Great Product <test>",
"rating": 5,
"comment": "This works perfectly! 5 < 10 & 3 > 2"
}
XML: <review>
<title>Great Product <test></title>
<rating>5</rating>
<comment>This works perfectly! 5 < 10 & 3 > 2</comment>
</review>Example 7: Array of Objects #
JSON: [
{
"id": "A001",
"amount": 150.5,
"status": "shipped"
},
{
"id": "A002",
"amount": 275.25,
"status": "processing"
},
{
"id": "A003",
"amount": 99.99,
"status": "delivered"
}
]
XML: <orders>
<order>
<id>A001</id>
<amount>150.5</amount>
<status>shipped</status>
</order>
<order>
<id>A002</id>
<amount>275.25</amount>
<status>processing</status>
</order>
<order>
<id>A003</id>
<amount>99.99</amount>
<status>delivered</status>
</order>
</orders>Example 8: Empty & Null Values #
JSON: {
"items": [],
"count": null,
"active": false
}
XML: <dataset>
<items/>
<count/>
<active>false</active>
</dataset>Example 9: Custom Indentation #
JSON: {
"setting1": "value1",
"setting2": "value2",
"nested": {
"optionA": true,
"optionB": false
}
}
XML: <configuration>
<setting1>value1</setting1>
<setting2>value2</setting2>
<nested>
<attr attr-name="optionA">true</attr>
<attr attr-name="optionB">false</attr>
</nested>
</configuration>Example 10: Example with attr-name default #
JSON: {
"setting1": "value1",
"setting2": "value2",
"nested": {
"optionA": true,
"optionB": false
}
}
XML: <configuration>
<setting1>value1</setting1>
<setting2>value2</setting2>
<nested>
<attr attr-name="optionA">true</attr>
<attr attr-name="optionB">false</attr>
</nested>
</configuration>Example 11: Primitive Value #
JSON: "Hello, World!"
XML: <message>Hello, World!</message>Example 12: Array of Objects #
JSON: [
{
"id": 1,
"name": "Alice",
"department": "Engineering"
},
{
"id": 2,
"name": "Bob",
"department": "Sales"
},
{
"id": 3,
"name": "Charlie",
"department": "HR"
}
]
XML: <company><employeeeeeee><id>1</id><name>Alice</name><department>Engineering</department></employeeeeeee><employeeeeeee><id>2</id><name>Bob</name><department>Sales</department></employeeeeeee><employeeeeeee><id>3</id><name>Charlie</name><department>HR</department></employeeeeeee></company>Example 13: Custom attr names #
JSON: [
{
"name": "keith",
"permissions": {
"read": true,
"write": false
}
}
]
XML: <xml><item><name>keith</name><permissions><value name="read">true</value><value name="write">false</value></permissions></item></xml>Example 14: Empty array in object #
JSON: [
{
"response": 200,
"data": []
}
]
XML: <xml><item><response>200</response><data/></item></xml>Script #
myJSON = JSON.parse(arguments[0])
options = JSON.parse(arguments[1])
print (jsonToXml (myJSON, options));
/**
* json_to_xml.js - Rhino-compatible JSON to XML converter
* Works with Rhino 1.7R5 and later
*/
/**
* Converts a JSON object to XML string
* NOTE: JSON object properties with simple values become child XML elements by default.
* With wrapSimpleValues=true, they are wrapped in <attrNodeName attrKeyName="key">value</attrNodeName> elements.
* Nested objects are converted to elements with <attrNodeName attrKeyName="key">value</attrNodeName> children.
* @param {Object|Array|string|number|boolean} json - The JSON data to convert
* @param {Object} [options] - Conversion options
* @param {string} [options.rootName='root'] - Name for the root XML element
* @param {string} [options.itemName='item'] - Name for array item elements
* @param {string} [options.attrPrefix='@'] - Prefix for attributes in JSON (e.g., {"@id": 1})
* @param {string} [optionts.attrNodeName='attr'] - Element name for nested object properties
* @param {string} [options.attrKeyName='attr-name'] - Attribute name for the key in nested object property elements
* @param {string} [options.wrapSimpleValues=''] - Tag name to wrap simple values (e.g., 'newtag' for <newtag>value</newtag>). If empty, simple values are not wrapped.
* @param {boolean} [options.prettyPrint=false] - Format XML with indentation
* @param {string} [options.indentChar=' '] - Indentation string
* @param {string} [options.additionalXml=''] - Additional XML string to append after the root element
* @param {boolean} [options.attrForAllKeys=false] - Apply attrNodeName and attrKeyName to all top-level JSON keys
* @returns {string} XML string
*/
function jsonToXml(json, options) {
options = options || {};
var rootName = options.rootName || 'root';
var itemName = options.itemName || 'item';
var attrPrefix = options.attrPrefix || '@';
var attrNodeName = options.attrNodeName || 'attr';
var attrKeyName = options.attrKeyName || 'attr-name';
var wrapSimpleValues = options.wrapSimpleValues || '';
// Backward compatibility: if wrapSimpleValues is true, default to 'value' as tag name
if (wrapSimpleValues === true) {
wrapSimpleValues = 'value';
}
var prettyPrint = options.prettyPrint || false;
var indentChar = options.indentChar || ' ';
var additionalXml = options.additionalXml || '';
var attrForAllKeys = options.attrForAllKeys || false;
function escapeXml(str) {
if (str == null) return '';
str = String(str);
str = str.replace(/&/g, '&');
str = str.replace(/</g, '<');
str = str.replace(/>/g, '>');
str = str.replace(/"/g, '"');
str = str.replace(/'/g, ''');
return str;
}
function sanitizeElementName(name) {
if (name == null || name === '') {
return 'null';
}
name = String(name);
// Replace invalid XML element name characters
name = name.replace(/[^a-zA-Z0-9_:.-]/g, '_');
// Ensure it doesn't start with a number or special char
if (!/^[a-zA-Z_]/.test(name)) {
name = '_' + name;
}
return name;
}
function isArray(arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
}
function getKeys(obj) {
var keys = [];
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
keys.push(key);
}
}
return keys;
}
function hasKeys(obj) {
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
return true;
}
}
return false;
}
function buildXml(obj, name, indent) {
var xml = '';
var attributes = {};
var children = {};
// Sanitize element name
name = sanitizeElementName(name);
// Handle null/undefined
if (obj == null) {
return indent + '<' + name + '/>';
}
// Separate attributes from children
if (typeof obj === 'object' && !isArray(obj)) {
var keys = getKeys(obj);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
if (key.indexOf(attrPrefix) === 0) {
var attrName = key.substring(attrPrefix.length);
attrName = sanitizeElementName(attrName);
attributes[attrName] = obj[key];
} else {
var sanitizedKey = sanitizeElementName(key);
children[sanitizedKey] = obj[key];
}
}
}
// Build attribute string
var attrString = '';
var attrKeys = getKeys(attributes);
for (var j = 0; j < attrKeys.length; j++) {
var attr = attrKeys[j];
attr = sanitizeElementName(attr);
attrString += ' ' + attr + '="' + escapeXml(attributes[attr]) + '"';
}
// Start tag
xml += indent + '<' + name + attrString;
// Handle different types
if (typeof obj === 'object') {
if (isArray(obj)) {
if (obj.length === 0) {
xml += '/>';
} else {
xml += '>';
if (prettyPrint) xml += '\n';
for (var k = 0; k < obj.length; k++) {
xml += buildXml(obj[k], itemName, indent + (prettyPrint ? indentChar : ''));
if (prettyPrint) xml += '\n';
}
xml += indent + '</' + name + '>';
}
} else if (hasKeys(children)) {
xml += '>';
if (prettyPrint) xml += '\n';
var childKeys = getKeys(children);
for (var m = 0; m < childKeys.length; m++) {
var childName = childKeys[m];
var childValue = children[childName];
// Sanitize child element name
childName = sanitizeElementName(childName);
// If attrForAllKeys is set, treat all children as attr nodes
if (attrForAllKeys) {
xml += indent + (prettyPrint ? indentChar : '');
xml += '<' + attrNodeName + ' ' + attrKeyName + '="' + escapeXml(childName) + '">';
if (typeof childValue === 'object' && childValue !== null && !isArray(childValue)) {
var nestedKeys = getKeys(childValue);
for (var n = 0; n < nestedKeys.length; n++) {
var nestedKey = nestedKeys[n];
var nestedValue = childValue[nestedKey];
if (nestedKey.indexOf(attrPrefix) === 0) continue;
nestedKey = sanitizeElementName(nestedKey);
xml += indent + (prettyPrint ? indentChar + indentChar : '');
xml += '<' + attrNodeName + ' ' + attrKeyName + '="' + escapeXml(nestedKey) + '">';
if (wrapSimpleValues) {
xml += '<' + wrapSimpleValues + '>' + escapeXml(nestedValue) + '</' + wrapSimpleValues + '>';
} else {
xml += escapeXml(nestedValue);
}
xml += '</' + attrNodeName + '>';
if (prettyPrint) xml += '\n';
}
} else if (isArray(childValue)) {
for (var k = 0; k < childValue.length; k++) {
xml += buildXml(childValue[k], itemName, indent + (prettyPrint ? indentChar + indentChar : ''));
if (prettyPrint) xml += '\n';
}
} else {
if (wrapSimpleValues) {
xml += '<' + wrapSimpleValues + '>' + escapeXml(childValue) + '</' + wrapSimpleValues + '>';
} else {
xml += escapeXml(childValue);
}
}
xml += '</' + attrNodeName + '>';
if (prettyPrint) xml += '\n';
// If wrapSimpleValues is set, treat all values (including nested objects) as attr nodes
} else if (wrapSimpleValues) {
if (typeof childValue === 'object' && childValue !== null && !isArray(childValue)) {
// For nested objects, create a container element with attr nodes for each property
xml += indent + (prettyPrint ? indentChar : '') + '<' + childName + '>';
if (prettyPrint) xml += '\n';
var nestedKeys = getKeys(childValue);
for (var n = 0; n < nestedKeys.length; n++) {
var nestedKey = nestedKeys[n];
var nestedValue = childValue[nestedKey];
nestedKey = sanitizeElementName(nestedKey);
xml += indent + (prettyPrint ? indentChar + indentChar : '');
xml += '<' + attrNodeName + ' ' + attrKeyName + '="' + escapeXml(nestedKey) + '">';
xml += '<' + wrapSimpleValues + '>' + escapeXml(nestedValue) + '</' + wrapSimpleValues + '>';
xml += '</' + attrNodeName + '>';
if (prettyPrint) xml += '\n';
}
xml += indent + (prettyPrint ? indentChar : '') + '</' + childName + '>';
if (prettyPrint) xml += '\n';
} else {
// For simple values, just wrap with the wrapSimpleValues tag
xml += indent + (prettyPrint ? indentChar : '') + '<' + childName + '>';
xml += '<' + wrapSimpleValues + '>' + escapeXml(childValue) + '</' + wrapSimpleValues + '>';
xml += '</' + childName + '>';
if (prettyPrint) xml += '\n';
}
} else if (typeof childValue === 'object' && childValue !== null && !isArray(childValue)) {
// If child value is an object (not array), convert its properties to <attr> elements
xml += indent + (prettyPrint ? indentChar : '') + '<' + childName + '>';
if (prettyPrint) xml += '\n';
var nestedKeys = getKeys(childValue);
for (var n = 0; n < nestedKeys.length; n++) {
var nestedKey = nestedKeys[n];
var nestedValue = childValue[nestedKey];
// Skip @ prefixed keys as they are already handled as attributes
if (nestedKey.indexOf(attrPrefix) === 0) continue;
nestedKey = sanitizeElementName(nestedKey);
xml += indent + (prettyPrint ? indentChar + indentChar : '');
xml += '<' + attrNodeName + ' ' + attrKeyName + '="' + escapeXml(nestedKey) + '">';
xml += escapeXml(nestedValue);
xml += '</' + attrNodeName + '>';
if (prettyPrint) xml += '\n';
}
xml += indent + (prettyPrint ? indentChar : '') + '</' + childName + '>';
if (prettyPrint) xml += '\n';
} else {
xml += buildXml(childValue, childName, indent + (prettyPrint ? indentChar : ''));
if (prettyPrint) xml += '\n';
}
}
xml += indent + '</' + name + '>';
} else {
xml += '/>';
}
} else {
xml += '>' + escapeXml(obj) + '</' + name + '>';
}
return xml;
}
// Handle array at root level
if (isArray(json)) {
var arrXml = '';
if (prettyPrint) arrXml += '\n';
for (var i = 0; i < json.length; i++) {
arrXml += buildXml(json[i], itemName, (prettyPrint ? indentChar : ''));
if (prettyPrint) arrXml += '\n';
}
return '<' + rootName + '>' + arrXml + '</' + rootName + '>' + additionalXml;
}
// Handle primitive at root level
if (typeof json !== 'object') {
return '<' + rootName + '>' + escapeXml(json) + '</' + rootName + '>' + additionalXml;
}
// Handle object at root level
return buildXml(json, rootName, '') + additionalXml;
}