PCSalt
YouTube GitHub
Back to Java
Java · 2 min read

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.json is built into the Android SDK — no dependency needed. Just use import 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:

WhatCount
Record classes8
Parse methods9
Explicit getString/getInt/getBoolean calls~30
Null checks with optString/isNull8
Manual array loops4
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 — use optString() for nullable fields, but it returns "" instead of null by default
  • optString("key", null) still returns "null" (the string) if the JSON value is null — you need isNull() 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.