camunda Spin Dataformat Reference

Introduction

Camunda Spin is a library for simple XML and JSON processing on the JVM (Java Virtual Machine), targeting Java and JVM-based scripting languages such as Groovy, JRuby, Jython, JavaScript and Java Expression Language. It provides a comprehensible fluent API for working with different data formats through lightweight wrapper objects.

Spin can be used in any Java-based application by adding the following maven dependency to your pom.xml file:

If you use Spin in combination with other Camunda BPM projects (such as the Camunda process engine), please import the Camunda BOM to ensure that you use the Camunda Spin version matching your process engine version.
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.camunda.spin</groupId>
      <artifactId>camunda-spin-bom</artifactId>
      <scope>import</scope>
      <type>pom</type>
      <version>${version.spin}</version>
    </dependency>
  </dependencies>
</dependencyManagement>
<dependencies>
  <dependency>
    <groupId>org.camunda.spin</groupId>
    <artifactId>camunda-spin-core</artifactId>
  </dependency>

  <dependency>
    <groupId>org.camunda.spin</groupId>
    <artifactId>camunda-spin-dataformat-all</artifactId>
  </dependency>
</dependencies>

Camunda Spin is published to maven central.

Reading XML

The XML dataformat supports reading XML from Strings or input streams.

Reading XML from a String:

import static org.camunda.spin.Spin.*;
import static org.camunda.spin.DataFormats.*;

SpinXmlElement xml = S("<order />", xml());

The second paramter xml() hints Spin to use the XML data format for parsing the XML.

Alternatively you can directly use the XML(...) function:

import static org.camunda.spin.Spin.*;

SpinXmlElement xml = XML("<order />");

Reading XML from a Reader:

Spin also supports reading XML directly from a java.io.Reader:

import static org.camunda.spin.Spin.*;
import static org.camunda.spin.DataFormats.*;

SpinXmlElement xml = S(reader, xml());

The XML(...) method also supports readers. The following example shows how to read the XML from a file (error handling ommitted):

import static org.camunda.spin.Spin.*;

FileInputStream fis = new FileInputStream("/tmp/incomingOrder.xml");
InputStreamReader reader = new InputStreamReader(fis, "utf-8");
SpinXmlElement xml = XML(reader);

Reading XML using a Script Language

XML can be read from script languages in the same way as from Java. Since script languages use dynamic typing, you do not need to hint the data format but you can use autodetection. The following example demonstrates how to read XML in JavaScript:

var orderId = S('<order id="1231" />').attr('id');

Manipulating XML

The XML data type supports manipulation of XML attributes and child elements.

Attributes

Checking for attributes in XML

import static org.camunda.spin.Spin.XML;

String xml = "<order xmlns:cam=\"http://camunda.org/example\" id=\"order1\" cam:name=\"name\" />";

boolean hasAttr = XML(xml).hasAttr("id");
assertTrue(hasAttr);

hasAttr = XML(xml).hasAttrNs("http://camunda.org/example", "id");
assertTrue(hasAttr);

Reading attributes from XML

import static org.camunda.spin.Spin.XML;

SpinXmlDomAttribute attribute = XML("<order id=\"order1\" />").attr("id");
String id = XML("<order id=\"order1\" />").attr("id").value();

The attr method returns a wrapper of the XML attribute and with value the value of the attribute can be accessed.

If you want to access an attribute in another namespace you have to use the attrNs method.

import static org.camunda.spin.Spin.XML;

String xml = "<order xmlns:cam=\"http://camunda.org/example\" id=\"order1\" cam:name=\"order1\" />";

SpinXmlDomAttribute attribute = XML(xml).attrNs("http://camunda.org/example", "name");

You can also get a collection of all attributes or only of a specific namespace.

import static org.camunda.spin.Spin.XML;

String xml = "<order xmlns:cam=\"http://camunda.org/example\" id=\"order1\" cam:name=\"order1\" />";

// All attributes
SpinCollection<SpinXmlDomAttribute> attributes = XML(xml).attrs();

// All attributes of a specific namespace
attributes = XML(xml).attrs("http://camunda.org/example");

