Jackson is the standard Object to JSON mapping library for Java. It converts POJOs to JSON strings and it parses JSON strings to populate POJOs. It sits behind many JSON based REST servers in Java including Spring MVC. For basic use cases it is very simple to use. Indeed, with Spring Boot it is completely transparent – the framework leverages Jackson to do Object to JSON mapping for you. It’s feature rich and extensible so it can usually handle more complex use cases too. One such use case is returning large JSON responses with Jackson.
In this post, I look at three approaches to returning JSON as a HTTP GET response. All examples use Spring Boot / Spring MVC as the web framework. I have a POJO Model
class that I want to map to JSON:
public class Model {
private long id;
private String name;
private String data;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
}
I also have a wrapper class to represent an array of Model
s:
public class SuperModel {
private List<Model> models = new ArrayList<>();
public List<Model> getModels() {
return models;
}
public void setModels(List<Model> models) {
this.models = models;
}
public void addModel(Model model) {
models.add(model);
}
}
You can find the full solution on GitHub.
Naive approach: writeValueAsString
A typical way to create a JSON representation of a POJO is Jackson’s ObjectMapper.writeValueAsString().
@GetMapping("/valueAsString")
public String valueAsString() throws IOException {
SuperModel superModel = new SuperModel();
for (int i=0; i<1024; i++) {
Model model = new Model();
model.setId(i);
model.setName("Model " + i);
model.setData(RandomStringUtils.randomAlphanumeric(1024));
}
return objectMapper.writeValueAsString(largeModel);
}
This returns the JSON representation of 1024 Model
objects. The Model
objects and Jackson’s JSON String representation must all fit in heap memory. We can’t garbage collect anything until the method returns. If I run this with maximum 32M memory (-Xmx32M
), I can return around 2 * 1024 = 2048 Model objects before I run out of heap:
java.lang.OutOfMemoryError: Java heap space
Better approach: writeValue to OutputStream
We can significantly reduce memory footprint by having Jackson write directly to the HTTP response’s OutputStream
without creating an intermediate String representation:
@GetMapping("/valueAsOutputStream")
public void valueAsOutputStream(HttpServletResponse response) throws IOException {
SuperModel superModel = new SuperModel();
for (int i=0; i<1024; i++) {
Model model = new Model();
model.setId(i);
model.setName("Model " + i);
model.setData(RandomStringUtils.randomAlphanumeric(1024));
superModel.addModel(model);
}
objectMapper.writeValue(response.getOutputStream(), largeModel);
}
Here we give the HTTP response’s OutputStream
to Jackson to populate. Jackson writes to OutputStream
incrementally so the full response does not need to reside in memory. With this approach, we can return up to 16 * 1024 = 16384 Model
objects. Jackson no longer needs memory to store the JSON representation. The limiting factor here is the SuperModel
POJO and all the Model
objects it contains. They must all reside on the heap until Jackson is done with them.
Best approach: SequenceWriter
If you want return really large JSON responses with Jackson, you’ll need to build the response iteratively. Jackson’s SequenceWriter allows us to iteratively map objects to a JSON output stream without closing the stream. This means that we can create the POJO, convert it to JSON and then discard it. In this case, I don’t explicitly remove it but the garbage collector is able to remove them as we go.
@GetMapping("/valuesArrayAsOutputStream")
public void valuesArrayAsOutputStream(HttpServletResponse response) throws IOException {
// Create a SequenceWriter to write Models one by one.
try (SequenceWriter sequenceWriter = objectMapper.writer().writeValues(response.getOutputStream())) {
sequenceWriter.init(true);
for (int i = 0; i < 128 * 1024; i++) {
Model model = new Model();
model.setId(i);
model.setName("Model " + i);
model.setData(RandomStringUtils.randomAlphanumeric(1024));
sequenceWriter.write(model);
}
}
}
Here, we write each Model
with the SequenceWriter
as it’s created. After it’s written to the SequenceWriter
, it’s no longer reachable and is garbage collected. I’ve written 128 * 1024 = 131072 Model objects to the HTTP response. This gives results in a 133MB response with a JVM limited to 32MB. The size of the response far exceeds the memory available to the JVM so this demonstrates that we never store the whole response or all of the Model
s in memory at the same time.
Be First to Comment