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.gradleand 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": nullbecomessocial.twitter() == null"billingCycle": nullbecomessubscription.billingCycle() == null"lastLoginAt": nullbecomesuser.lastLoginAt() == null"teams": []becomes an emptyList<Team>, notnull
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
| What | org.json | Gson |
|---|---|---|
| Parse methods needed | 9 | 1 |
| Lines of parsing code | ~100 | ~10 |
| Null handling | Manual (optString, isNull) | Automatic |
| Enum mapping | Manual | Automatic |
| Date handling | Manual Instant.parse() | One TypeAdapter |
| Array handling | Manual loops | Automatic |
| Nested objects | Manual recursion | Automatic |
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.