PCSalt
YouTube GitHub
Back to Java
Java · 2 min read

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:

  1. JavaTimeModule — handles Instant, LocalDate, ZonedDateTime, and all other java.time types. No custom adapter needed. This is Jackson’s biggest advantage over Gson and Moshi for date handling.

  2. 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 to false makes 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": null becomes social.twitter() == null
  • "billingCycle": null becomes subscription.billingCycle() == null
  • "lastLoginAt": null becomes user.lastLoginAt() == null
  • "teams": [] becomes an empty List<Team>, not null

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:

FeatureJacksonGsonMoshi
java.time supportBuilt-in moduleCustom TypeAdapterCustom adapter
Annotation@JsonProperty@SerializedName@Json
Unknown fieldsConfigurableIgnored by defaultStrict by default
Streaming APIYesYesYes
Tree model (like org.json)JsonNodeJsonElementNo
Spring Boot defaultYesNoNo
Size~1.5 MB~260 KB~170 KB

When to use Jackson:

  • Server-side Java (Spring Boot, Micronaut, Quarkus)
  • When you need java.time support 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.