PCSalt
YouTube GitHub
Back to Java
Java · 2 min read

JSON Parsing in Java — Parsing with Gson

Parse a real-world JSON response using Gson in Java 21. Automatic mapping, annotations, and custom adapters — the easy way.


This is Part 2 of the JSON Parsing in Java series. In the previous post, we parsed a complex JSON response manually with org.json — about 100 lines of getString(), null checks, and loops. Now let’s do the same thing with Gson.

Looking for the Kotlin version? Read it here.

The JSON

Same JSON as before — a SaaS API response with users, subscriptions, teams, and pagination. Nested objects, arrays, nullable fields, enums, dates, and decimals.

{
  "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

Add the Gson dependency. If you’re using Maven:

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.11.0</version>
</dependency>

Or Gradle:

implementation 'com.google.code.gson:gson:2.11.0'

Android note: Gson works out of the box on Android. Just add the dependency above to your module-level build.gradle and you’re good to go.

Model Classes

With Gson, we still define record classes — but we don’t write any parsing code. Gson maps JSON keys to record fields automatically. For fields where the JSON key doesn’t match Java naming conventions, we use @SerializedName.

import com.google.gson.annotations.SerializedName;

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,
    @SerializedName("billingCycle") BillingCycle billingCycle,
    double price,
    List<String> features,
    @SerializedName("trialEndsAt") Instant trialEndsAt
) {}

public record Team(
    String id,
    String name,
    Role role,
    @SerializedName("memberCount") int memberCount
) {}

public record User(
    String id,
    String name,
    String email,
    Role role,
    boolean verified,
    @SerializedName("createdAt") Instant createdAt,
    Profile profile,
    Subscription subscription,
    List<Team> teams,
    @SerializedName("lastLoginAt") Instant lastLoginAt
) {}

public record Pagination(
    int page,
    @SerializedName("perPage") int perPage,
    @SerializedName("totalItems") int totalItems,
    @SerializedName("totalPages") int totalPages,
    @SerializedName("hasNext") boolean hasNext
) {}

public record Meta(
    @SerializedName("requestId") String requestId,
    Instant timestamp,
    @SerializedName("apiVersion") String apiVersion
) {}

public record Data(
    List<User> users,
    Pagination pagination
) {}

public record ApiResponse(
    String status,
    Data data,
    Meta meta
) {}

Notice that role, plan, and billingCycle are now proper enums. Gson maps "ADMIN" to Role.ADMIN automatically — no manual conversion needed.

Custom TypeAdapter for Instant

Gson doesn’t know how to handle java.time.Instant out of the box. We need a small TypeAdapter to parse ISO 8601 date strings:

import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;
import java.time.Instant;

public class InstantAdapter extends TypeAdapter<Instant> {

  @Override
  public void write(JsonWriter out, Instant value) throws IOException {
    if (value == null) {
      out.nullValue();
    } else {
      out.value(value.toString());
    }
  }

  @Override
  public Instant read(JsonReader in) throws IOException {
    if (in.peek() == JsonToken.NULL) {
      in.nextNull();
      return null;
    }
    return Instant.parse(in.nextString());
  }
}

Parsing

Here’s the parsing code. Ready?

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import java.time.Instant;

public class GsonParser {

  private static final Gson gson = new GsonBuilder()
      .registerTypeAdapter(Instant.class, new InstantAdapter())
      .create();

  public static ApiResponse parse(String json) {
    return gson.fromJson(json, ApiResponse.class);
  }
}

That’s it. In the previous post, this took about 100 lines. With Gson, it’s about 10.

No getString(). No optString(). No isNull(). No loops over JSONArray. Gson handles all of it — nested objects, arrays, nulls, enums — automatically.

Handling Nulls

Gson handles nulls gracefully by default. If a JSON value is null, the corresponding Java field will be null. No special handling needed.

  • "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

No optString(), no isNull() checks, no worrying about "null" vs null. It just works.

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 = GsonParser.parse(json);

    System.out.println("Status: " + response.status());
    System.out.println("API Version: " + response.data().pagination().totalItems());
    System.out.println("Total users: " + response.meta().apiVersion());
    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

Same output as the org.json version. A fraction of the code.

Gson vs org.json

Whatorg.jsonGson
Parse methods needed91
Lines of parsing code~100~10
Null handlingManual (optString, isNull)Automatic
Enum mappingManualAutomatic
Date handlingManual Instant.parse()One TypeAdapter
Array handlingManual loopsAutomatic
Nested objectsManual recursionAutomatic

Using in Android

Gson is one of the most popular JSON libraries on Android. Add the dependency and use it exactly as shown above.

// In an Android context:
String json = responseBody.string(); // from OkHttp
ApiResponse response = GsonParser.parse(json);

Gson is also the default choice for many older Android projects and plays well with Retrofit:

import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .addConverterFactory(GsonConverterFactory.create(gson))
    .build();

What’s Next

In the next post, we’ll parse the same JSON using Moshi — a modern library from Square that’s built for both Java and Kotlin, and is the go-to choice for many Android projects using Retrofit.