JSON Parsing in Java — Parsing with Moshi
Parse a real-world JSON response using Moshi in Java 21. Clean annotations, custom adapters, and built-in null safety.
This is Part 3 of the JSON Parsing in Java series. We’ve seen manual parsing with org.json and automatic mapping with Gson. Now let’s try Moshi — a modern JSON library from Square, the same team behind OkHttp and Retrofit.
Looking for the Kotlin version? Read it here.
The JSON
Same JSON as the previous posts — 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
Add the Moshi dependency. If you’re using Maven:
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi</artifactId>
<version>1.15.1</version>
</dependency>
Or Gradle:
implementation 'com.squareup.moshi:moshi:1.15.1'
Android note: Moshi is the preferred JSON library for modern Android development. It pairs naturally with Retrofit and OkHttp (all from Square). Add the dependency above and you’re ready.
Model Classes
Moshi uses @Json annotations for field mapping. Like Gson, it handles nested objects, arrays, and nulls automatically.
import com.squareup.moshi.Json;
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,
@Json(name = "billingCycle") BillingCycle billingCycle,
double price,
List<String> features,
@Json(name = "trialEndsAt") Instant trialEndsAt
) {}
public record Team(
String id,
String name,
Role role,
@Json(name = "memberCount") int memberCount
) {}
public record User(
String id,
String name,
String email,
Role role,
boolean verified,
@Json(name = "createdAt") Instant createdAt,
Profile profile,
Subscription subscription,
List<Team> teams,
@Json(name = "lastLoginAt") Instant lastLoginAt
) {}
public record Pagination(
int page,
@Json(name = "perPage") int perPage,
@Json(name = "totalItems") int totalItems,
@Json(name = "totalPages") int totalPages,
@Json(name = "hasNext") boolean hasNext
) {}
public record Meta(
@Json(name = "requestId") String requestId,
Instant timestamp,
@Json(name = "apiVersion") String apiVersion
) {}
public record Data(
List<User> users,
Pagination pagination
) {}
public record ApiResponse(
String status,
Data data,
Meta meta
) {}
Very similar to the Gson version — just @Json(name = ...) instead of @SerializedName(...).
Custom Adapter for Instant
Like Gson, Moshi doesn’t handle java.time.Instant out of the box. We write a custom adapter:
import com.squareup.moshi.FromJson;
import com.squareup.moshi.ToJson;
import java.time.Instant;
public class InstantAdapter {
@FromJson
Instant fromJson(String value) {
return value == null ? null : Instant.parse(value);
}
@ToJson
String toJson(Instant value) {
return value == null ? null : value.toString();
}
}
Notice how clean this is compared to Gson’s TypeAdapter. No JsonReader, no JsonWriter, no JsonToken.NULL checks. Just two annotated methods.
Parsing
Build a Moshi instance with our adapter and create a JsonAdapter:
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import java.io.IOException;
public class MoshiParser {
private static final Moshi moshi = new Moshi.Builder()
.add(new InstantAdapter())
.build();
private static final JsonAdapter<ApiResponse> adapter =
moshi.adapter(ApiResponse.class);
public static ApiResponse parse(String json) throws IOException {
return adapter.fromJson(json);
}
}
The key difference from Gson: Moshi uses a typed JsonAdapter<ApiResponse> instead of passing ApiResponse.class every time you parse. This makes the adapter reusable and slightly more type-safe.
Handling Nulls
Moshi handles nulls cleanly. If a JSON value is null, the corresponding Java field will be null:
"twitter": nullbecomessocial.twitter() == null"billingCycle": nullbecomessubscription.billingCycle() == null"lastLoginAt": nullbecomesuser.lastLoginAt() == null"teams": []becomes an emptyList<Team>, notnull
One thing Moshi does differently: it’s strict about missing keys by default. If a key is completely absent from the JSON (not null, but missing entirely), Moshi will throw an error for non-nullable fields. This catches bugs early instead of silently returning 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 = MoshiParser.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
Why Moshi over Gson?
Both libraries give you automatic mapping, but Moshi has a few advantages:
| Feature | Gson | Moshi |
|---|---|---|
| Null safety | Lenient — silently allows nulls | Strict — catches missing fields |
| Custom adapters | TypeAdapter (verbose) | @FromJson/@ToJson (concise) |
| Kotlin support | Basic | First-class (kotlin-codegen) |
| Streaming | Supported | Supported |
| Maintained by | Square | |
| APK size impact | ~260 KB | ~170 KB |
For new projects — especially Android — Moshi is generally the better choice. It’s stricter, smaller, and has better Kotlin support.
Using in Android
Moshi is the go-to JSON library for modern Android projects, especially when paired with Retrofit:
import retrofit2.Retrofit;
import retrofit2.converter.moshi.MoshiConverterFactory;
Moshi moshi = new Moshi.Builder()
.add(new InstantAdapter())
.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build();
Add the Retrofit converter dependency:
implementation 'com.squareup.retrofit2:converter-moshi:2.11.0'
What’s Next
In the next post, we’ll parse the same JSON using Jackson — the powerhouse library that dominates server-side Java. It’s the most feature-rich option, but also the heaviest.