Skip to content

Large JSON responses with Jackson

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 Models:

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 Models in memory at the same time.

Published inHow ToSpring Boot

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *