JSON Parsing in Java — Parsing with Jackson
Parse a real-world JSON response using Jackson in Java 21. ObjectMapper, annotations, and the JavaTimeModule for dates.
This is Part 4 of the JSON Parsing in Java series. We’ve covered manual parsing with org.json, Gson, and Moshi. Now let’s look at Jackson — the most widely used JSON library in the Java ecosystem, especially on the server side.
Looking for the Kotlin version? Read it here.
The JSON
Same JSON as every other post in this series — a SaaS API response with users, subscriptions, teams, and pagination.
{
"status": "success",
"data": {
"users": [
{
"id": "usr_a1b2c3d4",
"name": "Navkrishna",
"email": "[email protected]",
"role": "ADMIN",
"verified": true,
"createdAt": "2026-01-15T10:30:00Z",
"profile": {
"avatar": "https://cdn.example.com/avatars/nav.jpg",
"bio": "Full-stack developer",
"social": {
"github": "krrishnaaaa",
"twitter": null,
"linkedin": "navkrishna"
}
},
"subscription": {
"plan": "PRO",
"billingCycle": "YEARLY",
"price": 199.99,
"features": ["analytics", "api_access", "priority_support"],
"trialEndsAt": null
},
"teams": [
{
"id": "team_x1y2",
"name": "Backend",
"role": "OWNER",
"memberCount": 5
},
{
"id": "team_z3w4",
"name": "Mobile",
"role": "MEMBER",
"memberCount": 3
}
],
"lastLoginAt": "2026-03-14T18:45:00Z"
},
{
"id": "usr_e5f6g7h8",
"name": "Priya Sharma",
"email": "[email protected]",
"role": "MEMBER",
"verified": false,
"createdAt": "2026-03-01T14:00:00Z",
"profile": {
"avatar": null,
"bio": null,
"social": {
"github": "priya-dev",
"twitter": "priyacodes",
"linkedin": null
}
},
"subscription": {
"plan": "TRIAL",
"billingCycle": null,
"price": 0.0,
"features": ["analytics"],
"trialEndsAt": "2026-03-31T23:59:59Z"
},
"teams": [],
"lastLoginAt": null
}
],
"pagination": {
"page": 1,
"perPage": 20,
"totalItems": 2,
"totalPages": 1,
"hasNext": false
}
},
"meta": {
"requestId": "req_9k8j7h6g",
"timestamp": "2026-03-15T00:00:00Z",
"apiVersion": "v2"
}
}
Setup
Jackson is modular. You need jackson-databind for the core mapping and jackson-datatype-jsr310 for java.time support.
Maven:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.17.0</version>
</dependency>
Gradle:
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.0'
Android note: Jackson works on Android, but it’s the heaviest of the three libraries we’ve covered (~1.5 MB vs ~260 KB for Gson and ~170 KB for Moshi). If you’re building a mobile app, Gson or Moshi are usually better choices. If you’re on a server with Spring Boot, Jackson is already included — no extra dependency needed.
Model Classes
Jackson uses @JsonProperty for field mapping. It also supports automatic camelCase mapping, but explicit annotations make the intent clear.
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.Instant;
import java.util.List;
public record Social(
String github,
String twitter,
String linkedin
) {}
public record Profile(
String avatar,
String bio,
Social social
) {}
public enum BillingCycle {
YEARLY,
MONTHLY,
QUARTERLY
}
public enum Plan {
TRIAL,
BASIC,
PRO,
ENTERPRISE
}
public enum Role {
ADMIN,
MEMBER,
OWNER,
VIEWER
}
public record Subscription(
Plan plan,
@JsonProperty("billingCycle") BillingCycle billingCycle,
double price,
List<String> features,
@JsonProperty("trialEndsAt") Instant trialEndsAt
) {}
public record Team(
String id,
String name,
Role role,
@JsonProperty("memberCount") int memberCount
) {}
public record User(
String id,
String name,
String email,
Role role,
boolean verified,
@JsonProperty("createdAt") Instant createdAt,
Profile profile,
Subscription subscription,
List<Team> teams,
@JsonProperty("lastLoginAt") Instant lastLoginAt
) {}
public record Pagination(
int page,
@JsonProperty("perPage") int perPage,
@JsonProperty("totalItems") int totalItems,
@JsonProperty("totalPages") int totalPages,
@JsonProperty("hasNext") boolean hasNext
) {}
public record Meta(
@JsonProperty("requestId") String requestId,
Instant timestamp,
@JsonProperty("apiVersion") String apiVersion
) {}
public record Data(
List<User> users,
Pagination pagination
) {}
public record ApiResponse(
String status,
Data data,
Meta meta
) {}
Same structure as Gson and Moshi — just different annotation names.
Parsing
Jackson’s core class is ObjectMapper. We configure it once and reuse it:
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.io.IOException;
public class JacksonParser {
private static final ObjectMapper mapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
public static ApiResponse parse(String json) throws IOException {
return mapper.readValue(json, ApiResponse.class);
}
}
Two key things here:
-
JavaTimeModule— handlesInstant,LocalDate,ZonedDateTime, and all otherjava.timetypes. No custom adapter needed. This is Jackson’s biggest advantage over Gson and Moshi for date handling. -
FAIL_ON_UNKNOWN_PROPERTIES = false— by default, Jackson throws an exception if the JSON has a field that doesn’t exist in your record. Setting this tofalsemakes it ignore unknown fields, which is safer when APIs evolve.
Handling Nulls
Jackson handles nulls the same way as Gson and Moshi — null in JSON becomes null in Java:
"twitter": nullbecomessocial.twitter() == null"billingCycle": nullbecomessubscription.billingCycle() == null"lastLoginAt": nullbecomesuser.lastLoginAt() == null"teams": []becomes an emptyList<Team>, notnull
Putting It Together
import java.nio.file.Files;
import java.nio.file.Path;
public class Main {
public static void main(String[] args) throws Exception {
String json = Files.readString(Path.of("response.json"));
ApiResponse response = JacksonParser.parse(json);
System.out.println("Status: " + response.status());
System.out.println("API Version: " + response.meta().apiVersion());
System.out.println("Total users: " + response.data().pagination().totalItems());
System.out.println();
for (User user : response.data().users()) {
System.out.println("--- " + user.name() + " ---");
System.out.println(" Email: " + user.email());
System.out.println(" Role: " + user.role());
System.out.println(" Verified: " + user.verified());
System.out.println(" Plan: " + user.subscription().plan());
System.out.println(" Price: $" + user.subscription().price());
System.out.println(" Features: " + user.subscription().features());
System.out.println(" GitHub: " + user.profile().social().github());
System.out.println(" Teams: " + user.teams().size());
for (Team team : user.teams()) {
System.out.println(" - " + team.name() + " (" + team.role() + ")");
}
System.out.println(" Last login: " + user.lastLoginAt());
System.out.println();
}
}
}
Output
Status: success
API Version: v2
Total users: 2
--- Navkrishna ---
Email: [email protected]
Role: ADMIN
Verified: true
Plan: PRO
Price: $199.99
Features: [analytics, api_access, priority_support]
GitHub: krrishnaaaa
Teams: 2
- Backend (OWNER)
- Mobile (MEMBER)
Last login: 2026-03-14T18:45:00Z
--- Priya Sharma ---
Email: [email protected]
Role: MEMBER
Verified: false
Plan: TRIAL
Price: $0.0
Features: [analytics]
GitHub: priya-dev
Teams: 0
Last login: null
Jackson vs Gson vs Moshi
Here’s how Jackson stacks up against the other libraries we’ve covered:
| Feature | Jackson | Gson | Moshi |
|---|---|---|---|
java.time support | Built-in module | Custom TypeAdapter | Custom adapter |
| Annotation | @JsonProperty | @SerializedName | @Json |
| Unknown fields | Configurable | Ignored by default | Strict by default |
| Streaming API | Yes | Yes | Yes |
| Tree model (like org.json) | JsonNode | JsonElement | No |
| Spring Boot default | Yes | No | No |
| Size | ~1.5 MB | ~260 KB | ~170 KB |
When to use Jackson:
- Server-side Java (Spring Boot, Micronaut, Quarkus)
- When you need
java.timesupport without custom adapters - When you need advanced features like polymorphic type handling, custom serializers, or XML support
When to use Gson or Moshi instead:
- Android apps where APK size matters
- Simple JSON parsing without advanced features
Using in Android
Jackson works on Android, but consider the size trade-off. If you’re already using Jackson on your backend and want consistency, it’s fine. Otherwise, Gson or Moshi are lighter alternatives.
// In an Android context:
String json = responseBody.string(); // from OkHttp
ApiResponse response = JacksonParser.parse(json);
For Retrofit:
import retrofit2.Retrofit;
import retrofit2.converter.jackson.JacksonConverterFactory;
ObjectMapper mapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(JacksonConverterFactory.create(mapper))
.build();
Add the Retrofit converter dependency:
implementation 'com.squareup.retrofit2:converter-jackson:2.11.0'
What’s Next
That wraps up the Java JSON parsing series. We went from 100 lines of manual parsing with org.json, down to about 10 lines with Gson, Moshi, and Jackson. Each library has its sweet spot — Gson for simplicity, Moshi for Android, Jackson for the server.
Next up, we’ll do the same series in Kotlin — where things get even more concise with data classes, null safety, and Kotlin-specific adapters.