POJOs without the boilerplate
Mapping JSON in Java usually means a POJO per object: private fields, a getter and setter for each, and @JsonProperty annotations wherever the JSON key differs from the Java name. For a realistic API response that can be hundreds of lines. This generator writes them from a sample payload, in a form Spring Boot, Micronaut, Quarkus and plain Jackson all understand.
What the generated file contains
Everything is merged into one compilation unit so you can paste it as Root.java: a single package line, the combined imports, the root class as the only public class, and every nested class as a package-private class in the same file. A comment at the top shows how to read data: new ObjectMapper().readValue(json, Root.class).
Each class has:
- private fields with camelCase names (
first-namebecomesfirstName), - a getter and setter per field, both annotated with
@JsonProperty("first-name")so the original key is used for reading and writing, java.util.List<T>for arrays.
If the JSON’s root is an array, the element class takes the root name and the usage comment shows a TypeReference<List<...>> instead.
Type choices
- Whole numbers become
long, decimals becomedouble, booleans becomeboolean, and text becomesString. - When a numeric or boolean field is missing from some array items, or is
nullin some, the boxed type is used (Long,Double,Boolean) so the value can be null instead of silently becoming 0 or false. - A field that is only ever
nullis typedObject. - Arrays that mix types — for example numbers and strings — get a small wrapper class with a custom Jackson
JsonDeserializerandJsonSerializer, holding one field per possible type. - Objects found in an array are merged into a single class covering all keys.
Strings written exactly as a calendar date (2024-03-11) are typed java.time.LocalDate, which Jackson reads once the jackson-datatype-jsr310 module is registered. Full timestamps, UUIDs and enum-like values stay String, because one sample cannot prove their format; adjust those fields by hand.
Options and tips
Root type name (default Root) names the top-level class. Package (default com.example) sets the package declaration; it must be a dotted Java identifier such as com.acme.billing, otherwise the default is used.
If your project uses Lombok or Java records, you can collapse the getters and setters afterwards; the field list and annotations are the useful part. Configure DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES to false if the API may add keys later. Integers beyond 2^53 in the sample are flagged; use BigInteger or String for them. Nothing is uploaded while generating, and the Java formatter can restyle the output to your conventions.
Examples
Order with line items
Produces a public Order class plus Customer and Item classes; giftWrap becomes a boxed Boolean because only one item has it.
{
"orderId": "ord_8f2k1",
"status": "paid",
"total": 287.9,
"customer": { "id": 1042, "name": "Aisha Tan", "vip": true },
"items": [
{ "sku": "KB-104", "qty": 1, "price": 129.9 },
{ "sku": "MS-220", "qty": 2, "price": 79.0, "giftWrap": true }
]
}package com.acme.shop;
import com.fasterxml.jackson.annotation.*;
import java.util.List;
// Deserialize with Jackson: Order data = new ObjectMapper().readValue(json, Order.class);
public class Order {
private String orderID;
private String status;
private double total;
private Customer customer;
private List<Item> items;
@JsonProperty("orderId")
public String getOrderID() { return orderID; }
@JsonProperty("orderId")
public void setOrderID(String value) { this.orderID = value; }
@JsonProperty("status")
public String getStatus() { return status; }
@JsonProperty("status")
public void setStatus(String value) { this.status = value; }
@JsonProperty("total")
public double getTotal() { return total; }
@JsonProperty("total")
public void setTotal(double value) { this.total = value; }
@JsonProperty("customer")
public Customer getCustomer() { return customer; }
@JsonProperty("customer")
public void setCustomer(Customer value) { this.customer = value; }
@JsonProperty("items")
public List<Item> getItems() { return items; }
@JsonProperty("items")
public void setItems(List<Item> value) { this.items = value; }
}
class Customer {
private long id;
private String name;
private boolean vip;
@JsonProperty("id")
public long getID() { return id; }
@JsonProperty("id")
public void setID(long value) { this.id = value; }
@JsonProperty("name")
public String getName() { return name; }
@JsonProperty("name")
public void setName(String value) { this.name = value; }
@JsonProperty("vip")
public boolean getVip() { return vip; }
@JsonProperty("vip")
public void setVip(boolean value) { this.vip = value; }
}
class Item {
private String sku;
private long qty;
private double price;
private Boolean giftWrap;
@JsonProperty("sku")
public String getSku() { return sku; }
@JsonProperty("sku")
public void setSku(String value) { this.sku = value; }
@JsonProperty("qty")
public long getQty() { return qty; }
@JsonProperty("qty")
public void setQty(long value) { this.qty = value; }
@JsonProperty("price")
public double getPrice() { return price; }
@JsonProperty("price")
public void setPrice(double value) { this.price = value; }
@JsonProperty("giftWrap")
public Boolean getGiftWrap() { return giftWrap; }
@JsonProperty("giftWrap")
public void setGiftWrap(Boolean value) { this.giftWrap = value; }
}
Hyphenated keys and nulls
Each hyphenated key maps to a camelCase field with @JsonProperty keeping the original name, and the always-null last-login is typed Object.
{
"user-id": 42,
"display-name": "Ben Okafor",
"last-login": null,
"roles": ["editor", "viewer"]
}package com.example;
import com.fasterxml.jackson.annotation.*;
import java.util.List;
// Deserialize with Jackson: Root data = new ObjectMapper().readValue(json, Root.class);
public class Root {
private long userID;
private String displayName;
private Object lastLogin;
private List<String> roles;
@JsonProperty("user-id")
public long getUserID() { return userID; }
@JsonProperty("user-id")
public void setUserID(long value) { this.userID = value; }
@JsonProperty("display-name")
public String getDisplayName() { return displayName; }
@JsonProperty("display-name")
public void setDisplayName(String value) { this.displayName = value; }
@JsonProperty("last-login")
public Object getLastLogin() { return lastLogin; }
@JsonProperty("last-login")
public void setLastLogin(Object value) { this.lastLogin = value; }
@JsonProperty("roles")
public List<String> getRoles() { return roles; }
@JsonProperty("roles")
public void setRoles(List<String> value) { this.roles = value; }
}
Common errors and how to fix them
| Error | Cause | Fix |
|---|---|---|
Unexpected content after the end of the JSON valueExplained | Two JSON documents were pasted one after another. | Wrap them in an array or convert them one at a time. |
NaN is not a valid JSON numberExplained | The sample came from a system that writes NaN or Infinity, which JSON forbids. | Replace those values with null or a number before converting. |
Some integers are larger than 2^53; they are typed as floating-point numbers. Use a 64-bit or big-integer type if you need exact values.Explained | A warning: an integer in the sample exceeds the range inference can handle exactly. | Use long if the values fit in 64 bits, otherwise BigInteger. |
Frequently asked questions
Which JSON library does the code target?
Jackson (com.fasterxml.jackson). The @JsonProperty annotations and the custom serializers for mixed types are Jackson-specific.
Why are all classes in one file?
So you can paste the output directly. Only the root class is public, which Java allows in a single file; split them into separate files if your style requires it.
Why is a field Long instead of long?
The field was missing or null in part of the sample. A boxed type can hold null, which keeps “no value” distinct from zero.
Can I generate records instead of classes?
Not directly. Convert by hand by keeping the field types and @JsonProperty names in the record components.