Working with JSON is a daily task in modern Java applications. Thanks to the Java 8 Stream API and the Jackson library, you can now process JSON data in a clean, functional style. Instead of writing verbose loops and conditionals, you can compose readable pipelines that handle filtering, mapping, and collecting with ease.
In this tutorial, we’ll walk through how to parse a JSON document and apply powerful filters using Java Streams. We’ll also cover best practices for Java 11+ and performance tips for handling large datasets.
Plain Java Streams filtering
Java Streams are wrappers around a data source, which allow us to operate with a data source, making bulk processing convenient and fast. A stream does not store data and, for this reason, is not a data structure. It also never modifies the underlying data source.
There are many usages of Java Streams API however we will show here how it can be used to filter through data. Consider the following example:
List<String> list = Arrays.asList("apple", "peach", "banana");
List<String> result = list.stream() // convert list to stream
.filter(line -> !"apple".equals(line)) // we exclude apple
.collect(Collectors.toList()); // collect the output and convert streams to a List
result.forEach(System.out::println); //output : peach banana
How can we use the same to filter a JSON Document? Let’s assume you have the following JSON Document:
{
"name": "School ABC",
"topics": [
"Math",
"Science",
"Music",
"History",
"English"
],
"classrooms": [
"SectionA",
"SectionB",
"SectionC"
],
"notes": null,
"active": true,
"visitors": 10000000
}
So, assumed that you have loaded your javax.json.JsonObject in a variable named ‘jsonObject’, here is how you can filter through the topics which start with ‘M’:
private JsonObject jsonObject = loadJsonObject();
public List<String> filterJsonArrayToList() {
List<String> topics = jsonObject.getJsonArray("topics").stream()
.filter(jsonValue -> ((JsonString) jsonValue).getString().startsWith("M"))
.map(jsonValue -> ((JsonString) jsonValue).getString())
.collect(Collectors.toList());
return topics;
}
That will return a List of Strings (‘Math’,’Music’). On the other hand, if you want it as JsonArray, you can use the following code:
public JsonArray filterJsonArrayToJsonArray() {
JsonArray topics = jsonObject.getJsonArray("topics").stream()
.filter(jsonValue -> ((JsonString) jsonValue).getString().startsWith("C"))
.collect(JsonCollectors.toJsonArray());
return topics;
}
Sure! Here’s the polished content for the three new sections you can directly paste into your article:
🔧 Java 11+ Tip: Use Map.of() and Collectors.toUnmodifiableList()
If you’re using Java 11 or later, you can take advantage of new factory methods like Map.of() and immutable collectors to make your code more concise and safer.
For example, instead of writing:
Map<String, Object> data = new HashMap<>();
data.put("name", "Alice");
data.put("age", 30);
You can write:
Map<String, Object> data = Map.of("name", "Alice", "age", 30);
Similarly, when collecting results from a Stream, you can use:
List<JsonNode> filtered = nodes.stream()
.filter(node -> node.get("active").asBoolean())
.collect(Collectors.toUnmodifiableList());
This ensures the resulting list is immutable, helping prevent accidental changes and making your code more robust in concurrent or shared contexts. These improvements align well with functional programming patterns and make JSON filtering pipelines cleaner and more expressive in Java 11+.
🛠️ Practical Example: Filtering a Complex JSON
Let’s consider a more realistic scenario—a JSON array representing a list of books:
[
{ "title": "Effective Java", "price": 40, "inStock": true },
{ "title": "Clean Code", "price": 35, "inStock": false },
{ "title": "Java Concurrency in Practice", "price": 50, "inStock": true }
]
You can filter this list using Jackson and Java Streams to retrieve only the books that are in stock and cost less than 45:
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(json);
ArrayNode books = (ArrayNode) root;
List<JsonNode> availableBooks = StreamSupport.stream(books.spliterator(), false)
.filter(book -> book.get("inStock").asBoolean())
.filter(book -> book.get("price").asInt() < 45)
.collect(Collectors.toList());
availableBooks.forEach(book ->
System.out.println(book.get("title").asText())
);
This practical example demonstrates how Java Streams allow you to combine multiple filtering criteria in a readable and declarative way. For even more concise syntax, you could consider using libraries like JsonPath (as discussed in another section).
⚙️ Performance Considerations
While using Stream and JsonNode is convenient for filtering small to medium-sized JSON payloads, it’s important to consider performance when working with large or deeply nested JSON documents.
Parsing an entire JSON file into a DOM-like structure (JsonNode) loads the full tree into memory, which can be inefficient. In high-throughput environments or large datasets, consider using Jackson’s Streaming API (JsonParser and JsonToken) to process data incrementally:
JsonFactory factory = new JsonFactory();
JsonParser parser = factory.createParser(new File("books.json"));
while (!parser.isClosed()) {
JsonToken token = parser.nextToken();
// Use token stream to locate and filter fields
}
This allows your application to process JSON records one at a time without loading the entire document into memory.
In short:
- Use
JsonNodeand streams for readability and developer speed. - Use the streaming parser for large documents or low-memory environments.
Filtering JSON with Jackson
If you want a more concise version, you can use Jackson databinding to simplify filtering JSON Data in combination with the Stream API. Let’s see a practical example of it:
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS com.fasterxml.jackson.core:jackson-core:2.14.2
//DEPS com.fasterxml.jackson.core:jackson-databind:2.14.2
//DEPS com.fasterxml.jackson.core:jackson-annotations:2.14.2
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
public class Main {
public static void main(String[] args) throws Exception {
// Example JSON document
String jsonString = "[{\"name\": \"John\", \"age\": 30}, {\"name\": \"Jane\", \"age\": 25}, {\"name\": \"Bob\", \"age\": 35}]";
// Parse the JSON document
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(jsonString);
// Convert the JSON document to a stream of objects
Stream<JsonNode> stream = StreamSupport.stream(jsonNode.spliterator(), false);
// Filter the stream of objects
Stream<JsonNode> filteredStream = stream.filter(node -> node.get("name").asText().equals("John"));
// Convert the filtered stream back to a JSON document
JsonNode filteredJsonNode = objectMapper.valueToTree(filteredStream.collect(Collectors.toList()));
String filteredJsonString = objectMapper.writeValueAsString(filteredJsonNode);
// Print the filtered JSON document
System.out.println(filteredJsonString);
}
}
Firstly, we will mention that the above source code contains JBang comments so that you can run it from the shell with no project attached to it. If you are new to JBang we recommend checking this article: JBang: Create Java scripts like a pro
In this example, the main method reads in a sample JSON document as a string, parses it into a JsonNode object using the Jackson library, converts it into a stream of JsonNode objects, filters the stream to only include objects where the “name” field is equal to “John”, and then converts the filtered stream back into a JsonNode object and prints it out as a string.
You can run this class to see the filtered JSON document printed to the console.
jbang Main.java
[{"name":"John","age":30}]
Conclusion
There are several options to filter through a JSON document using Java Stream API. In this article we have covered a plain Java Stream solution and another example which uses Jackson dependencies to simplify the filtering of data