1. Overview
Apache Camel is a powerful open-source integration framework implementing a number of known Enterprise Integration Patterns.
Typically when working with message routing using Camel, we’ll want to use one of the many supported pluggable data formats. Given that JSON is popular in many modern APIs and data services, it becomes a common choice.
In this tutorial, we’ll take a look at a couple of ways we can unmarshal a JSON Array into a list of Java objects using the camel-jackson component.
2. Dependencies
First, let’s add the camel-jackson-starter dependency to our pom.xml:
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-jackson-starter</artifactId>
<version>3.21.0</version>
</dependency>
Then, we’ll also add the camel-test-spring-junit5 dependency specifically for our unit tests:
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-test-spring-junit5</artifactId>
<version>3.21.0</version>
<scope>test</scope>
</dependency>
3. Fruit Domain Classes
Throughout this tutorial, we’ll use a couple of light POJO objects to model our fruit domain.
Let’s go ahead and define a class with an id and a name to represent a fruit:
public class Fruit {
private String name;
private int id;
// standard getter and setters
}
Next, we’ll define a container to hold a list of Fruit objects:
public class FruitList {
private List<Fruit> fruits;
public List<Fruit> getFruits() {
return fruits;
}
public void setFruits(List<Fruit> fruits) {
this.fruits = fruits;
}
}
In the next couple of sections, we’ll see how to unmarshal a JSON string representing a list of fruit into these domain classes. Ultimately what we are looking for is a variable of type List
4. Unmarshalling a JSON FruitList
In this first example, we’ll represent a simple list of fruit using JSON format:
{
"fruits": [
{
"id": 100,
"name": "Banana"
},
{
"id": 101,
"name": "Apple"
}
]
}
Above all, we should emphasize that this JSON represents an object that contains a property called fruits, which contains our array.
Now let’s set up our Apache Camel route to perform the deserialization:
@Bean
RoutesBuilder route() {
return new RouteBuilder() {
@Override
public void configure() throws Exception {
from("direct:jsonInput").unmarshal(new JacksonDataFormat(FruitList.class))
.to("mock:marshalledObject");
}
};
}
In this example, we use a direct endpoint with the name jsonInput. Next, we call the unmarshal method, which deserializes each message body received on our Camel exchange into the specified data format.
We’re using the JacksonDataFormat class with a custom unmarshal type of FruitList. This is essentially a simple wrapper around the Jackon ObjectMapper and lets us marshal to and from JSON.
Finally, we send the result of the unmarshal method to a mock endpoint called marshalledObject. As we’re going to see, this is how we’ll test our route to see if it is working correctly.
With that in mind, let’s go ahead and write our first unit test:
@CamelSpringBootTest
@SpringBootTest
public class FruitListJacksonUnmarshalUnitTest {
@Autowired
private ProducerTemplate template;
@EndpointInject("mock:marshalledObject")
private MockEndpoint mock;
@Test
public void givenJsonFruitList_whenUnmarshalled_thenSuccess() throws Exception {
mock.setExpectedMessageCount(1);
mock.message(0).body().isInstanceOf(FruitList.class);
String json = readJsonFromFile("/json/fruit-list.json");
template.sendBody("direct:jsonInput", json);
mock.assertIsSatisfied();
FruitList fruitList = mock.getReceivedExchanges().get(0).getIn().getBody(FruitList.class);
assertNotNull("Fruit lists should not be null", fruitList);
List<Fruit> fruits = fruitList.getFruits();
assertEquals("There should be two fruits", 2, fruits.size());
Fruit fruit = fruits.get(0);
assertEquals("Fruit name", "Banana", fruit.getName());
assertEquals("Fruit id", 100, fruit.getId());
fruit = fruits.get(1);
assertEquals("Fruit name", "Apple", fruit.getName());
assertEquals("Fruit id", 101, fruit.getId());
}
}
Let’s walk through the key parts of our test to understand what is going on:
- First things first, we start by adding the @CamelSpringBootTest annotation, a useful annotation for testing Camel with Spring Boot.
- Then we inject the MockEndpoint using @EndpointInject. This annotation allows us to set up mock endpoints without having to manually look them up from the Camel context. Here, we set mock:marshalledObject as the URI of the Camel endpoint. It’s pointing to a mock endpoint named marshalledObject.
- After that, we set up our test expectations. Our mock endpoint should receive one message, and the message type should be a FruitList.
- Now we’re ready to send out the JSON input file as a String to the direct endpoint we defined earlier. We use the ProducerTemplate class to send the message to the endpoint.
- After we check our mock expectations have been satisfied, we are free to retrieve the FruitList and check the contents are as expected.
This test confirms that our route is working properly and our JSON is being unmarshalled as expected. Awesome!
5. Unmarshalling a JSON Fruit Array
On the other hand, we may want to avoid using a container object to hold our Fruit objects. We can modify our JSON to hold a fruit array directly:
[
{
"id": 100,
"name": "Banana"
},
{
"id": 101,
"name": "Apple"
}
]
This time around, our route is almost identical, but we set it up to work specifically with a JSON array:
@Bean
RoutesBuilder route() {
return new RouteBuilder() {
@Override
public void configure() throws Exception {
from("direct:jsonInput").unmarshal(new ListJacksonDataFormat(Fruit.class))
.to("mock:marshalledObject");
}
};
}
As we can see, the only difference to our previous example is that we’re using the ListJacksonDataFormat class with a custom unmarshal type of Fruit. This is a Jackson data format type that we can use to work with lists.
Likewise, our unit test is very similar:
@Test
public void givenJsonFruitArray_whenUnmarshalled_thenSuccess() throws Exception {
mock.setExpectedMessageCount(1);
mock.message(0).body().isInstanceOf(List.class);
String json = readJsonFromFile("/json/fruit-array.json");
template.sendBody("direct:jsonInput", json);
mock.assertIsSatisfied();
@SuppressWarnings("unchecked")
List<Fruit> fruitList = mock.getReceivedExchanges().get(0).getIn().getBody(List.class);
assertNotNull("Fruit lists should not be null", fruitList);
assertEquals("There should be two fruits", 2, fruitList.size());
Fruit fruit = fruitList.get(0);
assertEquals("Fruit name", "Banana", fruit.getName());
assertEquals("Fruit id", 100, fruit.getId());
fruit = fruitList.get(1);
assertEquals("Fruit name", "Apple", fruit.getName());
assertEquals("Fruit id", 101, fruit.getId());
}
However, there are two subtle differences with respect to the test we saw in the previous section:
- We’re first setting up our mock expectation to contain a body with a List.class directly.
- When we retrieve the message body as a List.class, we’ll get a standard warning about type safety – hence the use of @SuppressWarnings(“unchecked”).
6. Conclusion
In this short article, we’ve seen two simple approaches for unmarshalling JSON arrays using camel message routing and the camel-jackson component. The key difference between the two approaches is that JacksonDataFormat deserializes to an object type, whereas ListJacksonDataFormat deserializes to a list type.
As always, the full source code of the article is available over on GitHub.