List.of(...) (Java 9+) creates an unmodifiable list (no add/remove/set), and disallows null elements.
Arrays.asList(...) creates a fixed-size list backed by an array (supports set, but add/remove throw UnsupportedOperationException).
If you want a modifiable ArrayList, wrap either of these in new ArrayList<>(...) to copy elements into a regular, resizable ArrayList.
Initializing an ArrayList with Multiple Items
To initialize an ArrayList with multiple items in a single line, you can create a List of items using either Arrays.asList() or List.of() methods. Both methods return a list containing the items passed to the factory method.
In the following examples, we add two strings "A" and "B" to the ArrayList:
1. Using Arrays.asList()
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class FromArraysAsList {
public static void main(String[] args) {
// Arrays.asList returns a fixed-size list backed by the provided array
List<String> fixed = Arrays.asList("A", "B");
// fixed.set(0, "X") is allowed (changes the backing array)
fixed.set(0, "X");
System.out.println(fixed); // [X, B]
// fixed.add("D") <-- throws UnsupportedOperationException
// To get a fully modifiable ArrayList, copy it:
ArrayList<String> list = new ArrayList<>(Arrays.asList("A", "B"));
list.add("C"); // OK
System.out.println(list); // [A, B, C]
}
}
✔ Allows null values. ✔ Works in all Java versions (since Java 1.2).
✔Arrays.asList(new int[]{1,2,3}) produces a List<int[]> of size 1 (because the primitive array is treated as a single element). Use boxed types (Integer[]) or streams (IntStream) to avoid this.
2. Using List.of() (Java 9+)
import java.util.ArrayList;
import java.util.List;
public class FromListOf {
public static void main(String[] args) {
// List.of returns an unmodifiable List
List<Integer> list= List.of("A", "B"); // Java 9+
// Create a mutable ArrayList by copying
ArrayList<Integer> mutable = new ArrayList<>(list);
System.out.println(mutable); // ["A", "B"]
mutable.add("C"); // works because `mutable` is an ArrayList
System.out.println(mutable); // ["A", "B","C"]
}
}
✔ More concise syntax. ❌ List.of(“A”, “B”, null) Does not allow null values (throws NullPointerException). ✔ List.of(...) list itself is unmodifiable — calling list.add("C") would throw UnsupportedOperationException.
Best practice (short)
Use List.of(...) for concise, immutable lists (configuration, constants) — Java 9+.
Use Arrays.asList(...) when you want a quick fixed-size list or when you already have an array and want a list view.
If you want a mutableArrayList, always wrap/copy:
ArrayList<T> mutable = new ArrayList<>(List.of(…)); // recommended (Java 9+) ArrayList<T> mutable2 = new ArrayList<>(Arrays.asList(…));
Java 25 doesn’t really introduce big changes around performance or immutability in Collections. Instead, it mainly brings usability, consistency, and convenience improvements (like getFirst(), getLast(), reversed(), and the standardized SequencedCollection APIs).
What’s New in Java 25 Collections?
1. Sequenced Collections Interfaces & Related Enhancements
Background: Introduced in Java 21 (JEP 431), SequencedCollection, SequencedSet, and SequencedMap were added to bring a uniform way of working with collections that have a defined encounter order (like List, LinkedHashSet, TreeMap, etc.).
What’s in Java 25:
Java 25 continues to evolve and stabilize these interfaces.
All ordered collections (List, Deque, LinkedHashSet, LinkedHashMap, TreeMap) now explicitly implement these new interfaces.
This means first element, last element, and reversed view operations are available consistently across ordered collections.
Example:
import java.util.*;
public class SequencedExample {
public static void main(String[] args) {
SequencedSet<String> names = new LinkedHashSet<>();
names.add("Amit");
names.add("Neha");
names.add("Rajesh");
System.out.println("First: " + names.getFirst()); // Amit
System.out.println("Last: " + names.getLast()); // Rajesh
System.out.println("Reversed view: " + names.reversed());
// Output: [Rajesh, Neha, Amit]
}
}
🔹 Before SequencedCollection, such operations were inconsistent:
Background: Many developers often needed the first or last element in a collection. Each collection type had different APIs, making code harder to generalize.
What’s in Java 25:
All sequenced collections (List, Deque, SequencedSet) expose:
🔹 Benefit: Cleaner, shorter, and more readable code without manual index math or custom reverse loops.
3. Collections.addAll(Collection<? super T>, T...) Overload
Background: Before Java 25, adding multiple elements required either Collections.addAll(collection, element1, element2, ...) or collection.addAll(List.of(...)).
What’s in Java 25:
Overloaded method now allows varargs directly with generics.
Simplifies bulk addition of elements into a collection.
Example:
import java.util.*;
public class AddAllExample {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
// New overload in Java 25
Collections.addAll(fruits, "Apple", "Banana", "Mango", "Orange");
System.out.println(fruits);
// [Apple, Banana, Mango, Orange]
}
}
🔹 Benefit: Less boilerplate, no need to wrap elements in List.of(...) or arrays.
4. View Collections & Reversed Views Formalized
Background: Java has long provided “views” — like subList(), unmodifiableList(), and synchronizedList().
What’s in Java 25:
The documentation and contracts for views are now more formalized.
SequencedCollection.reversed() explicitly guarantees a live view, meaning changes reflect both ways.
Here, reversedView is not a separate list. It’s a live mirror of the original, but in reverse order.
🔹 Benefit: No need to manually reverse with Collections.reverse(list) which creates a copy or mutates in place. Instead, reversed() provides a real-time, lightweight view.
What Might Be Coming / In Preview
These features aren’t specific to Collections but will affect how we use them.
1. Primitive Types in Patterns (JEP 507 – Preview)
What it is: JEP 507 lets primitive types appear in pattern contexts (top-level and nested), and extends instanceof and switch to work uniformly with primitive types. It is a preview language feature in Java 25 (third preview of this idea).
Example (Preview in Java 25):
public class PatternMatchingExample {
public static void main(String[] args) {
Object a = Integer.valueOf(42);
Object b = Double.valueOf(42.5);
if (a instanceof int ai) {
System.out.println("a matched int: " + ai);
} else {
System.out.println("a did NOT match int");
}
if (b instanceof int bi) {
System.out.println("b matched int: " + bi);
} else {
System.out.println("b did NOT match int");
}
}
}
Expected output
a matched int: 42
b did NOT match int
Integer.valueOf(42) matches int (boxing/unboxing + exact). Double.valueOf(42.5) does not match int because converting 42.5→int would lose information.
🔹 Impact on collections: If you retrieve from a List<Object> or generic collection, you can directly match primitive types instead of casting manually.
🔹 Before Java 25 (without primitive pattern matching)
If you had a List<Object> (a heterogeneous list), and you retrieved values, you couldn’t directly match primitives like int or long. You had to:
Check if the object was a wrapper class (Integer, Long, Double …).
Cast it.
Then unbox it manually.
Example (Java 21 or earlier):
List < Object > data = List.of(10, 20L, "hello");
for (Object o: data) {
if (o instanceof Integer) {
int i = (Integer) o; // manual cast + unboxing
System.out.println("Integer: " + (i * 2));
} else if (o instanceof Long) {
long l = (Long) o; // manual cast + unboxing
System.out.println("Long: " + (l + 100));
}
}
👉 This is verbose, error-prone, and harder to read.
🔹 With Java 25 (primitive pattern matching)
Java 25 introduces primitive patterns (JEP 507, still in preview). Now you can directly match primitives like int, long, double in your code — even when the object comes from a List<Object> or generic collection.
Example:
List < Object > data = List.of(10, 20L, "hello");
for (Object o: data) {
if (o instanceof int i) { // direct match to int
System.out.println("int: " + (i * 2));
} else if (o instanceof long l) { // direct match to long
System.out.println("long: " + (l + 100));
} else if (o instanceof String s) {
System.out.println("String: " + s.toUpperCase());
}
}
👉 No explicit casting or unboxing is required. The compiler does it safely for you.
🔹 Why this matters for Collections
Cleaner code: Collections often store Object (e.g., List<Object>, raw types, or generic wildcards). With primitive patterns, retrieval logic becomes concise.
Fewer bugs: No accidental ClassCastException or missing unboxing step.
Consistency: Works seamlessly in switch expressions and if statements across collection iteration.
Better readability: Code looks more declarative — “match an int” instead of “if Integer, then cast, then unbox”.
Switch with Pattern Matching in Java 25
Java 25 allows primitive types in pattern matching inside switch expressions/statements. You can now:
Match primitive values directly (int, long, double, etc.)
Use pattern variables inside the case block
Apply guards (when) to filter matched values
This makes switch more expressive and concise, especially for mixed-type collections or heterogeneous data.
1. Basic Syntax
switch (variable) {
case int i - >System.out.println("Matched int: " + i);
case long l - >System.out.println("Matched long: " + l);
case double d - >System.out.println("Matched double: " + d);
case String s - >System.out.println("Matched String: " + s);
default - >System.out.println("Unknown type/value");
}
Key points:
int i, long l, double d are pattern variables.
The switch now performs type checking + extraction in one step.
default handles unmatched types/values.
2. Using Guards with when
You can add a condition (when) to a case to filter matched values.
Object value = 25;
switch (value) {
case int i when i > 0 -> System.out.println("Positive int: " + i);
case int i when i < 0 -> System.out.println("Negative int: " + i);
case long l -> System.out.println("Long value: " + l);
case String s -> System.out.println("String value: " + s);
default -> System.out.println("Other type/value");
}
Explanation:
case int i when i > 0 → matches only if value is an int and positive.
The pattern variable i can be used inside the case.
Multiple cases for the same primitive type are allowed with different guards.
3. Using Switch with Collections
Primitive pattern matching is especially useful for List<Object> or heterogeneous collections.
List < Object > items = List.of(1, 2L, 3.5, "hello");
for (Object o: items) {
switch (o) {
case int i - >System.out.println("int: " + i);
case long l - >System.out.println("long: " + l);
case double d - >System.out.println("double: " + d);
case String s - >System.out.println("string: " + s);
default - >System.out.println("unknown type: " + o);
}
}
Output:
int: 1
long: 2
double: 3.5
string: hello
Benefit:
No manual casting/unboxing needed.
Cleaner and safer than traditional instanceof + cast logic.
4. Using Nested Patterns in Switch (with Records)
Java 25 also supports nested patterns, allowing extraction from record types with primitives.
record Point(int x, int y) {}
Object obj = new Point(5, 10);
switch (obj) {
case Point(int x, int y) p -> System.out.println("Point at: " + x + ", " + y);
default -> System.out.println("Not a Point");
}
Explanation:
The pattern Point(int x, int y) p checks if obj is a Point.
Extracts x and y as primitives.
p can still be used to refer to the original object if needed.
5. Advantages over traditional switch
Traditional switch
Switch with pattern matching (Java 25)
Only works with primitive values or enums
Works with objects, primitives, and records
Requires instanceof + cast
Type check + extraction in one concise statement
No guards on individual cases
Can use when to add conditions for more control
Hard to work with heterogeneous collections
Directly match mixed-type collections safely
6. Important Notes
Preview feature: This is still a preview in Java 25 → compile with --enable-preview.
Exactness rules: Primitive pattern matching only matches when a value can be safely narrowed without loss. For example: Object val = 100L; if (val instanceof int i) { ... } // matches only if 100L fits in int
Works seamlessly with collections: Heterogeneous List<Object> or arrays can be iterated with switch pattern matching, reducing boilerplate.
2. Other API Enhancements
Some API changes in Java 25 indirectly affect collections usage:
Better type inference for varargs and generics (making bulk operations simpler).
Documentation improvements making behavior of collection views, mutability, and concurrency guarantees clearer.
✅ In summary: Java 25 Collections Framework focuses on refining consistency (sequenced collections), convenience (getFirst(), getLast(), reversed()), and bulk operations (addAll). Preview features like primitive pattern matching will further simplify code when working with heterogeneous or generic collections.
Serialization is the process of converting an object into a byte stream, and deserialization is reconstructing the object back from the stream.
For immutable classes, special care is needed because:
Fields are declared final.
We rely on deep copy and defensive copies to ensure immutability.
The Java deserialization mechanism bypasses the constructor — meaning your deep copy logic in the constructor won’t automatically run during deserialization.
✅ Problem Scenario
Imagine you have an immutable class with a mutable field (Date).
import java.io.Serializable;
import java.util.Date;
public final class Employee implements Serializable {
private static final long serialVersionUID = 1L;
private final String name;
private final Date joiningDate; // mutable field
public Employee(String name, Date joiningDate) {
this.name = name;
this.joiningDate = new Date(joiningDate.getTime()); // defensive copy
}
public String getName() {
return name;
}
public Date getJoiningDate() {
return new Date(joiningDate.getTime()); // defensive copy
}
}
👉 During normal construction, joiningDate is safely deep-copied. 👉 But during deserialization, Java restores fields directly from the stream, bypassing the constructor — so no defensive copy is made.
This means the deserialized object might hold a direct reference to the original mutable object stored in the stream, breaking immutability.
✅ Solution: Custom readObject Method
To maintain immutability, override the readObject method and re-apply defensive copying:
private void readObject(java.io.ObjectInputStream in)
throws java.io.IOException, ClassNotFoundException {
in.defaultReadObject();
// Defensive copy after deserialization
if (joiningDate != null) {
// Reflection sets final field, so we must ensure immutability manually
java.lang.reflect.Field field;
try {
field = Employee.class.getDeclaredField("joiningDate");
field.setAccessible(true);
field.set(this, new Date(joiningDate.getTime())); // deep copy
} catch (Exception e) {
throw new java.io.InvalidObjectException("Failed to maintain immutability");
}
}
}
✅ Now even after deserialization, the joiningDate field is safely deep-copied.
✅ Alternative: readResolve Method
Another technique is using readResolve, which allows you to replace the deserialized object with a properly constructed one:
private Object readResolve() {
// Recreate immutable object using constructor
return new Employee(this.name, this.joiningDate);
}
✅ This ensures your constructor logic (including deep copy) is applied.
⚠️ Things to Be Careful About
Final Fields and Reflection
Java deserialization sets final fields via reflection, so ensure your custom logic (readObject or readResolve) maintains immutability.
Serialization Proxy Pattern (Best Practice)
Instead of allowing direct serialization of your immutable class, use a proxy object that handles serialization safely.
Example:
private Object writeReplace() {
return new SerializationProxy(this);
}
private Object readResolve() {
throw new java.io.InvalidObjectException("Proxy required");
}
private static class SerializationProxy implements Serializable {
private final String name;
private final Date joiningDate;
SerializationProxy(Employee e) {
this.name = e.name;
this.joiningDate = e.joiningDate;
}
private Object readResolve() {
return new Employee(name, joiningDate); // constructor ensures immutability
}
}
✅ This is the safest approach for immutable classes.
🔑 Summary
Issue: Deserialization bypasses constructor, so deep copy logic isn’t applied.
Solution:
Use readObject to manually enforce immutability.
Or use readResolve to reconstruct via constructor.
Best practice: adopt Serialization Proxy Pattern for maximum safety.
Java 25 introduces Flexible Constructor Bodies (JEP 513), a feature that makes constructors more flexible and easier to write. With this feature, you can now write code before calling super() or this() inside a constructor.
This means you can:
Validate inputs before passing them to the parent class.
Initialize fields before the parent constructor runs.
Avoid using static helper methods for simple tasks.
This change makes your code cleaner and safer, especially when dealing with validation or initialization logic.
Why This Feature Was Needed
Before Java 25, the first line of a constructor had to be super(...) or this(...).
This caused some problems:
You couldn’t validate input before calling the parent constructor.
You often needed static helper methods to handle validation.
Fields in the subclass couldn’t be initialized before the parent constructor ran.
👉 In short: constructors felt rigid and awkward.
What Changed in Java 25
Now, in Java 25, you can:
Write statements beforesuper(...) or this(...).
Validate input directly inside the constructor.
Assign values to fields before calling the parent constructor.
⚠️ Rules to remember:
You cannot use this, call instance methods, or read instance fields before super(...).
You can only assign values to fields.
Example 1: Validation Before Super
Before Java 25 (old way):
public class Child extends Parent {
private final String normalizedName;
public Child(String name) {
super(validateAndNormalize(name)); // must be first
this.normalizedName = validateAndNormalize(name);
}
private static String validateAndNormalize(String s) {
if (s == null || s.isBlank()) {
throw new IllegalArgumentException("Name required");
}
return s.trim().toUpperCase();
}
}
Problems: Duplicate validation and a static helper method needed.
After Java 25 (New Way):
public class Child extends Parent {
private final String normalizedName;
public Child(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name required");
}
String norm = name.trim().toUpperCase();
this.normalizedName = norm; // field initialized
super(norm); // parent constructor called
}
}
✅ Cleaner, no duplicate validation, no static helper required.
👉In the new approach, you can validate the input and initialize the field before calling super(), making the code more readable and less error-prone.
Example 2: Fixing a Classic Bug
Sometimes the parent constructor calls a method overridden by the child. In older versions, subclass fields might not be ready yet. causing bugs.
Old behavior:
class Parent {
Parent() { setup(); }
void setup() { System.out.println("Parent setup"); }
}
class Sub extends Parent {
private int value;
Sub(int v) {
this.value = v; // runs AFTER super()
}
@Override
void setup() {
System.out.println("value = " + value);
}
}
Output: value = 0 (because value wasn’t set before setup() ran).
New Java 25 behavior:
class Sub extends Parent {
private int value;
Sub(int v) {
this.value = v; // initialize field first
super(); // parent constructor runs safely
}
}
Output: value = 5 (or whatever you passed).
Key Points to Remember
No this or instance method calls before super().
Field initialization is allowed before super().
Helps you write cleaner and safer constructors.
Eliminates the need for static helper methods for validation or initialization.
When to Use Flexible Constructor Bodies
To validate inputs naturally inside constructors.
To initialize subclass fields before parent methods run.
To prepare values once and reuse them in both subclass and parent constructors.
🚀 Why It Matters
Cleaner code: You can write validation and initialization logic directly in the constructor.
Safer initialization: Prevents issues where the parent constructor might call overridden methods before the subclass is fully initialized.
No need for static helpers: Eliminates the need for static methods to handle validation or initialization.
Key Takeaways
Java 25 makes constructors more flexible.
You can now write logic before super(...).
Great for validation, field setup, and cleaner code.
Just remember: no this or method calls before super(...).
Gson (Google’s JSON library) is a powerful Java library developed by Google to serialize Java objects into JSON format and deserialize JSON back into Java objects. It is simple to use and provides flexibility in handling JSON data.
✅ Why Gson?
Lightweight and easy to use
Supports serialization and deserialization
Handles complex objects and generic types
No need for XML configuration
Integrates well with Java applications
⚡How to Add Gson to Your Project?
If you are using Maven, add the following dependency to your pom.xml:
Gson is a great library for handling JSON in Java applications. It provides easy-to-use APIs for serialization and deserialization, with flexibility for custom logic when necessary. Perfect for small to medium-sized applications or when you need quick and efficient JSON handling.
When working with JSON, the most common approach is to bind JSON data directly to Java POJOs (Plain Old Java Objects) using Jackson’s data binding feature. However, sometimes the structure of the incoming JSON is dynamic, unknown, or too flexible to predefine Java classes. In these cases, Jackson’s Tree Model API comes in handy by allowing us to parse JSON into a tree structure that can be traversed dynamically.
1.What Is the Tree Model (JsonNode)?
The Tree Model allows us to parse JSON into a tree of JsonNode objects.
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
public class JacksonTreeModelExample {
public static void main(String[] args) {
try {
String json = "{ \"name\": \"Ashish Kumar\", \"age\": 30, \"address\": { \"city\": \"Delhi\", \"zipcode\": \"110001\" }, \"skills\": [\"Java\", \"Spring Boot\", \"Jackson\"] }";
ObjectMapper objectMapper = new ObjectMapper();
// Parse JSON into a tree of JsonNode
JsonNode rootNode = objectMapper.readTree(json);
// Access simple properties
String name = rootNode.get("name").asText();
int age = rootNode.get("age").asInt();
System.out.println("Name: " + name);
System.out.println("Age: " + age);
// Access nested object
JsonNode addressNode = rootNode.get("address");
String city = addressNode.get("city").asText();
String zipcode = addressNode.get("zipcode").asText();
System.out.println("City: " + city);
System.out.println("Zipcode: " + zipcode);
// Access array
JsonNode skillsNode = rootNode.get("skills");
System.out.print("Skills: ");
for (JsonNode skill: skillsNode) {
System.out.print(skill.asText() + ",");
}
} catch(Exception e) {
e.printStackTrace();
}
}
}
✔️ Expected Output:
Name: Ashish Kumar
Age: 30
City: Delhi
Zipcode: 110001
Skills: Java Spring Boot Jackson
✅ Detailed Explanation of the Process
✔ Parsing JSON:
objectMapper.readTree(json) reads the entire JSON string and creates a tree of JsonNode objects.
rootNode represents the root of the tree.
✔ Accessing Simple Properties:
rootNode.get("name").asText() extracts the "name" property as a String.
rootNode.get("age").asInt() extracts the "age" property as an integer.
✔ Accessing a Nested Object:
rootNode.get("address") returns a JsonNode representing the "address" object.
Then we extract individual fields from addressNode:
addressNode.get("city").asText()
addressNode.get("zipcode").asText()
✔ Accessing Array Elements:
rootNode.get("skills") returns a JsonNode representing the JSON array.
Iterating through the array elements using a for-each loop and accessing each skill’s value with skill.asText().
✅ Advantages of Tree Model
Advantage
Explanation
Dynamic Parsing
No need to create Java classes in advance. Useful for unknown or changing JSON structures.
Selective Data Access
Access only the parts of the JSON you need, saving memory and effort.
Nested and Array Support
Easily traverse deeply nested objects and arrays without mapping everything upfront.
✅ When to Use Tree Model vs Data Binding
Scenario
Recommended Approach
Static and well-defined structure
Use POJOs + ObjectMapper (Serialization/Deserialization)
Unknown, dynamic, or partial structure
Use Tree Model (JsonNode)
Need to modify part of the JSON
Use ObjectNode (which extends JsonNode) to manipulate fields
4. Creating JSON Objects Dynamically with ObjectNode
In some scenarios, you don’t have a predefined Java object (POJO) to represent the JSON you want to generate. For example, when building dynamic JSON responses in REST APIs, or transforming data on the fly. Jackson’s ObjectNode class is ideal for such cases.
✅ What Is ObjectNode?
ObjectNode is a subclass of JsonNode that represents a JSON object { ... }.
Allows you to dynamically create, modify, and remove fields without needing a POJO.
Ideal when you want to construct a JSON object programmatically
✅ Demonstrates how to create simple fields using put().
✅ Shows how to add a nested object with ObjectNode.
✅ Explains how to build an array field using ArrayNode.
✅ Illustrates use cases like building dynamic JSON responses in REST APIs, building configuration files dynamically, or performing data transformations.
5. Working with JSON Arrays in Java
A JSON Array is an ordered collection of values, which may include objects, strings, numbers, booleans, or other primitives. In Java, using Jackson, you can easily parse and construct JSON arrays with the help of the ArrayNode class, which extends JsonNode.
Imagine you receive a JSON array from an external API that lists products available in an online store. You can parse the JSON array string into an ArrayNode and iterate through its elements like this:
Suppose your application needs to dynamically build a product catalog before returning it as a JSON response. You can use ArrayNode to construct the array programmatically:
👉 In a real-time e-commerce system, this dynamic creation of product lists allows you to build flexible APIs that respond with the most up-to-date product information in INR.
6. Choosing Between ArrayNode and JsonNode
When working with JSON data using Jackson, knowing when to use ArrayNode versus JsonNode helps you write cleaner, more efficient, and maintainable code.
✅ Use ArrayNode when:
You are working with a JSON structure where you know the data is always an array.
You need to perform specific operations on array elements, such as filtering, sorting, or mapping.
The structure is well-defined, and you want to work directly with array-specific methods (like .add(), .remove(), .size(), etc.).
✅ Use JsonNode when:
You are dealing with dynamic or nested JSON structures, where some fields could be arrays, objects, or primitive values.
You need a flexible and generic approach to process JSON without assuming a strict structure.
You want to parse arbitrary JSON data and explore it at runtime, especially when the schema is not predefined.
👉 Choosing the right type between ArrayNode and JsonNode helps improve code clarity and performance in your application.
7. Difference Between get()and path() in Jackson Tree Model
When working with the Jackson Tree Model (JsonNode), you often need to access specific fields or nested data in a JSON structure. Two commonly used methods for this are .get() and .path(). Understanding their differences helps you write more robust and error-resistant code.
7.1 get(String fieldName)
Behavior: Returns the value of the specified field as a JsonNode.
If the field does not exist: It returns null.
Use Case: When you are sure the field exists and want to get the exact node.
Example:
JsonNode nameNode = rootNode.get("name"); // returns null if "name" field is missing
⚠️ Risk: If you immediately call .asText() or .asInt() after .get(), and the field does not exist (i.e., null is returned), you will get a NullPointerException.
7.2 path(String fieldName)
Behavior: Returns the value of the specified field as a JsonNode.
If the field does not exist: Returns a missing node (MissingNode), which is a special JsonNode that does not throw NullPointerException and safely returns default values.
Example:
JsonNode nameNode = rootNode.path("name"); // Returns MissingNode if "name" does not exist
String name = nameNode.asText(); // Returns "" (empty string) if missing
⚡ Advantages of path()
✔ Safe Access Without NullPointerException Even if the field doesn’t exist, path() ensures that your code doesn’t throw a NullPointerException. Instead, it returns a MissingNode, and calling .asText(), .asInt(), etc., returns sensible defaults:
.asText() → ""
.asInt() → 0
✔ More Robust for Dynamic or Uncertain JSON Structures In real-world applications where the JSON structure might change or fields may be missing, using .path() prevents your code from breaking unexpectedly.
✅ Example Comparison
// Using get() – Potential NullPointerException
String name = rootNode.get("name").asText(); // Works if "name" exists
String phone = rootNode.get("phone").asText(); // Throws NullPointerException if "phone" is missing
// Using path() – Safe even if field missing
String name = rootNode.path("name").asText(); // Returns "Ashish Kumar"
String phone = rootNode.path("phone").asText(); // Returns "" (empty string)
Note :
📦 When Was path() Introduced?
The path() method has been available since early versions of Jackson (introduced around Jackson 2.x series) and has become a standard best practice for safely accessing JSON tree nodes when the presence of fields is not guaranteed.
🎯 Conclusion
Jackson’s Tree Model API is a powerful and flexible approach for handling JSON data in Java, especially when working with dynamic, unknown, or partially structured JSON. Unlike the traditional POJO-based data binding approach, the Tree Model provides the ability to parse JSON into a tree of JsonNode objects, enabling dynamic traversal, selective data access, and easy manipulation of JSON content.
By using ObjectNode and ArrayNode, developers can dynamically construct JSON objects and arrays programmatically, making it ideal for use cases such as building REST API responses or handling configuration files. Furthermore, the choice between .get() and .path() methods ensures safer and more robust handling of potentially missing fields in JSON data.
In summary, when dealing with unpredictable or frequently changing JSON structures, leveraging the Tree Model approach offers maximum flexibility, reduces boilerplate code, and improves the maintainability of your Java applications. Always prefer path() over get() in uncertain scenarios to avoid NullPointerException and ensure more stable code execution.
When working with JSON data in real-world applications, it is very common to deal with collections, especially lists of objects. Jackson provides easy and powerful mechanisms to handle JSON arrays and convert them into Java collections, such as List<User>.
1. What Is Collection Handling?
Collection handling refers to the process of deserializing a JSON array into a Java collection (usually a List, Set, or Map) of objects, and serializing a Java collection back into a JSON array.
2. Why Is This Important?
API responses often return lists of items (e.g., list of users, orders, products).
Data persistence can involve storing or reading multiple records in a structured JSON array format.
public class User {
private String name;
private int age;
public User() { }
public User(String name, int age) {
this.name = name;
this.age = age;
}
// Getters and Setters
}
4.1. Deserialize JSON Array into List<User>
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
import java.util.List;
public class JacksonCollectionDeserializationExample {
public static void main(String[] args) {
try {
ObjectMapper objectMapper = new ObjectMapper();
// Read JSON array from file
File jsonFile = new File("users.json");
// Deserialize JSON array into List<User>
List < User > users = objectMapper.readValue(jsonFile, new TypeReference < List < User >> () {});
// Iterate over users
for (User user: users) {
System.out.println("Name: " + user.getName() + ", Age: " + user.getAge());
}
} catch(Exception e) {
e.printStackTrace();
}
}
}
➤ Explanation of Key Concepts:
new TypeReference<List<User>>() {} is necessary because of Java’s type erasure: It tells Jackson the full generic type to deserialize into, since List<User> cannot be determined at runtime without help.
The JSON file (users.json) contains an array of user objects.
would return a List<Map<String, Object>> instead of List<User>.
By using:
new TypeReference<List<User>>() {}
Jackson understands the correct mapping.
6. When to Use Collection Handling
Scenario
Use Case
API returns a list of entities
Deserialize JSON array into List<User>
Bulk data persistence
Serialize List<User> into JSON array in a file
Data transformation
Map JSON array to Java collection, then manipulate data
✅ Conclusion
Handling collections is a critical aspect of real-world JSON processing. Jackson’s combination of TypeReference, ObjectMapper.readValue(), and writeValue() makes it effortless to convert JSON arrays into Java collections and back. This ensures efficient, type-safe handling of structured batch data from APIs, configuration files, or persistent storage.
Jackson provides a set of powerful annotations that help customize how Java objects are serialized into JSON and deserialized back into Java objects. These annotations allow fine-grained control over the mapping behavior without modifying the core logic of your application.
Common Jackson Annotations
✅ @JsonProperty
Purpose: Maps a Java field or getter/setter to a specific JSON property name during serialization and deserialization.
Useful when the JSON property name does not match the Java field name.
Example:
import com.fasterxml.jackson.annotation.JsonProperty;
public class Employee {
@JsonProperty("full_name")
private String name;
private int age;
public Employee() { }
public Employee(String name, int age) {
this.name = name;
this.age = age;
}
// Getters and Setters
}
👉 In this case, the JSON property will be "full_name" instead of "name".
Serialization Example:
ObjectMapper objectMapper = new ObjectMapper();
Employee emp = new Employee("Ashish Kumar", 30);
String jsonString = objectMapper.writeValueAsString(emp);
System.out.println(jsonString);
Output:
{"full_name":"Ashish Kumar","age":30}
✅ @JsonIgnore
Purpose: Prevents a specific field from being serialized (Java to JSON) or deserialized (JSON to Java).
Useful for sensitive data (like passwords) or unnecessary properties.
Example:
import com.fasterxml.jackson.annotation.JsonIgnore;
public class Employee {
private String name;
private int age;
@JsonIgnore
private String password;
public Employee() { }
public Employee(String name, int age, String password) {
this.name = name;
this.age = age;
this.password = password;
}
// Getters and Setters
}
Serialization Example:
ObjectMapper objectMapper = new ObjectMapper();
Employee emp = new Employee("Ashish Kumar", 30, "secure123");
String jsonString = objectMapper.writeValueAsString(emp);
System.out.println(jsonString);
👉 The password field will be excluded from the JSON output and will not be read during deserialization.
Output:
{"name":"Ashish Kumar","age":30}
✅ @JsonInclude
Purpose: Controls the inclusion of properties during serialization based on certain conditions (e.g., exclude null values).
Example usage: Exclude null fields to reduce output size.
Example:
import com.fasterxml.jackson.annotation.JsonInclude;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Employee {
private String name;
private Integer age;
private String department;
public Employee() { }
public Employee(String name, Integer age, String department) {
this.name = name;
this.age = age;
this.department = department;
}
// Getters and Setters
}
Serialization Example:
ObjectMapper objectMapper = new ObjectMapper();
Employee emp = new Employee("Ashish Kumar", 30, null);
String jsonString = objectMapper.writeValueAsString(emp);
System.out.println(jsonString);
👉 Fields with null values (like department if not set) will be excluded from the serialized JSON.
The @JsonIgnoreProperties annotation is used to ignore multiple unknown properties during deserialization.
By default, if the incoming JSON contains fields that do not match any properties in the target Java class, Jackson throws an exception (UnrecognizedPropertyException).
Adding @JsonIgnoreProperties(ignoreUnknown = true) prevents this exception by simply ignoring any unknown fields.
➤ Why Is It Useful?
Useful when the JSON comes from an external source (like a third-party API) that may include additional fields your Java class does not care about.
Avoids tightly coupling your Java model to every field in the JSON.
Example:
JSON Input:
{
"name": "Ashish Kumar",
"age": 30,
"extra": "this field is not mapped in Java class"
}
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public class Employee {
private String name;
private int age;
// Getters and Setters
}
Behavior:
The extra field will be ignored during deserialization.
No exception will be thrown, and only name and age will be mapped.
✅ @JsonCreator and @JsonProperty Combination
These annotations are used when deserializing into immutable objects or when the target class does not have a default (no-argument) constructor.
@JsonCreator: Marks the constructor to be used for deserialization.
@JsonProperty: Specifies how the constructor arguments map to JSON properties.
➤ Why Is It Useful?
Ensures proper deserialization when using final fields or classes designed to be immutable.
Promotes safe object construction without relying on setters.
JSON Input:
{
"name": "Ashish Kumar",
"age": 30
}
Java Class:
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class Employee {
private final String name;
private final int age;
@JsonCreator
public Employee(@JsonProperty("name") String name, @JsonProperty("age") int age) {
this.name = name;
this.age = age;
}
// Getters
public String getName() { return name; }
public int getAge() { return age; }
}
Behavior:
Jackson uses the annotated constructor to instantiate the object.
No need for a default constructor or setters.
Allows fully immutable objects while still supporting deserialization.
✅ Conclusion
@JsonProperty → Maps a Java field to a specific JSON property name.
@JsonIgnore → Prevents a field from being serialized or deserialized.
@JsonInclude → Controls inclusion of properties during serialization (e.g., omit nulls).
These annotations remain stable and correct in the latest Jackson versions (e.g., 2.15.x) and Java versions (e.g., Java 17 or later).
@JsonIgnoreProperties(ignoreUnknown = true) helps prevent errors caused by unexpected fields in JSON.
@JsonCreator with @JsonProperty enables deserialization into immutable objects by mapping constructor arguments directly from JSON properties.
Jackson is a widely used Java library for processing JSON data. It provides easy-to-use APIs for serializing Java objects to JSON and deserializing JSON into Java objects. Jackson is popular due to its high performance, flexible configuration, and powerful data-binding features.
What is Jackson?
Jackson is a high-performance JSON processor for Java. It helps convert Java objects to JSON and vice versa. The core class is ObjectMapper, and it also provides useful annotations for customization.
readTree(String json) → Parse JSON into a tree of JsonNode.
2. Serialization (Marshalling)
What is Serialization?
Serialization is the process of converting a Java object into a format (in this case, JSON) that can be easily stored or transmitted, and later reconstructed back into the original object. In the context of Jackson, serialization is also known as marshalling.
When working with APIs, databases, or file storage, JSON is commonly used to represent structured data in a lightweight and human-readable format. Jackson’s ObjectMapper makes this conversion simple and efficient.
2.1 Serialization Java Object to JSON
✔️ Example User.java Class
public class User {
private String name;
private int age;
public User() {}
public User(String name, int age) {
this.name = name;
this.age = age;
}
// Getters and Setters
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
✔️ Serialization Example
import com.fasterxml.jackson.databind.ObjectMapper;
public class JacksonSerializationExample {
public static void main(String[] args) throws Exception {
ObjectMapper objectMapper = new ObjectMapper();//Create an instance of ObjectMapper
User user = new User("Ashish Kumar", 30);
String jsonString = objectMapper.writeValueAsString(user);//Serialize the Object to JSON String
System.out.println("Serialized JSON: " + jsonString);
}
}
➔ Output:
{"name":"Ashish Kumar","age":30}
2.2 Writing JSON to a File
⚡ Instead of working only with strings, Jackson allows you to directly write JSON into files using:
✔️ Example
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
public class JacksonWriteToFileExample {
public static void main(String[] args) {
try {
// Create ObjectMapper instance
ObjectMapper objectMapper = new ObjectMapper();
// Create a User object
User user = new User("Ashish Kumar", 30);
// Write the User object as JSON to the file "user.json"
objectMapper.writeValue(new File("user.json"), user);
System.out.println("JSON file has been written successfully.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
➔Output : Resulting File Content (user.json):
{"name":"Ashish Kumar","age":30}
👉This approach is helpful when persisting data or creating configuration files.
✅ Why Is This Useful?
Data Transmission: Serialized JSON is easy to send over HTTP in REST APIs.
Data Storage: Persist objects in a JSON file for later retrieval.
Logging and Debugging: Easily log object states in a readable format.
Configuration Files: Store application settings in JSON.
3. Deserialization (Unmarshalling)
What is Deserialization?
Deserialization is the process of converting JSON data into a Java object. In the context of Jackson, this process is often called unmarshalling. It allows us to take a structured JSON string (or file) and transform it into a corresponding Java object so that we can easily work with the data in a type-safe manner.
✅ Why Is Deserialization Important?
Reading API Responses: When calling REST APIs, the response is often in JSON format. To work with the data in Java, we deserialize it into objects.
Reading Configuration Files: JSON configuration files are commonly used for application settings.
Data Persistence: Deserialize stored JSON files back into Java objects for processing or display.
✅ How Does Jackson Perform Deserialization?
Once Jackson is imported and the ObjectMapper is available, we use it to convert JSON strings or files into Java objects.
3.1 Deserialization JSON String to Java Object
✔️ Example JSON String
{"name":"Simone","age":28}
Deserialization Example
import com.fasterxml.jackson.databind.ObjectMapper;
public class JacksonDeserializationExample {
public static void main(String[] args) {
try {
ObjectMapper objectMapper = new ObjectMapper();
String jsonString = "{\"name\":\"Bhasker\",\"age\":28}";
User user = objectMapper.readValue(jsonString, User.class);
System.out.println("Name: " + user.getName());
System.out.println("Age: " + user.getAge());
} catch (Exception e) {
e.printStackTrace();
}
}
}
Output:
Name: Bhasker
Age: 28
👉This shows how the JSON string is parsed and converted into a User object.
3.2 Deserializing from a JSON File to Java Object
✔️ Example JSON File (user.json):
{
"name": "Bhasker",
"age": 28
}
✔️ Example Java Code:
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
public class JacksonReadFileExample {
public static void main(String[] args) {
try {
ObjectMapper objectMapper = new ObjectMapper();
File jsonFile = new File("user.json");
User user = objectMapper.readValue(jsonFile, User.class);
System.out.println("Name: " + user.getName());
System.out.println("Age: " + user.getAge());
} catch (Exception e) {
e.printStackTrace();
}
}
}
👉This approach is helpful when working with large or persistent JSON data stored in files.
Output :
Name: Bhasker
Age: 28
✅ How Does Jackson Know How to Map JSON to Java Object?
Jackson relies on:
Default Constructor: The target class must have a no-argument constructor (can be implicit).
Getters and Setters: Jackson uses public setters to set values during deserialization.
Field Matching: JSON property names must match Java field names, unless annotations (like @JsonProperty) are used to map them explicitly.
4. Jackson Annotations
Jackson provides useful annotations to control serialization and deserialization.
Common Jackson Annotations
@JsonProperty → Maps a Java field to a specific JSON property name
@JsonIgnore → Prevents a specific field from being serialized or deserialized.
@JsonInclude → Controls inclusion of properties during serialization (e.g., omit nulls).
@JsonIgnoreProperties → Prevents multiple unknown fields from causing exceptions during deserialization.
@JsonCreator → Useful when deserializing immutable objects with final fields and no default constructor.
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
public class JacksonTreeExample {
public static void main(String[] args) throws Exception {
String json = "{\"name\":\"Ashish\",\"age\":30}";
ObjectMapper mapper = new ObjectMapper();
JsonNode node = mapper.readTree(json);
System.out.println("Name: " + node.get("name").asText());
System.out.println("Age: " + node.get("age").asInt());
}
}
Output :
Name: Ashish
Age: 30
6. Handling Collections
✅ What Is Collection Handling?
Collection handling refers to the process of deserializing a JSON array into a Java collection (usually a List, Set, or Map) of objects, and serializing a Java collection back into a JSON array.
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
public class JacksonListExample {
public static void main(String[] args) throws Exception {
String json = "[{\"name\":\"Bhasker\",\"age\":28}, {\"name\":\"Ashish\",\"age\":30}]";
ObjectMapper mapper = new ObjectMapper();
List < User > users = mapper.readValue(json, new TypeReference < List < User >> () {});
for (User u: users) {
System.out.println(u.getName() + " - " + u.getAge());
}
}
}
Output :
Bhasker - 28
Ashish - 30
7. Reading List of Objects from JSON File
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
import java.util.List;
public class JacksonReadListFromFileExample {
public static void main(String[] args) throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
File jsonFile = new File("users.json");
List<User> users = objectMapper.readValue(jsonFile, new TypeReference<List<User>>() {});
for (User user : users) {
System.out.println(user.getName() + " - " + user.getAge());
}
}
}
8. Pretty Printing JSON
When working with JSON data, especially for debugging, configuration files, or manual inspection, a compact JSON string without any formatting can be hard to read. By default, Jackson produces a compact JSON output like this:
{"name":"Ashish Kumar","age":30}
This is fine for machine processing but not ideal for humans. To make the JSON output more readable, we use Pretty Printing to add line breaks, indentation, and proper spacing.
✔️ Example
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
public class JacksonPrettyPrintExample {
public static void main(String[] args) throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
User user = new User("Ashish Kumar", 30);
String prettyJson = objectMapper.writeValueAsString(user);
System.out.println(prettyJson);
}
}
Output:
{
"name" : "Ashish Kumar",
"age" : 30
}
9. Common Exceptions to Handle
JsonProcessingException
UnrecognizedPropertyException
MismatchedInputException
Summary of ObjectMapper Usage
Operation
Method Example
Serialize Object to String
writeValueAsString(obj)
Serialize Object to File
writeValue(new File("output.json"), obj)
Deserialize String to Object
readValue(jsonString, User.class)
Deserialize File to Object
readValue(new File("user.json"), User.class)
Read Tree Model
readTree(jsonString)
Deserialize Array from File
readValue(new File("users.json"), new TypeReference<List<User>>(){})
Pretty Print
Enable SerializationFeature.INDENT_OUTPUT
🎯Conclusion
Jackson is a powerful and flexible library that makes working with JSON in Java effortless. By mastering serialization, deserialization, annotations, tree model, file handling, collections, and pretty printing, you can efficiently handle JSON data in any Java project.