POJO to JSON in Java: Jackson and Gson Examples
To convert a POJO (Plain Old Java Object) to JSON, call new ObjectMapper().writeValueAsString(obj) with Jackson or new Gson().toJson(obj) with Gson. Jackson is the default in Spring Boot and gives you the most control; Gson has a smaller API and is in maintenance mode. This process is called serialization, and it is how a Java backend sends data to a JavaScript frontend or another service over a REST API.
Java libraries for POJO to JSON conversion
| Library | Strength | Best for |
|---|---|---|
| Jackson | Annotations, modules for extra types, a streaming API. The default in Spring Boot. | Most server code, Spring Boot apps |
| Gson | Small API; reads fields directly, so it needs no getters. | Simple Java projects |
| Moshi | Kotlin support, from Square. | Kotlin and Android projects |
| org.json | No dependencies; you build JSON objects by hand. | Small scripts |
The examples below use Jackson 2 (2.22) and Gson 2.14. Jackson 3 changes package names and some defaults; see what changes in Jackson 3.
Your first POJO to JSON conversion with Jackson

Add jackson-databind to your build; it pulls in jackson-core and jackson-annotations. For Maven, in pom.xml:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.22.3</version>
</dependency>
For Gradle, in build.gradle:
implementation 'com.fasterxml.jackson.core:jackson-databind:2.22.3'
The basic serialization
Start with a User POJO. Jackson reads properties through public getters (or public fields), so give it getters:
public class User {
private int id;
private String name;
private String email;
public User() {}
public User(int id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
public int getId() { return id; }
public String getName() { return name; }
public String getEmail() { return email; }
}
ObjectMapper does the work in both directions: serialization (POJO to JSON) and deserialization (JSON to POJO).
import com.fasterxml.jackson.databind.ObjectMapper;
public class Main {
public static void main(String[] args) throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
User user = new User(1, "Alex Johnson", "alex.j@example.com");
String jsonString = objectMapper.writeValueAsString(user);
System.out.println(jsonString);
}
}
This prints one line: {"id":1,"name":"Alex Johnson","email":"alex.j@example.com"}. For indented output while debugging, use objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(user).
Reuse one
ObjectMapper. Creating one is expensive because it builds and caches serializers. Once configured, it is thread-safe, so create it once and share it across your application.
Renaming and hiding fields with annotations
Annotations change the JSON without changing your class’s fields. The two you will use most:
@JsonProperty("name")renames a field in the output, for when your Java names don’t match the API spec.@JsonIgnoreleaves a field out, for internal or sensitive data.
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
public class Product {
@JsonProperty("productId")
private int id;
private String name;
@JsonIgnore
private String internalSku;
public Product(int id, String name, String internalSku) {
this.id = id;
this.name = name;
this.internalSku = internalSku;
}
public int getId() { return id; }
public String getName() { return name; }
public String getInternalSku() { return internalSku; }
}
Serializing a Product now gives {"name":"Desk","productId":7}: id is renamed and internalSku is gone.
POJO to JSON with Gson
Gson from Google is the simpler option: no annotations needed, and it reads fields directly, so a class without getters still serializes. Its README says Gson is in maintenance mode: bugs get fixed, but large new features are unlikely. The latest release is 2.14.0 (April 2026).
For Maven:
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.14.0</version>
</dependency>
For Gradle:
implementation 'com.google.code.gson:gson:2.14.0'
The code mirrors Jackson’s, with Gson in place of ObjectMapper:
import com.google.gson.Gson;
public class Main {
public static void main(String[] args) {
Gson gson = new Gson();
User user = new User(1, "Alex Johnson", "alex.j@example.com");
String jsonString = gson.toJson(user);
System.out.println(jsonString);
}
}
The output is the same: {"id":1,"name":"Alex Johnson","email":"alex.j@example.com"}. A Gson instance is thread-safe, so create it once and reuse it.
Gson leaves out null fields by default. A
Userwith a null email serializes as{"id":2,"name":"Sam"}. To write"email":null, build the instance withnew GsonBuilder().serializeNulls().create().
For indented output, use GsonBuilder:
Gson gson = new GsonBuilder().setPrettyPrinting().create();
String prettyJson = gson.toJson(user);
Gson ignores Jackson’s annotations. Its equivalents are @SerializedName("productId") to rename a field and the transient keyword to leave one out.
When to choose Gson
- Simple models. If you don’t need custom rules, Gson’s API is short.
- Classes without getters. Gson serializes fields directly, which helps with older code that doesn’t follow JavaBean conventions.
- Not on Android or Kotlin. Gson’s README recommends against it on Android, where its reflection breaks under code shrinking (it suggests Kotlin Serialization instead), and says Kotlin features such as non-null types aren’t supported.
Records, nested objects and collections
Java records serialize without extra setup in Jackson 2.12+ and Gson 2.10+. record Point(int x, int y) becomes {"x":3,"y":4} in both.
Nested objects and collections also work out of the box. Given a Book that holds an Author and a list of tags:
import java.util.List;
public class Author {
private String name;
private String country;
// constructor and getters
}
public class Book {
private String title;
private Author author;
private List<String> tags;
// constructor and getters
}
Jackson and Gson walk the object graph and write the Author as a nested object and the list as an array:
{"title":"Dune","author":{"name":"Frank Herbert","country":"USA"},"tags":["sci-fi","classic"]}
Serializing java.time dates
Jackson 2 rejects java.time types such as LocalDate by default: writeValueAsString throws an InvalidDefinitionException that tells you to add the jackson-datatype-jsr310 module. Add that dependency (same version as jackson-databind), register JavaTimeModule, and turn off WRITE_DATES_AS_TIMESTAMPS. Without that last step, a LocalDate comes out as an array like [2026,7,31] instead of "2026-07-31".
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
// A LocalDate field is now written as "2026-07-31"
Gson 2.14 has built-in adapters for java.time, but they write the classes’ internal fields, so a LocalDate becomes {"year":2026,"month":7,"day":31}. For an ISO string, register a serializer:
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializer;
import java.time.LocalDate;
Gson gson = new GsonBuilder()
.registerTypeAdapter(LocalDate.class,
(JsonSerializer<LocalDate>) (date, type, context) -> new JsonPrimitive(date.toString()))
.create();
Writing a custom serializer

