Library Comparison Overview
The Java ecosystem offers three major JSON libraries, each with distinct strengths:
| Feature | Jackson | Gson | Moshi |
|---|---|---|---|
| Performance | Excellent | Good | Very Good |
| Streaming API | Yes | No | Yes (Okio) |
| Annotation Support | Extensive | Moderate | Moderate |
| Kotlin Support | Good | Basic | Excellent |
| Community | Largest | Large | Growing |
Jackson: The Industry Standard
Jackson is the most widely used JSON library in Java, offering both tree model and data binding approaches.
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
// Serialize
User user = new User("Alice", 30);
String json = mapper.writeValueAsString(user);
// {"name":"Alice","age":30}
// Deserialize
User parsed = mapper.readValue(json, User.class);
Key Annotations
public class User {
@JsonProperty("user_name")
private String name;
@JsonIgnore
private String password;
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate birthDate;
@JsonInclude(JsonInclude.Include.NON_NULL)
private String optionalField;
}
Gson: Google’s Lightweight Option
Gson offers a simpler API with less configuration overhead.
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
Gson gson = new GsonBuilder()
.setPrettyPrinting()
.setDateFormat("yyyy-MM-dd")
.create();
// Serialize
String json = gson.toJson(user);
// Deserialize
User parsed = gson.fromJson(json, User.class);
Custom Type Adapters
public class LocalDateAdapter implements JsonSerializer<LocalDate>, JsonDeserializer<LocalDate> {
@Override
public JsonElement serialize(LocalDate date, Type type, JsonSerializationContext context) {
return new JsonPrimitive(date.toString());
}
@Override
public LocalDate deserialize(JsonElement element, Type type, JsonDeserializationContext context) {
return LocalDate.parse(element.getAsString());
}
}
Moshi: Modern and Kotlin-Friendly
Moshi is designed with Kotlin in mind, offering null-safety and code generation. While it works in pure Java, it is most commonly used in Kotlin projects — for Java-only codebases, Jackson or Gson are typically preferred. The examples below show Kotlin syntax (Moshi’s primary use case), but a Java equivalent follows each example.
Kotlin (primary use case):
@JsonClass(generateAdapter = true)
data class User(
val name: String,
val age: Int,
val email: String? = null
)
val moshi = Moshi.Builder().build()
val adapter = moshi.adapter(User::class.java)
val json = adapter.toJson(user)
val parsed = adapter.fromJson(json)
Java equivalent:
import com.squareup.moshi.Moshi;
import com.squareup.moshi.JsonAdapter;
Moshi moshi = new Moshi.Builder().build();
JsonAdapter<User> adapter = moshi.adapter(User.class);
String json = adapter.toJson(user);
User parsed = adapter.fromJson(json);
Spring Boot Integration
Spring Boot uses Jackson by default. Configure it in application.yml:
spring:
jackson:
serialization:
indent-output: true
write-dates-as-timestamps: false
deserialization:
fail-on-unknown-properties: false
default-property-inclusion: non_null
Custom ObjectMapper bean:
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
return mapper;
}
Polymorphic Deserialization
Handling inheritance hierarchies during deserialization requires special configuration.
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = Dog.class, name = "dog"),
@JsonSubTypes.Type(value = Cat.class, name = "cat")
})
public abstract class Animal { }
public class Dog extends Animal {
public String breed;
}
public class Cat extends Animal {
public boolean indoor;
}
Performance Tips
- Reuse ObjectMapper — Creating a new instance is expensive
- Use
@JsonCreatorfor immutable objects instead of setters - Stream large arrays with
JsonParserinstead of loading into memory - Consider Blackbird module (modern replacement for Afterburner) for bytecode-optimized serialization
- Use
JsonFactoryreuse across multipleObjectMapperinstances
// Stream processing for large JSON arrays
try (JsonParser parser = mapper.getFactory().createParser(inputStream)) {
parser.nextToken(); // START_ARRAY
while (parser.nextToken() == JsonToken.START_OBJECT) {
Item item = mapper.readValue(parser, Item.class);
processItem(item);
}
}
JSON-B (Jakarta EE)
JSON-B is the standard JSON binding API for Jakarta EE:
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
// Basic usage
Jsonb jsonb = JsonbBuilder.create();
String json = jsonb.toJson(user);
// Deserialize
User parsed = jsonb.fromJson(json, User.class);
Custom configuration:
import jakarta.json.bind.JsonbConfig;
JsonbConfig config = new JsonbConfig()
.withFormatting(true)
.withNullValues(true)
.withDateFormat("yyyy-MM-dd", Locale.US);
Jsonb customJsonb = JsonbBuilder.create(config);
String json = customJsonb.toJson(user);
Streaming Large Files
Process gigabyte-scale JSON without loading into memory:
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
try (JsonParser parser = new JsonFactory().createParser(new File("large.json"))) {
if (parser.nextToken() != JsonToken.START_ARRAY) {
throw new IllegalStateException("Expected array");
}
while (parser.nextToken() == JsonToken.START_OBJECT) {
Item item = mapper.readValue(parser, Item.class);
processItem(item); // Process one at a time
}
}
JSON Schema Validation
Using everit-org/json-schema:
import org.everit.json.schema.Schema;
import org.everit.json.schema.loader.SchemaLoader;
import org.json.JSONObject;
import org.json.JSONArray;
JSONObject schemaJson = new JSONObject(new JSONTokener(
getClass().getResourceAsStream("/schema.json")));
Schema schema = SchemaLoader.load(schemaJson);
JSONObject data = new JSONObject("{\"name\": \"Alice\", \"age\": 30}");
schema.validate(data); // Throws ValidationException if invalid
Error Handling
JSON parsing can fail for many reasons — malformed input, type mismatches, or unexpected fields. Always wrap deserialization in a try-catch block to handle these gracefully.
import com.fasterxml.jackson.core.JsonProcessingException;
try {
User user = mapper.readValue(json, User.class);
} catch (JsonProcessingException e) {
System.err.println("JSON error at line " + e.getLocation().getLineNr());
System.err.println("Message: " + e.getOriginalMessage());
}
Jackson’s JsonProcessingException provides the exact line and column where the error occurred, making it straightforward to locate the problem in large payloads. Gson throws JsonSyntaxException and Moshi throws JsonDataException — both carry similar diagnostic information.
Maven/Gradle Dependencies
To get started with any of these libraries, add the appropriate dependency to your project:
| Library | Maven Artifact |
|---|---|
| Jackson | com.fasterxml.jackson.core:jackson-databind |
| Gson | com.google.code.gson:gson |
| Moshi | com.squareup.moshi:moshi |
Maven example (Jackson):
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>
Gradle example (Jackson):
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.2'
Related Resources
- JSON Best Practices — Universal guidelines for all languages
- JSON Schema Validation — Validate your JSON structures
- JSON Security — Protect against common vulnerabilities
- JSON Formatter — Format and validate your JSON online
- JSON Schema Validator — Test your schemas