Skip to main content

JSON Processing: Jackson and Gson

JSON is the lingua franca of web APIs. Java does not include a built-in JSON library, so you need either Jackson (the de facto standard) or Gson (Google's lighter alternative). This page focuses on Jackson with a Gson comparison at the end.

Jackson setup

Maven dependency:

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>

ObjectMapper basics

ObjectMapper is the central class. Reuse a single instance -- it is thread-safe and expensive to create.

Serialisation (Java to JSON)

ObjectMapper mapper = new ObjectMapper();

record User(String name, String email, int age) {}

User user = new User("Alice", "alice@example.com", 30);
String json = mapper.writeValueAsString(user);
// {"name":"Alice","email":"alice@example.com","age":30}

// Pretty-printed
String pretty = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(user);

Deserialisation (JSON to Java)

String json = """
{"name":"Alice","email":"alice@example.com","age":30}
""";

User user = mapper.readValue(json, User.class);
// User[name=Alice, email=alice@example.com, age=30]

// From file
User user = mapper.readValue(new File("user.json"), User.class);

// From InputStream
User user = mapper.readValue(inputStream, User.class);

// From byte[]
User user = mapper.readValue(bytes, User.class);

Deserialising generic types

// List<User> -- cannot use List<User>.class directly
List<User> users = mapper.readValue(json, new TypeReference<List<User>>() {});

// Map<String, Object>
Map<String, Object> map = mapper.readValue(json, new TypeReference<>() {});

ObjectMapper configuration

ObjectMapper mapper = new ObjectMapper()
// Don't fail on unknown JSON properties
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)

// Don't fail on empty beans
.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)

// Use ISO-8601 for dates (instead of timestamps)
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)

// Register Java 8 date/time module
.registerModule(new JavaTimeModule())

// Pretty print
.enable(SerializationFeature.INDENT_OUTPUT);

Common modules

ModuleDependencyPurpose
JavaTimeModulejackson-datatype-jsr310Java 8+ date/time (LocalDate, Instant, etc.)
Jdk8Modulejackson-datatype-jdk8Optional support
ParameterNamesModulejackson-module-parameter-namesConstructor parameter names (avoid @JsonCreator)
KotlinModulejackson-module-kotlinKotlin data classes

Annotations

Field-level annotations

record Product(
@JsonProperty("product_name") // JSON field name differs from Java
String name,

@JsonProperty(access = JsonProperty.Access.READ_ONLY)
String id, // serialised but not deserialised

@JsonIgnore // excluded from JSON entirely
String internalCode,

@JsonFormat(pattern = "yyyy-MM-dd")
LocalDate releaseDate,

@JsonInclude(JsonInclude.Include.NON_NULL)
String optionalField // omitted from JSON if null
) {}

Class-level annotations

@JsonIgnoreProperties(ignoreUnknown = true)   // ignore unknown JSON fields
@JsonInclude(JsonInclude.Include.NON_NULL) // omit all null fields
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) // snake_case
record ApiResponse(
int statusCode,
String errorMessage,
List<Item> items
) {}

// JSON output: {"status_code":200,"items":[...]}
// (errorMessage omitted because it's null)

@JsonCreator for custom construction

record Money(BigDecimal amount, String currency) {

@JsonCreator
Money(
@JsonProperty("amount") BigDecimal amount,
@JsonProperty("currency") String currency
) {
this.amount = amount;
this.currency = currency.toUpperCase();
}
}

Custom serialisers and deserialisers

Custom serialiser

public class MoneySerializer extends JsonSerializer<Money> {
@Override
public void serialize(Money value, JsonGenerator gen, SerializerProvider provider)
throws IOException {
gen.writeStartObject();
gen.writeStringField("amount", value.amount().toPlainString());
gen.writeStringField("currency", value.currency());
gen.writeEndObject();
}
}

// Register on the class
@JsonSerialize(using = MoneySerializer.class)
record Money(BigDecimal amount, String currency) {}

// Or register on the mapper
SimpleModule module = new SimpleModule();
module.addSerializer(Money.class, new MoneySerializer());
mapper.registerModule(module);