Write a custom serializer when the API needs a different shape than your class. Say the API wants the author as one string, "Frank Herbert (USA)", instead of a nested object. In Jackson 2, extend StdSerializer:
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import java.io.IOException;
public class CustomBookSerializer extends StdSerializer<Book> {
public CustomBookSerializer() {
super(Book.class);
}
@Override
public void serialize(Book book, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeStringField("title", book.getTitle());
gen.writeStringField("author",
String.format("%s (%s)", book.getAuthor().getName(), book.getAuthor().getCountry()));
gen.writeEndObject();
}
}
Then tell Jackson to use it, either with @JsonSerialize(using = CustomBookSerializer.class) on the Book class or by registering it on the mapper:
import com.fasterxml.jackson.databind.module.SimpleModule;
objectMapper.registerModule(new SimpleModule().addSerializer(Book.class, new CustomBookSerializer()));
// {"title":"Dune","author":"Frank Herbert (USA)"}
What changes in Jackson 3
Jackson 3.0 was released on October 3, 2025 (release notes); the current version is 3.2. Spring Boot 4 uses it by default and auto-configures a JsonMapper bean (Spring Boot docs). What changes for the code above:
- New coordinates and packages. The Maven group and Java packages move from
com.fasterxml.jacksontotools.jackson(tools.jackson.core:jackson-databind). Annotations such as@JsonPropertykeep theircom.fasterxml.jackson.annotationpackage. - java.time is built in. No extra module, and
WRITE_DATES_AS_TIMESTAMPSis off by default, so dates come out as ISO strings. - Unchecked exceptions.
JacksonExceptionextendsRuntimeException, andJsonMappingExceptionis nowDatabindException. - Immutable mappers. You configure a mapper through a builder, for example
JsonMapper.builder()...build(). - Other defaults. Properties are sorted alphabetically in the output, unknown properties no longer fail deserialization, and Java 17 is the minimum.
- Custom serializers.
StdSerializerstays, butserializetakes aSerializationContextinstead of aSerializerProvider, andwriteStringFieldbecomeswriteStringProperty.
The basic conversion in Jackson 3:
import tools.jackson.databind.json.JsonMapper;
JsonMapper mapper = JsonMapper.builder().build();
String json = mapper.writeValueAsString(user); // no checked exception
// {"email":"alex.j@example.com","id":1,"name":"Alex Johnson"}
Performance for high-throughput APIs
The biggest win is the simplest: reuse one ObjectMapper or Gson instance. Both are thread-safe once configured, and creating one per request repeats work the first instance already cached:
- discovering and caching serializers and deserializers,
- scanning your classes with reflection to find their properties,
- applying modules and configuration.
In Spring Boot, inject the mapper the framework already configured (a Jackson 3 JsonMapper in Spring Boot 4, a Jackson 2 ObjectMapper in Spring Boot 3) instead of creating your own.
Streaming large data
Data binding builds the whole object, or the whole JSON tree, in memory. For data too large for that, Jackson’s streaming API reads and writes JSON one token at a time:
JsonGeneratorwrites JSON token by token.JsonParserreads JSON token by token (START_OBJECT,FIELD_NAMEand so on).
With a JsonGenerator you can write a large list of database records to a file while holding only one record in memory. It takes more code than data binding, so use it only when memory is the constraint; for typical API responses, a reused ObjectMapper is enough.
Common POJO to JSON errors
Infinite recursion (StackOverflowError)
Two objects that reference each other loop forever. An Order points to its Customer, and the Customer holds a list of Order objects, so Jackson goes back and forth until the stack overflows. Two annotations break the cycle:
@JsonManagedReferenceon the parent side, such as theList<Order>inCustomer. This side is serialized normally.@JsonBackReferenceon the child side, such as theCustomerfield inOrder. This side is left out of the output.
No serializer found for class
No serializer found for class X and no properties discovered to create BeanSerializer means Jackson found nothing to write: the class has private fields and no public getters. Add getters, make the fields public, or annotate them with @JsonProperty.
JsonMappingException when reading JSON back
A JsonMappingException (in Jackson 2.x often its subclass InvalidDefinitionException) means Jackson can’t map between the JSON and your class. When you deserialize, the usual cause is a missing no-argument constructor: Jackson needs to create the object before it can set fields. Add a no-argument constructor, or mark a constructor with @JsonCreator. Records need neither.
Frequently asked questions
Which is better for POJO to JSON conversion: Jackson or Gson?
Jackson, for most projects. It is the default in Spring Boot, has annotations and modules for nearly every case, and is actively developed. Gson is simpler and fine for small Java projects, but it is in maintenance mode and its README recommends against it on Android.
How do I leave null fields out of the JSON?
Gson already leaves them out; call serializeNulls() on the GsonBuilder if you want them. Jackson writes nulls by default; call objectMapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL) to drop them everywhere, or put @JsonInclude(JsonInclude.Include.NON_NULL) on a class or field.
What is a JsonMappingException and how do I fix it?
It is Jackson 2’s error for a mismatch between JSON and your class (Jackson 3 calls it DatabindException). The usual causes are a class without a no-argument constructor, a JSON field name that doesn’t match the Java property (fix it with @JsonProperty("json_name")), an unknown JSON field (UnrecognizedPropertyException), or a type mismatch such as a string where a number is expected. The exception message names the class and field to fix.