Or you can directly get all attribute names instead.

import static org.camunda.spin.Spin.XML;

String xml = "<order xmlns:cam=\"http://camunda.org/example\" id=\"order1\" cam:name=\"order1\" />";

// All attribute names
List<String> names = XML(xml).attrNames();

// All attribute names of a specific namespace
names = XML(xml).attrNames("http://camunda.org/example");

Writing attributes to XML

It is possible to set a new attribute value directly from the element wrapper or on the attribute wrapper.

import static org.camunda.spin.Spin.XML;

String xml = "<order id=\"order1\" />";

XML(xml).attr("id", "newId");

SpinXmlDomAttribute attribute = XML(xml).attr("id");
attribute.value("newId");

You can also specify the namespace of the attribute to set.

import static org.camunda.spin.Spin.XML;

String xml = "<order xmlns:cam=\"http://camunda.org/example\" id=\"order1\" cam:name=\"name\" />";

XML(xml).attrNs("http://camunda.org/example", "name", "newName");

SpinXmlDomAttribute attribute = XML(xml).attrNs("http://camunda.org/example", "name");
attribute.value("newName");

Removing attributes from XML

It is possible to remove an attribute from the element directly or to remove the attribute itself.

import static org.camunda.spin.Spin.XML;

String xml = "<order id=\"order1\" />";

