JSON to Java Classes: Generate POJOs, Records, and Lombok @Data
Convert any JSON payload into typed Java classes with Jackson or Gson annotations, correct boxing, nested objects, and generated getters and setters.
JSON to Java Classes: Generate POJOs, Records, and Lombok @Data
Every Java developer who has ever consumed an HTTP API knows the chore. A REST endpoint returns a 30-field payload, and now you have to translate that JSON into a .java file by hand: declare each field with the right type, decide whether a number is an int or a long, write a @JsonProperty for every snake_case key, generate getters and setters, and lift each nested object into its own class. Get one type wrong and Jackson either throws at deserialization or, worse, silently binds a wrong value. This guide walks through how to turn JSON into Java classes correctly, and why the type decisions matter more than the boilerplate.
How JSON fields map to typed Java properties
JSON has exactly six value kinds — string, number, boolean, null, object, and array — and Java has many more. The whole job of converting JSON to a Java class is making the right choice for each field. Here is the mapping that matters:
- A JSON string becomes
String. - A JSON boolean becomes
boolean(orBooleanwhen it can be null). - A JSON integer that fits in 32 bits becomes
int; a larger one — a
millisecond timestamp, a snowflake ID — becomes long.
- A JSON number with a fraction becomes
double. - A JSON object lifts into its own named class.
- A JSON array becomes
List<T>, whereTis the element type.
The snake_case key also has to become a valid Java identifier. site_admin turns into siteAdmin, is-active into isActive, created_at into createdAt. The moment the Java field name differs from the wire key, you need a @JsonProperty("site_admin") (Jackson) or @SerializedName("site_admin") (Gson) annotation, or the binding silently breaks — Jackson goes looking for a siteAdmin key that the server never sends.
A worked example: from JSON to a Java class
Take a small but realistic payload from a user API:
{
"id": 4815162342,
"login": "lilei",
"site_admin": false,
"created_at": "2026-05-29T08:00:00Z",
"public_repos": 12,
"balance": 19.95
}
Paste that into the JSON to Java Class converter, name the root User, and keep Jackson. Here is the POJO it produces:
import com.fasterxml.jackson.annotation.JsonProperty;
public class User {
private long id;
private String login;
@JsonProperty("site_admin")
private boolean siteAdmin;
@JsonProperty("created_at")
private String createdAt;
@JsonProperty("public_repos")
private int publicRepos;
private double balance;
public long getId() { return id; }
public void setId(long id) { this.id = id; }
public String getLogin() { return login; }
public void setLogin(String login) { this.login = login; }
public boolean isSiteAdmin() { return siteAdmin; }
public void setSiteAdmin(boolean siteAdmin) { this.siteAdmin = siteAdmin; }
public String getCreatedAt() { return createdAt; }
public void setCreatedAt(String createdAt) { this.createdAt = createdAt; }
public int getPublicRepos() { return publicRepos; }
public void setPublicRepos(int publicRepos) { this.publicRepos = publicRepos; }
public double getBalance() { return balance; }
public void setBalance(double balance) { this.balance = balance; }
}
Read the type decisions closely. id is 4815162342, which overflows a 32-bit int, so it infers long. public_repos is small, so it stays int. balance has a fractional part, so it lands on double. login and id carry no annotation because their camelCase name already equals the wire key; only the three snake_case fields get a @JsonProperty. That is the discipline a hand-written model usually skips — and the bug you find a week later when one key binds to null.
Boxing: the rule most generators get wrong
A Java primitive cannot hold null. int x = null; does not compile. So when a field might be absent, it has to be boxed to its wrapper — Integer, Long, Double, Boolean. Where does "might be absent" come from? From an array of objects where some elements carry a key and others don't.
Feed in this array:
[
{"id": 1, "name": "alpha"},
{"id": 2}
]
The two elements fold into one shared class. name is missing from the second element, so it boxes — String already handles null, but if it were a number it would become Integer rather than int. This matters because a primitive int name would silently deserialize a missing key to 0, and your code would never know the difference between "the value is zero" and "the field wasn't sent." List generics always box too, since List<int> is illegal Java — it is always List<Integer>. A single object, by contrast, gives no missing-key signal, so every numeric field in it stays primitive unless its value is literally null. If you want to probe optionality from one sample, wrap it as [{...}].
Nested objects, arrays, and the type graph
Real payloads nest. Rather than collapsing a nested object into Map<String, Object> and casting your way through it, each nested object should lift into its own named class. User.address becomes a UserAddress class, referenced by an address field on User. An array of objects folds every element into a single shared class, so a list whose objects vary slightly across the wire produces one type with the union of fields — not a near-duplicate class per element.
This is exactly how a typed model replaces the casting trap. Instead of:
Map<String, Object> data = mapper.readValue(body, Map.class);
String id = (String) data.get("id"); // ClassCastException waiting in prod
you get a User with getId() returning a real long, and the compiler catches a renamed key at build time instead of a runtime exception in production.
Picking the output shape for your API integration
The field inference is identical across the three output shapes — only the boilerplate differs:
- POJO — plain, dependency-free, works on any Java version. You get private
fields plus full getX()/setX() accessors. Use it for RestTemplate or any framework that binds via setters.
- Lombok
@Data— if your project already uses Lombok, the class stays a few
lines and Lombok generates the getters, setters, equals, hashCode, and toString at compile time. Keeps a Lombok codebase consistent and PRs short.
record(Java 16+) — immutable DTOs with value equality and a compact
constructor. Jackson deserializes records out of the box since 2.12, so they are ideal for response models you never mutate. One caveat: on a build that targets Java 11 or 8, a record won't compile — pick POJO or Lombok there.
For a Spring Boot integration, the flow is: curl the endpoint once, paste the JSON, name the root OrderResponse, keep Jackson, drop the output into OrderResponse.java, and call restTemplate.getForObject(url, OrderResponse.class). You get a fully typed object back instead of an untyped map. One field worth hand-editing afterward: anything that represents money. The tool infers double for any fractional number, but double is binary floating point and misrepresents values like 0.1 — switch those to BigDecimal yourself, because JSON carries no signal that a number needs exact base-10 precision.
I converted a payment-webhook payload this way last week — a vendor's body that sometimes included cancelled_at and sometimes omitted it. Pasting several captured payloads as a JSON array made the array-fold detection box cancelled_at as a wrapper type. That single boxing decision let a clean null check distinguish "cancelled" from "still active" in my handler, instead of me guessing whether a default value meant the field was simply absent. It is the kind of correctness I'd never have gotten from typing the class by hand at 6pm.
Before you ship the class
A few habits keep the generated model honest. Don't delete a @JsonProperty expecting binding to still work — that annotation is the only thing mapping the wire key to the field name; remove it and the field reads null. If you bind keys globally instead, set the annotation style to None and configure PropertyNamingStrategies.SNAKE_CASE on your ObjectMapper (or Gson's FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES). And if the JSON itself looks suspect — a stray trailing comma, a key you didn't expect — run it through the JSON formatter first to validate and pretty-print it, then convert the clean version. Everything runs in your browser; the payload never touches a server, so even a production response is safe to paste.
Made by Toolora · Updated 2026-06-13