Custom deserialiser

public class MoneyDeserializer extends JsonDeserializer<Money> {
@Override
public Money deserialize(JsonParser p, DeserializationContext ctx)
throws IOException {
JsonNode node = p.getCodec().readTree(p);
BigDecimal amount = new BigDecimal(node.get("amount").asText());
String currency = node.get("currency").asText();
return new Money(amount, currency);
}
}

@JsonDeserialize(using = MoneyDeserializer.class)
record Money(BigDecimal amount, String currency) {}

Polymorphism

When a JSON field can contain different subtypes, use @JsonTypeInfo:

@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
property = "type" // JSON discriminator field
)
@JsonSubTypes({
@JsonSubTypes.Type(value = Circle.class, name = "circle"),
@JsonSubTypes.Type(value = Rectangle.class, name = "rectangle")
})
sealed interface Shape permits Circle, Rectangle {}

record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}

// Serialisation
Shape shape = new Circle(5.0);
String json = mapper.writeValueAsString(shape);
// {"type":"circle","radius":5.0}

// Deserialisation
Shape deserialized = mapper.readValue(json, Shape.class);
// Circle[radius=5.0]

Working with JSON trees

When you don't have (or want) a POJO:

// Parse to tree
JsonNode root = mapper.readTree(jsonString);

// Navigate
String name = root.get("name").asText();
int age = root.get("age").asInt();
boolean hasEmail = root.has("email");

// Nested access
String city = root.path("address").path("city").asText("Unknown");

// Array iteration
JsonNode items = root.get("items");
for (JsonNode item : items) {
System.out.println(item.get("name").asText());
}

// Build a tree
ObjectNode node = mapper.createObjectNode();
node.put("name", "Alice");
node.put("age", 30);
node.putArray("tags").add("java").add("dev");
String json = mapper.writeValueAsString(node);

Gson comparison

Setup

<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.11.0</version>
</dependency>

Basic usage

Gson gson = new GsonBuilder()
.setPrettyPrinting()
.setDateFormat("yyyy-MM-dd")
.create();

// Serialise
String json = gson.toJson(user);

// Deserialise
User user = gson.fromJson(json, User.class);

// Generic types
List<User> users = gson.fromJson(json, new TypeToken<List<User>>() {}.getType());

Jackson vs Gson

FeatureJacksonGson
PerformanceFaster (streaming parser)Slower
Annotations@JsonProperty, @JsonIgnore, etc.@SerializedName, @Expose
Tree modelJsonNodeJsonElement
Module systemYes (Java 8 dates, Kotlin, etc.)Limited
Streaming APIJsonParser / JsonGeneratorJsonReader / JsonWriter
Null handlingOmits nulls by defaultIncludes nulls by default
Bundle size~1.5 MB (core + databind + annotations)~300 KB
Default in SpringYesNo
Default in AndroidNoCommon

Recommendation: Use Jackson for server-side Java (it is the Spring default and has the richest feature set). Use Gson if you need a smaller footprint or are on Android.


Common pitfalls

PitfallProblemFix
Creating ObjectMapper per requestExpensive (class metadata caching)Create once and reuse
Unknown properties cause failureUnrecognizedPropertyException on extra JSON fieldsFAIL_ON_UNKNOWN_PROPERTIES = false or @JsonIgnoreProperties(ignoreUnknown = true)
Java 8 dates serialise as numbersLocalDate becomes [2024, 1, 15]Register JavaTimeModule, disable WRITE_DATES_AS_TIMESTAMPS
Circular referencesStackOverflowError during serialisation@JsonManagedReference / @JsonBackReference, or @JsonIdentityInfo
Records need parameter namesJackson can't match constructor params without -parameters flagAdd jackson-module-parameter-names or use @JsonProperty
Mutable ObjectMapper after sharingThread-safety issuesConfigure once, then call mapper.copy() if you need a variant
Optional fieldsSerialised as {"present":true,"value":"x"}Register Jdk8Module for correct "x" / null handling

See also