Chapter 18: Working with Data and JSON
Modern applications rely heavily on structured data, whether it comes from configuration files, APIs, databases, or web services. In Java, working with data formats such as JSON and XML is a fundamental skill, as these formats are widely used to exchange and store information across systems and platforms. Java provides both built-in and third-party libraries that make parsing, generating, validating, and transforming data straightforward and efficient.
JSON (JavaScript Object Notation) has become the de facto standard for lightweight data exchange due to its readability and ease of use. Java includes multiple APIs and libraries for processing JSON, such as the built-in java.json package, and popular third-party libraries like Gson and Jackson. XML, though older, remains prevalent in enterprise systems, configuration files, and standards such as SOAP and SVG. Java’s DOM and SAX parsers offer reliable ways to read and manipulate XML data structures.
This chapter explores how to parse and produce JSON and XML, how to use high-level data-binding libraries to convert between Java objects and text formats, and how to validate and transform data for integration between systems. Practical examples will demonstrate reading from APIs, creating structured data output, and mapping Java objects to portable representations that work seamlessly across platforms.
Parsing and generating JSON
JSON (JavaScript Object Notation) is a lightweight text format used to represent structured data. It is language-independent, simple to read, and easy to generate, making it the preferred choice for modern APIs and configuration files. In Java, you can work with JSON using either the built-in java.json package (part of Jakarta EE and available in several standard distributions) or third-party libraries like Gson and Jackson.
Understanding JSON structure
JSON represents data as key–value pairs inside objects and arrays. A typical JSON object might look like this:
{
"name": "Robin",
"age": 42,
"languages": ["Java", "Python", "C"],
"active": true
}
This example defines an object with string, numeric, array, and boolean values. In Java, such data can be represented using maps, lists, and primitive types.
Parsing JSON with javax.json
The javax.json API (also available as jakarta.json in newer versions) provides classes such as JsonReader, JsonObject, and JsonArray for reading and writing JSON in a streaming or object-based manner.
import javax.json.*;
import java.io.*;
public class JsonParsing {
public static void main(String[] args) throws Exception {
String json = "{ \"name\": \"Robin\", \"age\": 42 }";
try (JsonReader reader = Json.createReader(new StringReader(json))) {
JsonObject obj = reader.readObject();
System.out.println("Name: " + obj.getString("name"));
System.out.println("Age: " + obj.getInt("age"));
}
}
}
The JsonReader reads data into a structured JsonObject, allowing you to access fields using type-safe getter methods.
javax.json API supports both in-memory object models and streaming (event-based) processing through JsonParser for handling very large data efficiently.
Creating JSON objects
You can construct JSON programmatically using JsonObjectBuilder and JsonArrayBuilder, then serialize it to text.
import javax.json.*;
import java.io.StringWriter;
public class JsonBuilding {
public static void main(String[] args) {
JsonObject person = Json.createObjectBuilder()
.add("name", "Robin")
.add("age", 42)
.add("languages", Json.createArrayBuilder()
.add("Java")
.add("Python")
.add("C"))
.build();
StringWriter writer = new StringWriter();
Json.createWriter(writer).write(person);
System.out.println(writer.toString());
}
}
This produces a properly formatted JSON string that can be saved or transmitted. The builder pattern provides a clear, readable way to construct complex JSON structures.
Streaming JSON parsing
For very large JSON documents, the streaming API reads data incrementally, avoiding the need to load entire structures into memory. It operates similarly to an XML SAX parser, generating events for each key or value encountered.
import javax.json.stream.*;
import java.io.*;
public class JsonStreamExample {
public static void main(String[] args) throws Exception {
String json = "{\"x\":10, \"y\":20}";
try (JsonParser parser = Json.createParser(new StringReader(json))) {
while (parser.hasNext()) {
JsonParser.Event event = parser.next();
System.out.println(event);
}
}
}
}
This approach is useful for parsing large data feeds or log streams efficiently without building the full JSON object tree.
Generating JSON manually
You can also generate JSON strings manually using Java’s standard types or libraries like Gson and Jackson. For simple cases, concatenating strings may suffice, but structured builders or mappers are safer and more maintainable.
String json = String.format(
"{ \"name\": \"%s\", \"age\": %d }", "Robin", 42
);
System.out.println(json);
This manual method works for small tasks, but for larger or dynamic data structures, prefer dedicated JSON APIs that ensure correct formatting and escaping.
XML basics with DOM and SAX
XML (Extensible Markup Language) is a structured format for representing hierarchical data. It is widely used in enterprise systems, configuration files, and standards-based data exchange. Unlike JSON, XML includes metadata in the form of tags and attributes, making it suitable for schemas, validation, and complex document structures.
Java provides robust XML handling through its standard libraries in javax.xml (or jakarta.xml in newer platforms). The two main processing models are DOM (Document Object Model) and SAX (Simple API for XML). DOM loads an entire XML document into memory as a tree structure, while SAX processes the document sequentially using events. Each approach is suited to different needs.
Parsing XML with DOM
The DOM API represents an XML document as a tree of nodes that can be traversed and manipulated. It is easy to use for smaller documents where random access to elements is needed.
import javax.xml.parsers.*;
import org.w3c.dom.*;
import java.io.*;
public class DOMExample {
public static void main(String[] args) throws Exception {
File file = new File("example.xml");
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(file);
Element root = doc.getDocumentElement();
System.out.println("Root element: " + root.getTagName());
NodeList items = doc.getElementsByTagName("item");
for (int i = 0; i < items.getLength(); i++) {
Element item = (Element) items.item(i);
System.out.println("Item: " + item.getTextContent());
}
}
}
DOM is intuitive and supports full modification of the XML structure, including adding, removing, or updating elements and attributes.
setIgnoringComments(true) or setNamespaceAware(true) on the factory to customize parsing behavior when working with complex XML documents.
Generating XML with DOM
DOM can also be used to create XML documents from scratch. You can build elements programmatically and write them to a file or stream.
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.*;
public class DOMWrite {
public static void main(String[] args) throws Exception {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.newDocument();
Element root = doc.createElement("books");
doc.appendChild(root);
Element book = doc.createElement("book");
book.setAttribute("id", "1");
book.setTextContent("Learning Java");
root.appendChild(book);
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.transform(new DOMSource(doc), new StreamResult(System.out));
}
}
This example builds a simple XML tree and prints it to standard output with indentation for readability.
Parsing XML with SAX
SAX provides an event-driven approach, where the parser triggers callbacks as it reads through the document. This method is far more memory-efficient than DOM, making it suitable for large XML files or continuous data streams.
import org.xml.sax.*;
import org.xml.sax.helpers.*;
import javax.xml.parsers.*;
public class SAXExample extends DefaultHandler {
public void startElement(String uri, String localName,
String qName, Attributes attributes) {
System.out.println("Start element: " + qName);
}
public void characters(char[] ch, int start, int length) {
System.out.println("Text: " + new String(ch, start, length).trim());
}
public void endElement(String uri, String localName, String qName) {
System.out.println("End element: " + qName);
}
public static void main(String[] args) throws Exception {
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser parser = factory.newSAXParser();
parser.parse("example.xml", new SAXExample());
}
}
With SAX, you define handlers for events such as element starts, text content, and element ends. Because the parser does not build a full in-memory representation, it can process extremely large files efficiently.
Choosing between DOM and SAX
The choice between DOM and SAX depends on your needs:
- DOM: Best for small to medium documents where you need full control and manipulation of the XML tree.
- SAX: Best for large or streamed XML data where efficiency and low memory usage are priorities.
In modern Java applications, these core parsers remain the foundation for more advanced XML frameworks such as JAXB and StAX, which combine convenience with performance.
Using Gson and Jackson
While Java’s built-in JSON libraries are capable and standards-compliant, most modern developers prefer higher-level libraries such as Gson and Jackson. These libraries make it easy to convert between Java objects and JSON (a process known as data binding), offering concise syntax, strong typing, and advanced features such as custom serialization, field filtering, and streaming.
Working with Gson
Gson is a popular JSON library from Google that provides simple APIs for parsing and generating JSON. It automatically converts Java objects to JSON and back again, using reflection to map field names and types.
import com.google.gson.Gson;
class Person {
String name;
int age;
boolean active;
Person(String name, int age, boolean active) {
this.name = name;
this.age = age;
this.active = active;
}
}
public class GsonExample {
public static void main(String[] args) {
Gson gson = new Gson();
// Serialize Java object to JSON
Person person = new Person("Robin", 42, true);
String json = gson.toJson(person);
System.out.println("JSON: " + json);
// Deserialize JSON back to object
Person copy = gson.fromJson(json, Person.class);
System.out.println("Name: " + copy.name + ", Age: " + copy.age);
}
}
Gson supports nested objects, lists, and maps automatically. It also provides options for pretty-printing, excluding fields, and handling nulls or special data types.
new GsonBuilder().setPrettyPrinting().create().
Customizing serialization in Gson
You can customize how Gson serializes and deserializes objects by using annotations or adapters. For example, to change field names in JSON, use @SerializedName.
import com.google.gson.annotations.SerializedName;
class User {
@SerializedName("full_name")
String name;
@SerializedName("user_age")
int age;
}
Custom type adapters allow precise control over conversion logic, such as formatting dates or transforming special types.
Working with Jackson
Jackson is another widely used library that provides similar functionality but with a richer feature set and higher performance. It uses annotations, data binding, and a flexible streaming API for fine-grained control.
import com.fasterxml.jackson.databind.ObjectMapper;
class Product {
public String name;
public double price;
}
public class JacksonExample {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
// Serialize object to JSON
Product p = new Product();
p.name = "Laptop";
p.price = 899.99;
String json = mapper.writeValueAsString(p);
System.out.println("JSON: " + json);
// Deserialize JSON to object
Product result = mapper.readValue(json, Product.class);
System.out.println("Product: " + result.name + " ($" + result.price + ")");
}
}
Jackson automatically handles most Java types, including nested objects, collections, and generic containers. Its ObjectMapper can read and write to files, URLs, streams, or strings with equal ease.
Jackson annotations and configuration
Jackson supports annotations for customizing JSON mapping. For example, you can rename fields, ignore specific properties, or mark fields as required.
import com.fasterxml.jackson.annotation.*;
class Account {
@JsonProperty("user_name")
public String username;
@JsonIgnore
public String password;
@JsonInclude(JsonInclude.Include.NON_NULL)
public String email;
}
These annotations give precise control over serialization behavior without requiring manual converters or adapters.
Streaming and large data handling
Jackson provides a powerful streaming API for reading and writing large JSON documents efficiently. It processes data incrementally, similar to SAX for XML, making it suitable for high-performance applications.
import com.fasterxml.jackson.core.*;
public class StreamExample {
public static void main(String[] args) throws Exception {
JsonFactory factory = new JsonFactory();
// Writing JSON
try (JsonGenerator gen = factory.createGenerator(System.out)) {
gen.writeStartObject();
gen.writeStringField("name", "Streamed JSON");
gen.writeNumberField("version", 1);
gen.writeEndObject();
}
}
}
Streaming avoids the need to build large object trees in memory, which is ideal for big data pipelines or log processing.
Data validation and transformation
Once data is loaded into your Java application (whether from JSON, XML, or another source) it is important to validate and transform it before use. Validation ensures that the data meets expected formats, ranges, and constraints, while transformation adapts it into the structures your program needs. Proper handling of these steps helps prevent bugs, runtime errors, and security vulnerabilities caused by malformed or unexpected input.
Validating data structures
Validation can be performed manually or through libraries such as Jakarta Bean Validation (formerly Java EE Bean Validation) that use annotations to enforce constraints on object fields.
import jakarta.validation.constraints.*;
public class User {
@NotNull
private String name;
@Min(0)
@Max(120)
private int age;
@Email
private String email;
}
These annotations describe the allowed values for each field. A validator framework (for example, Hibernate Validator) can check objects automatically and report violations.
import jakarta.validation.*;
import java.util.Set;
public class ValidateExample {
public static void main(String[] args) {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
User user = new User();
user.setName(null);
user.setAge(200);
user.setEmail("invalid");
Set<ConstraintViolation<User>> violations = validator.validate(user);
for (ConstraintViolation<User> v : violations) {
System.out.println(v.getPropertyPath() + ": " + v.getMessage());
}
}
}
This approach centralizes validation rules within the data model itself, making it easier to maintain and reuse across projects.
Manual validation
For simple cases or when libraries are unavailable, manual checks are sufficient. These can include range checks, regular expressions, or logical constraints applied to incoming values.
public static boolean isValidAge(int age) {
return age >= 0 && age <= 120;
}
public static boolean isValidEmail(String email) {
return email != null && email.matches("^[\\w.%+-]+@[\\w.-]+\\.[A-Za-z]{2,6}$");
}
Manual validation is especially useful for lightweight programs or environments where dependency-free solutions are preferred.
Transforming JSON to Java objects
Once validated, data often needs to be transformed into domain-specific structures. Gson and Jackson both provide mechanisms to map raw JSON data into typed Java objects, handling nested structures automatically.
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.*;
class Post {
public String title;
public String author;
public List<String> tags;
}
public class TransformExample {
public static void main(String[] args) throws Exception {
String json = "{ \"title\": \"Learning Java\", \"author\": \"Robin\", " +
"\"tags\": [\"Java\", \"Programming\"] }";
ObjectMapper mapper = new ObjectMapper();
Post post = mapper.readValue(json, Post.class);
System.out.println("Title: " + post.title);
System.out.println("Tags: " + post.tags);
}
}
This data-binding approach automatically converts compatible types and builds lists, maps, and nested objects as needed.
Transforming between formats
Applications frequently need to convert between data formats such as JSON and XML. Jackson supports multiple data formats through modules like jackson-dataformat-xml, allowing seamless transformation between them.
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
public class FormatConversion {
public static void main(String[] args) throws Exception {
ObjectMapper jsonMapper = new ObjectMapper();
XmlMapper xmlMapper = new XmlMapper();
String json = "{ \"name\": \"Robin\", \"age\": 42 }";
// Convert JSON to XML
Object obj = jsonMapper.readValue(json, Object.class);
String xml = xmlMapper.writeValueAsString(obj);
System.out.println(xml);
}
}
Transforming data in this way allows a single data model to serve multiple endpoints or APIs without redundant logic.
Filtering and restructuring data
Transformation often involves reshaping or filtering large datasets before use or export. Libraries like Jackson’s JsonNode API provide a flexible tree model for traversing and modifying JSON dynamically.
import com.fasterxml.jackson.databind.*;
public class FilterExample {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
String json = "{ \"name\":\"Robin\", \"role\":\"Author\", \"salary\":5000 }";
JsonNode node = mapper.readTree(json);
((ObjectNode) node).remove("salary");
System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(node));
}
}
Filtering lets you control what information is passed along, helping to reduce data size and protect sensitive content when sharing results across systems or users.
Practical examples
To consolidate what you have learned about working with data in Java, this section demonstrates practical examples that combine parsing, validation, and transformation. These examples show how to load, process, and output JSON and XML data safely and efficiently in real-world scenarios.
Example 1: Reading and processing JSON API data
This example fetches JSON data from a web API using the HttpClient class, parses it with Jackson, and filters selected fields for display. It demonstrates asynchronous I/O combined with structured data handling.
import java.net.http.*;
import java.net.URI;
import com.fasterxml.jackson.databind.*;
public class ApiExample {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://jsonplaceholder.typicode.com/posts/1"))
.build();
HttpResponse<String> response =
client.send(request, HttpResponse.BodyHandlers.ofString());
ObjectMapper mapper = new ObjectMapper();
JsonNode node = mapper.readTree(response.body());
System.out.println("Title: " + node.get("title").asText());
System.out.println("Body: " + node.get("body").asText());
}
}
This program retrieves a JSON object, parses it into a JsonNode tree, and extracts specific properties without needing to define a dedicated Java class. This approach is ideal for exploratory or lightweight data integration tasks.
JsonNode) when the structure of incoming JSON is variable or only a subset of fields is needed.
Example 2: Validating and transforming input data
The following example validates a JSON payload, converts it into a Java object, and writes the result to an XML file. It demonstrates data pipeline stages—input validation, transformation, and output formatting.
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
class User {
public String name;
public String email;
public int age;
}
public class DataPipeline {
public static void main(String[] args) throws Exception {
String json = "{ \"name\":\"Robin\", \"email\":\"robin@example.com\", \"age\":42 }";
ObjectMapper jsonMapper = new ObjectMapper();
XmlMapper xmlMapper = new XmlMapper();
// Validate input fields
JsonNode node = jsonMapper.readTree(json);
if (!node.hasNonNull("name") || !node.hasNonNull("email")) {
throw new IllegalArgumentException("Missing required fields");
}
// Transform to Java object and then to XML
User user = jsonMapper.treeToValue(node, User.class);
String xml = xmlMapper.writerWithDefaultPrettyPrinter().writeValueAsString(user);
System.out.println(xml);
}
}
Here, JSON is validated for key fields before conversion, ensuring that only well-formed and complete data is transformed into XML. This pattern is especially useful in ETL (Extract, Transform, Load) systems and API gateways.
Example 3: Converting XML to JSON
Sometimes data originates in XML but needs to be served as JSON for modern web APIs. This example uses Jackson’s XML and JSON mappers to convert formats seamlessly.
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
public class XmlToJson {
public static void main(String[] args) throws Exception {
String xml = "<user><name>Robin</name><age>42</age></user>";
XmlMapper xmlMapper = new XmlMapper();
ObjectMapper jsonMapper = new ObjectMapper();
Object obj = xmlMapper.readValue(xml, Object.class);
String json = jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
System.out.println(json);
}
}
This technique is valuable in legacy system integrations, where older XML-based services must interface with newer REST or JavaScript clients.
Example 4: Filtering and exporting selected fields
In data analytics and reporting, you often need to export only specific fields from a larger dataset. This example uses Jackson’s tree model to remove unwanted values before writing output.
import com.fasterxml.jackson.databind.*;
import java.io.File;
public class ExportExample {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(new File("data.json"));
for (JsonNode node : root) {
((ObjectNode) node).remove("internalId");
((ObjectNode) node).remove("password");
}
mapper.writerWithDefaultPrettyPrinter().writeValue(
new File("filtered.json"), root
);
System.out.println("Filtered data written to filtered.json");
}
}
This ensures that sensitive or unnecessary data is excluded before export. The same technique can be used to anonymize or aggregate information for compliance and privacy reasons.
Example 5: Combining JSON and database data
JSON data is often combined with database content for reporting or integration. In this example, JSON records are read from a file and merged with database metadata before generating a combined output.
import com.fasterxml.jackson.databind.*;
import java.sql.*;
import java.util.*;
public class MergeExample {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
List<Map<String, Object>> data =
mapper.readValue(new java.io.File("records.json"),
new com.fasterxml.jackson.core.type.TypeReference<>() {});
try (Connection conn = DriverManager.getConnection("jdbc:h2:mem:test")) {
Statement stmt = conn.createStatement();
stmt.execute("CREATE TABLE info (id INT, category VARCHAR)");
stmt.execute("INSERT INTO info VALUES (1, 'A'), (2, 'B')");
ResultSet rs = stmt.executeQuery("SELECT * FROM info");
Map<Integer, String> categories = new HashMap<>();
while (rs.next()) categories.put(rs.getInt("id"), rs.getString("category"));
for (Map<String, Object> record : data) {
Integer id = (Integer) record.get("id");
record.put("category", categories.getOrDefault(id, "Unknown"));
}
mapper.writerWithDefaultPrettyPrinter().writeValue(
new java.io.File("merged.json"), data
);
System.out.println("Merged output written to merged.json");
}
}
}
This workflow shows how JSON and relational data can coexist cleanly within Java applications, enabling unified reporting, synchronization, and data exchange between heterogeneous systems.