JSON Parsing in Java — Manual Parsing with org.json
Parse a complex, real-world JSON response manually using org.json in Java 21. Every getString, every null check, every nested loop — the hard way.
This is Part 1 of the JSON Parsing in Java series. We start with the hardest approach on purpose — manual parsing with org.json. By the end of this post, you’ll understand exactly why libraries like Gson, Moshi, and Jackson exist.
Looking for the Kotlin version? Read it here.
The JSON
Throughout this series, we’ll parse the same JSON — a typical SaaS API response with users, subscriptions, teams, and pagination. It has 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 org.json dependency. If you’re using Maven:
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20240303</version>
</dependency>
Or Gradle:
implementation 'org.json:json:20240303'
Android note:
org.jsonis built into the Android SDK — no dependency needed. Just useimport org.json.JSONObject.
Model Classes
First, let’s define our data classes. With org.json, there’s no automatic mapping — we build these by hand and populate them ourselves.
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 record Subscription(
String plan,
String billingCycle,
double price,
List<String> features,
Instant trialEndsAt
) {}
public record Team(
String id,
String name,
String role,
int memberCount
) {}
public record User(
String id,
String name,
String email,
String role,
boolean verified,
Instant createdAt,
Profile profile,
Subscription subscription,
List<Team> teams,
Instant lastLoginAt
) {}
public record Pagination(
int page,
int perPage,
int totalItems,
int totalPages,
boolean hasNext
) {}
public record Meta(
String requestId,
Instant timestamp,
String apiVersion
) {}
public record ApiResponse(
String status,
List<User> users,
Pagination pagination,
Meta meta
) {}
That’s 8 record classes just to hold the data. Now comes the fun part — writing the parsing code.
Parsing
Here’s where the pain begins. Every field needs an explicit get call. Every nullable field needs isNull() or optString(). Every nested object needs its own parsing block. Every array needs a loop.
import org.json.JSONArray;
import org.json.JSONObject;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
public class OrgJsonParser {
public static ApiResponse parse(String jsonString) {
JSONObject root = new JSONObject(jsonString);
String status = root.getString("status");
JSONObject data = root.getJSONObject("data");
List<User> users = parseUsers(data.getJSONArray("users"));
Pagination pagination = parsePagination(data.getJSONObject("pagination"));
Meta meta = parseMeta(root.getJSONObject("meta"));
return new ApiResponse(status, users, pagination, meta);
}
private static List<User> parseUsers(JSONArray usersArray) {
List<User> users = new ArrayList<>();
for (int i = 0; i < usersArray.length(); i++) {
users.add(parseUser(usersArray.getJSONObject(i)));
}
return users;
}
private static User parseUser(JSONObject obj) {
return new User(
obj.getString("id"),
obj.getString("name"),
obj.getString("email"),
obj.getString("role"),
obj.getBoolean("verified"),
parseInstant(obj, "createdAt"),
parseProfile(obj.getJSONObject("profile")),
parseSubscription(obj.getJSONObject("subscription")),
parseTeams(obj.getJSONArray("teams")),
parseInstant(obj, "lastLoginAt")
);
}
private static Profile parseProfile(JSONObject obj) {
return new Profile(
obj.optString("avatar", null),
obj.optString("bio", null),
parseSocial(obj.getJSONObject("social"))
);
}
private static Social parseSocial(JSONObject obj) {
return new Social(
obj.optString("github", null),
obj.optString("twitter", null),
obj.optString("linkedin", null)
);
}
private static Subscription parseSubscription(JSONObject obj) {
List<String> features = new ArrayList<>();
JSONArray featuresArray = obj.getJSONArray("features");
for (int i = 0; i < featuresArray.length(); i++) {
features.add(featuresArray.getString(i));
}
return new Subscription(
obj.getString("plan"),
obj.optString("billingCycle", null),
obj.getDouble("price"),
features,
parseInstant(obj, "trialEndsAt")
);
}
private static List<Team> parseTeams(JSONArray teamsArray) {
List<Team> teams = new ArrayList<>();
for (int i = 0; i < teamsArray.length(); i++) {
JSONObject teamObj = teamsArray.getJSONObject(i);
teams.add(new Team(
teamObj.getString("id"),
teamObj.getString("name"),
teamObj.getString("role"),
teamObj.getInt("memberCount")
));
}
return teams;
}
private static Pagination parsePagination(JSONObject obj) {
return new Pagination(
obj.getInt("page"),
obj.getInt("perPage"),
obj.getInt("totalItems"),
obj.getInt("totalPages"),
obj.getBoolean("hasNext")
);
}
private static Meta parseMeta(JSONObject obj) {
return new Meta(
obj.getString("requestId"),
parseInstant(obj, "timestamp"),
obj.getString("apiVersion")
);
}
private static Instant parseInstant(JSONObject obj, String key) {
if (obj.isNull(key)) {
return null;
}
return Instant.parse(obj.getString(key));
}
}
That’s ~100 lines of parsing code for one JSON response. And we haven’t even added error handling yet.
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 = OrgJsonParser.parse(json);
System.out.println("Status: " + response.status());
System.out.println("API Version: " + response.meta().apiVersion());
System.out.println("Total users: " + response.pagination().totalItems());
System.out.println();
for (User user : response.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
The Problems with Manual Parsing
Let’s count what we had to do:
| What | Count |
|---|---|
| Record classes | 8 |
| Parse methods | 9 |
Explicit getString/getInt/getBoolean calls | ~30 |
Null checks with optString/isNull | 8 |
| Manual array loops | 4 |
| Lines of parsing code | ~100 |
And this is for a single API response. In a real app with 10+ endpoints, this becomes unmaintainable. Add a field to the JSON? You need to update the record class AND the parse method. Rename a field? Find every getString("oldName") by hand. Miss a null check? Runtime crash.
Common pitfalls
getString()throws if the key is missing or null — useoptString()for nullable fields, but it returns""instead ofnullby defaultoptString("key", null)still returns"null"(the string) if the JSON value isnull— you needisNull()first for true null safety- No type safety — typo in a key name? You’ll only find out at runtime
- No date handling — you parse strings and call
Instant.parse()yourself - No enum mapping —
"ADMIN"stays as a string unless you convert manually
Using in Android
org.json is part of the Android SDK — no dependency needed. The code above works as-is in any Android project. Just replace Files.readString() with however you get your JSON (Retrofit, OkHttp, etc.).
// In an Android context, you'd typically get JSON from a network call:
String json = responseBody.string(); // from OkHttp
ApiResponse response = OrgJsonParser.parse(json);
What’s Next
In the next post, we’ll parse this exact same JSON using Gson — and you’ll see the parsing code shrink from 100 lines to about 10. Same result, fraction of the effort.