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:
<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.
The XML dataformat supports reading XML from Strings or input streams.
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 />");
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);
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');
The XML data type supports manipulation of XML attributes and child elements.
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);
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");
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");
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"));
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");
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");
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);
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"));
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);
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'
The XML datatype supports writing XML to Strings, output streams or writers.
import static org.camunda.spin.Spin.XML;
SpinXmlTreeElement element = XML("<root id=\"test\"/>");
String xml = element.toString();
String value = element.attr("id").toString();
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);
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);
The XML datatype supports querying with the XPath 1.0 query language.
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();
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();
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();
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();
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();
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();
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();
To use namespaces in spin with xml you can choose one of the following methods or combine both of them.
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();
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();
xmlns="<URI>"
in your xml file, spin uses DEFAULT
as prefix for the namespace.<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.
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.
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;
}
}
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);
We can map the customer
back to XML as follows:
import static org.camunda.spin.Spin.XML;
String xml = XML(customer).toString();
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.
The JSON datatype supports reading JSON from Strings or Readers.
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
.
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);
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"}');
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();
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 boolYou 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();
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)
To set a property you can use the .prop("name", object)
method. This allows you to set one of the following 5 simple types:
.prop("name", "Waldo")
.prop("age", 42)
.prop("period", 4200000000)
.prop("price", 42.00)
.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);
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);
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.
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'
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
The JSON datatype supports writing JSON to Strings or Writers.
import static org.camunda.spin.Spin.JSON;
SpinJsonNode jsonNode = JSON("{\"customer\": \"Kermit\"}");
String json = jsonNode.toString();
import static org.camunda.spin.Spin.JSON;
SpinJsonNode jsonNode = JSON("{\"customer\": \"Kermit\"}");
StringWriter writer = jsonNode.writeToWriter(new StringWriter());
The JSON datatype supports querying with the JSONPath query language.
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();
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();
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();
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();
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();
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();
Spin can deserialize JSON to Java objects and serialize Java objects to JSON by integrating Jackson's mapping features into its fluent API.
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;
}
}
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);
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.
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 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.
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.
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
org.camunda.spin.spi.DataFormatConfigurator
META-INF/services/org.camunda.spin.spi.DataFormatConfigurator
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
org.camunda.spin.spi.DataFormat
org.camunda.spin.spi.DataFormatProvider
META-INF/services/org.camunda.spin.spi.DataFormatProvider
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.