SpinXmlDomElement element = XML(xml).removeAttr("id");
assertFalse(element.hasAttr("id));

SpinXmlDomAttribute attribute = XML(xml).attr("id");
element = attribute.remove();
assertFalse(element.hasAttr("id));

You can also specify the namespace of the attribute to remove.

import static org.camunda.spin.Spin.XML;

String xml = "<order xmlns:cam=\"http://camunda.org/example\" id=\"order1\" cam:name=\"name\" />";

SpinXmlDomElement element = XML(xml).removeAttrNs("http://camunda.org/example", "name");
assertFalse(element.hasAttrNs("http://camunda.org/example/", "name"));

SpinXmlDomAttribute attribute = XML(xml).attrNs("http://camunda.org/example", "name");
element = attribute.remove()
assertFalse(element.hasAttrNs("http://camunda.org/example", "name"));

Text Content

It is possible to read and write the text content of an XML element with the textContent method.

import static org.camunda.spin.Spin.XML;

SpinXmlDomElement element = XML("<customer>Foo</customer>");
assertEquals("Foo", element.textContent());
element.textContent("Bar");

Child Elements

Reading child elements from XML

Besides attributes you can also get a unique or all child elements of a specific type. Optionally, a namespace can be passed to the methods as first parameter.

import static org.camunda.spin.Spin.XML;

String xml = "<order xmlns:cam=\"http://camunda.org/example\">" +
      "<date/><cam:due/><item/><item/><cam:op/><cam:op/></order>";

SpinXmlDomElement date = XML(xml).childElement("date");
SpinXmlDomElement due = XML(xml).childElement("http://camunda.org/example", "due");

SpinCollection<SpinXmlDomElement> items = XML(xml).childElements("item");
SpinCollection<SpinXmlDomElement> ops = XML(xml).childElements("http://camunda.org/example", "ops");

Append child elements

The method append is used to append a single or multiple child elements to an XML element.

import static org.camunda.spin.Spin.XML;

SpinXmlTreeElement root = XML("<root/>");

SpinXmlTreeElement child1 = XML("<child/>");
SpinXmlTreeElement child2 = XML("<child/>");
SpinXmlTreeElement child3 = XML("<child/>");

root.append(child1, child2, child3);

Remove child elements

To remove child elements from an XML element the method remove is used. It accepts single or multiple child elements and removes them from the parent element.

import static org.camunda.spin.Spin.XML;

SpinXmlTreeElement root = XML("<root><child/><child/><child/></root>");

root.remove(root.childElements("child"));

Replace elements

To replace an element or a child element the methods replace and replaceChild are used.

import static org.camunda.spin.Spin.XML;

SpinXmlTreeElement root = XML("<root><date/><order/></root>");

SpinXmlTreeElement child1 = XML("<child/>");
root.replaceChild(root.childElement("date"), child1);

SpinXmlTreeElement child2 = XML("<child/>");
root.childElement("order").replace(child2);

Manipulating XML using a Script Language

XML can be manipulated from script languages in the same was as from Java. Since script languages use dynamic typing, you do not need to hint the data format but you can use autodetection. The following example demonstrates how to access an attribute and a child element from XML in Python:

xml = """
<order id="1231">
  <item id="1"/>
</order>
"""

assert S(xml).hasAttr('id')

order_id = S(xml).attr('id').value()
assert order_id == '1231'

element = S(xml).attr('order', 'order1')
assert element.hasAttr('order')
assert element.Attr('order').value() == 'order1'

element.removeAttr('order')
assert not element.hasAttr('order')

item_id = S(xml).childElement('item').attr('id').value()
assert item_id == '1'

Writing XML

The XML datatype supports writing XML to Strings, output streams or writers.

Writing to a String:

import static org.camunda.spin.Spin.XML;

SpinXmlTreeElement element = XML("<root id=\"test\"/>");

String xml = element.toString();


String value = element.attr("id").toString();

Writing to an output stream:

import static org.camunda.spin.Spin.XML;

SpinXmlTreeElement element = XML("<root id=\"test\"/>");

OutputStream ouputStream = element.toStream();

// write element again to stream
element.writeToStream(outputStream);


SpinXmlTreeAttribute attr = element.attr("id");
ouputStream = attr.toStream();

// write attribute value again to stream
attr.writeToStream(outputStream);

Write to writer

import static org.camunda.spin.Spin.XML;

SpinXmlTreeElement element = XML("<root id=\"test\"/>");

StringWriter writer = element.writeToWriter(new StringWriter());


SpinXmlTreeAttribute attr = element.attr("id");

writer = attr.writeToWriter(writer);

Querying XML

The XML datatype supports querying with the XPath 1.0 query language.

Querying an element

import static org.camunda.spin.Spin.XML;

String xml = "<root><child id=\"child\"><a id=\"a\"/><a id=\"b\"/></child></root>";

SpinXmlTreeElement child = XML(xml).xPath("/root/child").element();

Querying an element list

import static org.camunda.spin.Spin.XML;

String xml = "<root><child id=\"child\"><a id=\"a\"/><a id=\"b\"/></child></root>";

SpinList<SpinXmlTreeElement> childs = XML(xml).xPath("/root/child/a").elementList();

Querying an attribute

import static org.camunda.spin.Spin.XML;

String xml = "<root><child id=\"child\"><a id=\"a\"/><a id=\"b\"/></child></root>";

SpinXmlTreeAttribute attribute = XML(xml).xPath("/root/child/@id").attribute();

Querying an attribute list

import static org.camunda.spin.Spin.XML;

String xml = "<root><child id=\"child\"><a id=\"a\"/><a id=\"b\"/></child></root>";

SpinList<SpinXmlTreeAttribute> attributes = XML(xml).xPath("/root/child/a/@id").attributeList();

Querying a String

import static org.camunda.spin.Spin.XML;

String xml = "<root><child id=\"child\"><a id=\"a\"/><a id=\"b\"/></child></root>";

String value = XML(xml).xPath("string(/root/child/@id)").string();

Querying a Number

import static org.camunda.spin.Spin.XML;

String xml = "<root><child id=\"child\"><a id=\"a\"/><a id=\"b\"/></child></root>";

Double count = XML(xml).xPath("count(/root/child/a)").number();

Querying a Boolean

import static org.camunda.spin.Spin.XML;

String xml = "<root><child id=\"child\"><a id=\"a\"/><a id=\"b\"/></child></root>";

Boolean exists = XML(xml).xPath("boolean(/root/child)").bool();

Querying with namespaces

To use namespaces in spin with xml you can choose one of the following methods or combine both of them.

1. Using a single prefix - URI pair

import static org.camunda.spin.Spin.XML;

String xml = "<root xmlns:t=\"http://camunda.org\"><t:child id=\"child\"><a id=\"a\"/></t:child></root>";

SpinXmlTreeElement child = XML(xml).xPath("/root/t:child")
                                   .ns("t", "http://camunda.org");
                                   .element();

2. Using a map of prefix - URI pairs

import static org.camunda.spin.Spin.XML;

String xml = "<root xmlns:t=\"http://camunda.org\"><t:child id=\"child\"><a id=\"a\"/></t:child></root>";

Map<String, String> namespaceMap = new HashMap<String, String>();
namespaceMap.put("t", "http://camunda.org");
namespaceMap.put("s", "http://camunda.com");

SpinXmlTreeElement child = XML(xml).xPath("/root/t:child")
                                   .ns(namespaceMap);
                                   .element();

If you are using xmlns="<URI>" in your xml file, spin uses DEFAULT as prefix for the namespace.
E.g.: <root xmlns="http://camunda.org"></root> -- prefix: DEFAULT, namespace: http://camunda.org so you need to use XML(xml).xPath("/DEFAULT:root") to fetch the correct element.

Mapping XML

Spin can deserialize XML to Java objects and serialize the annotated Java objects to XML by integrating mapping features into its fluent API. JAXB annotations can be added to the involved Java classes to configure the (de-)serialization process but are not required.

Mapping between Representations:

Assume we have a class Customer defined as follows:

@XmlRootElement(name="customer", namespace="http://camunda.org/test")
public class Customer {

  private String name;

  @XmlElement(namespace="http://camunda.org/test")
  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }
}

Mapping XML to Java:

We can map the following XML object

<?xml version="1.0" encoding="UTF-8"?>
<customer xmlns="http://camunda.org/example">
  <name>Kermit</name>
</customer>

to an instance of Customer in the following way:

import static org.camunda.spin.Spin.XML;

String xmlInput = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><customer xmlns=\"http://camunda.org/example\"><name>Kermit</name></customer>";

Customer customer = XML(xmlInput).mapTo(Customer.class);

Mapping Java to XML:

We can map the customer back to XML as follows:

import static org.camunda.spin.Spin.XML;

String xml = XML(customer).toString();

Configuring XML Handling

Spin can be configured to change XML parsing, writing and mapping settings. Spin uses JAXB and DOM to handle XML. The XML data format therefore uses instances of javax.xml.parsers.DocumentBuilderFactory, javax.xml.transform.TransformerFactory and javax.xml.bind.JAXBContext that can be configured using Spin's configuration mechanism. For example, a custom application may provide an implementation of org.camunda.spin.spi.DataFormatConfigurator that exchanges the JAXBContext Spin uses and caches the context to improve performance.

The data format class to register a configurator for is org.camunda.spin.impl.xml.dom.format.DomXmlDataFormat. An instance of this class provides setter methods for the above-mentioned entities that can be used to replace the default object mapper. Please refer to the JDK documentation on what configuration can be applied.

Reading JSON

The JSON datatype supports reading JSON from Strings or Readers.

Reading JSON from a String:

import static org.camunda.spin.Spin.*;
import static org.camunda.spin.DataFormats.*;

SpinJsonNode json = S("{\"customer\": \"Kermit\"}", json());

The second paramter json() hints Spin to use the JSON data format for parsing the JSON.

Alternatively, you can directly use the JSON(...) function:

import static org.camunda.spin.Spin.*;

SpinJsonNode json = JSON("{\"customer\": \"Kermit\"}");

String values that represent JSON primitive values can also be read. For example, JSON("true") returns a SpinJsonNode that represents the boolean value true.

Reading JSON from a Reader:

Spin also supports reading JSON from an instance of java.io.Reader:

import static org.camunda.spin.Spin.*;
import static org.camunda.spin.DataFormats.*;

SpinJsonNode json = S(reader, json());

The JSON(...) method also supports readers. The following example shows how to read JSON from a file (error handling ommitted):

import static org.camunda.spin.Spin.*;

FileInputStream fis = new FileInputStream("/tmp/customer.json");
InputStreamReader reader = new InputStreamReader(fis, "utf-8");
SpinJsonNode json = JSON(reader);

Reading JSON using a Script Language

JSON can be read from script languages in the same way as from Java. Since script languages use dynamic typing, you do not need to hint the data format but you can use autodetection. The following example demonstrates how to read JSON in Javascript:

var customer = S('{"customer": "Kermit"}');

Reading JSON Properties

To fetch properties from the JSON tree you can use .prop("name"). This will return the property as SpinJsonNode and you can use this to get the value of the property as the following examples will demonstrate:

in Java:

import static org.camunda.spin.Spin.*;

SpinJsonNode json = JSON("{\"customer\": \"Kermit\"}");
SpinJsonNode customerProperty = json.prop("customer");
String customerName = customerProperty.value();

in Javascript:

var json = S('{"customer": "Kermit"}');
var customerProperty = json.prop("customer");
var customerName = customerProperty.value();

The different value types

With .value() you will fetch a String representation of the value. There are also:

  • .numberValue() - will fetch a number representation of the value or throws an exception if the value is not a number
  • .boolValue() - will fetch a boolean representation of the value or throws an exception if the value is not a bool

Fetch array of data

You can also fetch a list of items if your property is an array of data.

in Java:

import static org.camunda.spin.Spin.*;

SpinJsonNode json = JSON("{\"customer\": \[\"Kermit\", \"Waldo\"\]}");
SpinJsonNode customerProperty = json.prop("customer");
SpinList customers = customerProperty.elements();
SpinJsonNode customer = customers.get(0);
String customerName = customer.value();

in Javascript:

var json = S('{"customer": ["Kermit", "Waldo"]}');
var customerProperty = json.prop("customer");
var customers = customerProperty.elements();
var customer = customers.get(0)
var customerName = customer.value();

Fetch field names

Spin allows us to use the .fieldNames() method to fetch the names of all child nodes and properties in a node. The following example shows you how to use .fieldNames() in Java and Javascript.

in Java:

import static org.camunda.spin.Spin.*;

SpinJsonNode json = JSON("{\"customer\": \[\"Kermit\", \"Waldo\"\]}");
ArrayList fieldNames = json.fieldNames();
String fieldName1 = fieldNames.get(0)

in Javascript:

var json = S('{"customer": ["Kermit", "Waldo"]}');
var fieldNames = json.fieldNames();
var fieldName1 = fieldNames.get(0)

Set JSON Properties

To set a property you can use the .prop("name", object) method. This allows you to set one of the following 5 simple types:

  • String - Example: .prop("name", "Waldo")
  • Integer - Example: .prop("age", 42)
  • Long - Example: .prop("period", 4200000000)
  • Float - Example: .prop("price", 42.00)
  • Boolean - Example: .prop("active", true)

or one of the 2 following container types:

  • Array - Could contain a number of simple and container types Example in Java:

    import static org.camunda.spin.Spin.*;
    
    SpinJsonNode json = JSON("{\"customer\": \[\"Kermit\", \"Waldo\"\]}");
    ArrayList<Object> list = new ArrayList<Object>();
    list.add("new entry");
    list.add("new entry2");
    json.prop("new_array", list);

    Example in Javascript:

    var json = S('{"customer": ["Kermit", "Waldo"]}');
    var list = ["new entry", "new entry2"];
    json.prop("new_array", list);
  • Object - Could contain a number of simple and container types Example in Java:

    import static org.camunda.spin.Spin.*;
    
    SpinJsonNode json = JSON("{\"customer\": \[\"Kermit\", \"Waldo\"\]}");
    Map<String, Object> object = new HashMap<String, Object>();
    object.put("new_entry", 42);
    object.put("new_entry2", "Yeah!");
    json.prop("new_object", object);

    Example in Javascript:

    var json = S('{"customer": ["Kermit", "Waldo"]}');
    var object = {
    "new_entry": 1,
    "new_entry2": "Yeah!"
    };
    json.prop("new_array", object);

Remove JSON Properties

There are 2 ways to remove properties from a JSON object.

  • .deleteProp("name") - Removes a property with given name.
  • .deleteProp(<List of names>) - Removes one or more properties with given names.

For more Details see the following Examples for Javascript and Java.

Java:

import static org.camunda.spin.Spin.*;

SpinJsonNode json = JSON("{\"customer\": \"Kermit\", \"language\": \"en\"}");
List<String> listOfNames = new ArrayList<String>();
listOfNames.add("customer");
listOfNames.add("language");

// removes only the customer property
json.deleteProp("customer");

// removes customer and language
json.deleteProp(list);

Javascript:

var json = S('{"customer": ["Kermit", "Waldo"], "language": "en"}');
var list = ["customer", "en"];

// removes only the customer property
json.deleteProp("customer");

// removes customer and language
json.deleteProp(list);

Work with JSON Arrays

JSON arrays represent a list of objects. Spin offers the following methods to manipulate this list:

  • .indexOf(<Object>) - Fetches the index of the FIRST occurrence of the searched object.
  • .lastIndexOf(<Object>) - Fetches the index of the LAST occurrence of the searched object.
  • .append(<Object>) - Appends an object to the end of the list.
  • .insertAt(<Index>, <Object>) - Appends an object at the specific index of the list.
  • .insertBefore(<Search object>, <Object>) - Inserts an object before the FIRST occurrence of another object.
  • .insertAfter(<Search object>, <Object>) - Inserts an object after the FIRST occurrence of another object.
  • .remove(<Object>) - Removes the FIRST occurrence of the object.
  • .removeLast(<Object>) - Removes the LAST occurrence of the object.
  • .removeAt(Index) - Removes the list entry at the specified index.

These methods allow us to work with JSON arrays in a fast way. To show this, we will use the following JSON Object as an example:

{
  "test-array" : [
    "testdata1",
    "testdata2",
    1,
    2,
    true,
    1,
    false,
    1
  ]
}

So let's see how we can manipulate this list in some examples.

Example 1 - Get the index of testdata2 and the last occurrence of '1':

import static org.camunda.spin.Spin.*;

SpinJsonNode json = JSON("{\"test-array\" : [\"testdata1\",\"testdata2\",1,2,true,1,false,1]}");
SpinJsonNode list = json.prop("test-array");

Integer i = list.indexOf("testdata2"); // Should be '1'
Integer j = list.lastIndexOf(1); // Should be '7'
var json = S('{"test-array" : ["testdata1","testdata2",1,2,true,1,false,1]}');
var list = json.prop("test-array");

var i = list.indexOf("testdata2"); // should be 1
var j = list.lastIndexOf(1); // Should be '7'

Example 2 - Add and Remove data the the list:

import static org.camunda.spin.Spin.*;

SpinJsonNode json = JSON("{\"test-array\" : [\"testdata1\",\"testdata2\",1,2,true,1,false,1]}");
SpinJsonNode list = json.prop("test-array");

list.append("test2"); // at the end of the list there should now be "test2"
list.remove("test2"); // Aaaand now, it is gone ;)

list.insertAt(1, "test3"); // test3 should now be inserted before testdata2
list.removeAt(1, "test3"); // Aaaand now, it is gone ;)

list.insertBefore(true, "test4"); // now there should be test4 on index 4
list.insertAfter(true, 5); // So now 5 is on index 6
var json = S('{"test-array" : ["testdata1","testdata2",1,2,true,1,false,1]}');
var list = json.prop("test-array");

list.append("test2"); // at the end of the list there should now be "test2"
list.remove("test2"); // Aaaand now, it is gone ;)

list.insertAt(1, "test3"); // test3 should now be inserted before testdata2
list.removeAt(1, "test3"); // Aaaand now, it is gone ;)

list.insertBefore(true, "test4"); // now there should be test4 on index 4
list.insertAfter(true, 5); // So now 5 is on index 6

Writing JSON

The JSON datatype supports writing JSON to Strings or Writers.

Writing to a String:

import static org.camunda.spin.Spin.JSON;

SpinJsonNode jsonNode = JSON("{\"customer\": \"Kermit\"}");

String json = jsonNode.toString();

Writing to a writer

import static org.camunda.spin.Spin.JSON;

SpinJsonNode jsonNode = JSON("{\"customer\": \"Kermit\"}");

StringWriter writer = jsonNode.writeToWriter(new StringWriter());

Querying JSON

The JSON datatype supports querying with the JSONPath query language.

Querying an element

import static org.camunda.spin.Spin.JSON;

String json = "{\"child\": [{\"id\": 1,\"name\": \"Lucy\",\"sex\": \"female\"},{\"id\": 2,\"name\": \"Tracy\",\"sex\": \"female\"}],\"number\": 1,\"boolean\": true}";

SpinJsonNode child = JSON(json).jsonPath("$.child[0]").element();

Querying an element list

import static org.camunda.spin.Spin.JSON;

String json = "{\"child\": [{\"id\": 1,\"name\": \"Lucy\",\"sex\": \"female\"},{\"id\": 2,\"name\": \"Tracy\",\"sex\": \"female\"}],\"number\": 1,\"boolean\": true}";

SpinList<SpinJsonNode> childs = JSON(json).jsonPath("$.child").elementList();

Querying a String

import static org.camunda.spin.Spin.JSON;

String json = "{\"child\": [{\"id\": 1,\"name\": \"Lucy\",\"sex\": \"female\"},{\"id\": 2,\"name\": \"Tracy\",\"sex\": \"female\"}],\"number\": 1,\"boolean\": true}";

String value = JSON(json).jsonPath("$.child[0].name").stringValue();

Querying a Number

import static org.camunda.spin.Spin.JSON;

String json = "{\"child\": [{\"id\": 1,\"name\": \"Lucy\",\"sex\": \"female\"},{\"id\": 2,\"name\": \"Tracy\",\"sex\": \"female\"}],\"number\": 1,\"boolean\": true}";

Double count = JSON(json).jsonPath("$.number").numberValue();

Querying a Boolean

import static org.camunda.spin.Spin.JSON;

String json = "{\"child\": [{\"id\": 1,\"name\": \"Lucy\",\"sex\": \"female\"},{\"id\": 2,\"name\": \"Tracy\",\"sex\": \"female\"}],\"number\": 1,\"boolean\": true}";

Boolean exists = JSON(json).jsonPath("$.boolean").boolValue();

Filtering a Query

Be aware that a filtering expression - the expression in the () - is not allowed to contain double quotes!

import static org.camunda.spin.Spin.JSON;

String json = "{\"child\": [{\"id\": 1,\"name\": \"Lucy\",\"sex\": \"female\"},{\"id\": 2,\"name\": \"Tracy\",\"sex\": \"female\"}],\"number\": 1,\"boolean\": true}";

SpinList<SpinJsonNode> girls = JSON(json).jsonPath("$.child[?(@.sex == 'female')]").elementList();

Mapping JSON

Spin can deserialize JSON to Java objects and serialize Java objects to JSON by integrating Jackson's mapping features into its fluent API.

Mapping between Representations:

Assume we have a class Customer defined as follows:

public class Customer {

  private String name;

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }
}

Mapping JSON to Java:

We can map a JSON object {"name" : "Kermit"} to an instance of Customer as follows:

import static org.camunda.spin.Spin.JSON;

Customer customer = JSON("{\"customer\": \"Kermit\"}").mapTo(Customer.class);

Mapping Java to JSON:

We can map the customer back to JSON as follows:

import static org.camunda.spin.Spin.JSON;

String json = JSON(customer).toString();

You can also map Java primitives like boolean or number values to the corresponding JSON values. For example, JSON(true) maps to the JSON constant of the boolean value true. However, note that String values are not converted but are interpreted as JSON input (see Reading JSON). For example, JSON("a String") raises an exception because "a String" lacks additional escaped quotes and is no valid JSON. Nevertheless, a list of String values is properly converted to a JSON array of String values.

Mapping to Generic Types:

Assume we have a list of customers that we would declare as List<Customer> in Java. For mapping a JSON array [{"name" : "Kermit"}, {"name" : "Hugo"}] to such a list, calling mapTo(ArrayList.class) is not sufficient as Jackson cannot tell of which type the array's elements are. This case can be handled by providing mapTo with a canonical type string, following Jackson's conventions:

import static org.camunda.spin.Spin.JSON;

String json = "[{\"customer\": \"Kermit\"}, {\"customer\": \"Kermit\"}]"

List<Customer> customers = JSON(json).mapTo("java.util.ArrayList<somepackage.Customer>");

Mapping to Polymorphic Types:

Mapping JSON to Java objects is particularly tricky as JSON does not contain type information which is required to deserialize it to Java objects. In the above examples, we have explicitly told Spin to map the JSON object to a Customer object using the mapTo method. For nested JSON objects, Jackson is able to infer the desired deserialization type by inspecting the declared fields of the supplied class. However, this does not work for polymorphic types. Consider the following example, where the Customer has a reference to a Car.

public class Customer {

  private String name;
  private Car car;

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public Car getCar() {
    return car;
  }

  public void setCar(Car car) {
    this.car = car;
  }
}

Assuming that Car is an interface with various implementations, such as StationWagon or Van, Jackson cannot tell which implementation to use based solely on the static structure of Customer. In these cases, Jackson relies on type information that is part of the JSON. See the Jackson documentation for the various options Jackson offers to configure type serialization and deserialization. You can configure these options in Spin as described in the configuration section.

Configuring JSON Handling

Spin can be configured to change JSON parsing, writing and mapping settings, for example to tolerate documents that are not strictly compliant to the standard. Spin uses Jackson to handle JSON. The JSON data format therefore uses an instance of com.fasterxml.jackson.databind.ObjectMapper that can be configured using Spin's configuration mechanism.

The data format class to register a configurator for is org.camunda.spin.impl.json.jackson.format.JacksonJsonDataFormat. An instance of this class provides a setter for an ObjectMapper that can be used to replace the default object mapper. This logic goes into a configurator class that implements the interface org.camunda.spin.spi.DataFormatConfigurator. Please refer to the Jackson's documentation on what configuration options are available.

Configuring Data Formats

The data formats available to Spin may not always suit your needs. Sometimes, it is necessary to provide configuration. For example, when using Spin to map Java objects to JSON, a format for how dates are serialized has to specified. While the Spin data formats use reasonable default values, they can also be changed.

To configure a data format detected by Spin, the SPI org.camunda.spin.spi.DataFormatConfigurator can be implemented. A configurator specifies which classes it can configure. Spin discovers a configurator by employing Java's service loader mechanism and will then provide it with all data formats that match the specified class (or are a subclass thereof). The concrete configuration options depend on the actual data format. For example, a Jackson-based JSON data format can modify the ObjectMapper that the data format uses.

In order to provide a custom configurator, you have to

  • Provide a custom implementation of org.camunda.spin.spi.DataFormatConfigurator
  • Add the configurator's fully qualified classname to a file named META-INF/services/org.camunda.spin.spi.DataFormatConfigurator
  • Ensure that the artifact containing the configurator is reachable from Spin's classloader

Implementing custom Data Formats

A Spin data format is an implementation of the interface org.camunda.spin.spi.DataFormat. An implementation of this interface can be registered by implementing the SPI org.camunda.spin.spi.DataFormatProvider. Spin uses the Java platform's service loader mechanism to lookup provider implementations at runtime.

In order to provide a custom dataformat, you have to

  • Provide a custom implementation of org.camunda.spin.spi.DataFormat
  • Provide a custom implementation of org.camunda.spin.spi.DataFormatProvider
  • Add the provider's fully qualified classname to a file named META-INF/services/org.camunda.spin.spi.DataFormatProvider
  • Ensure that the artifact containing the provider is reachable from Spin's classloader

If you now call org.camunda.spin.DataFormats.getAvailableDataFormats(), then the custom dataformat is returned along with the built-in dataformats. Furthermore, org.camunda.spin.DataFormats.getDataFormat(String dataFormatName) can be used to explicity retrieve the data format by a specific provider.