Configuring Spring Boot to Use Gson Instead of Jackson
Published Updated Spring Boot 11 min read
How to switch the JSON mapper, and the four things that stop working when you do — java.time without adapters, Actuator, Spring Data REST, and null fields disappearing from responses.
Spring Boot uses Jackson for JSON and auto-configures Gson too if it finds it on the classpath. When both are present, Jackson wins.
Before switching. It is worth being clear that Gson is the smaller library rather than the better one for this job. Parts of Spring depend on Jackson directly and do not go through your configured mapper at all, so “use Gson instead” is really “use Gson for my controllers, and Jackson for the framework”. That is a supported arrangement, and it is not what most people mean when they set the property.
Written against Spring Boot 3.2, Gson 2.10 and Java 17.
Making the switch
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
spring-boot-starter-json is the artefact that brings Jackson in through the web starter. Excluding it
is what makes the switch complete rather than nominal.
If you keep Jackson for other reasons, and the next section explains why you might have to — tell Spring which mapper the MVC converters should prefer:
spring.mvc.converters.preferred-json-mapper=gson
Without that property and with both libraries present, nothing changes: Jackson keeps serving your controllers and you conclude the configuration does not work.
Then configure Gson through the usual properties:
spring.gson.date-format=yyyy-MM-dd'T'HH:mm:ss'Z'
spring.gson.disable-html-escaping=true
spring.gson.serialize-nulls=false
spring.gson.pretty-printing=false
spring.gson.field-naming-policy=lower_case_with_underscores
Or a customiser, for anything the properties do not cover:
@Configuration
public class GsonConfig {
@Bean
GsonBuilderCustomizer typeAdapters() {
return builder -> builder
.registerTypeAdapter(Instant.class, new InstantAdapter())
.registerTypeAdapter(LocalDate.class, new LocalDateAdapter())
.setExclusionStrategies(new SkipInternalFields());
}
}
GsonBuilderCustomizer beans are applied to the auto-configured builder, so several can coexist and
you keep Spring’s defaults. Defining your own Gson bean replaces the auto-configuration entirely,
which is usually more than you wanted.
java.time needs adapters you write
This is the practical difference that matters most. Gson has no built-in support for
java.time, no Instant, no LocalDate, no ZonedDateTime. Jackson has it via a module Spring
Boot registers for you.
Serialise an Instant with an unconfigured Gson and you get its internal fields:
{ "createdAt": { "seconds": 1710415290, "nanos": 0 } }
Reflection over the object’s fields, faithfully. It is not wrong so much as useless to a client.
public class InstantAdapter implements JsonSerializer<Instant>, JsonDeserializer<Instant> {
@Override
public JsonElement serialize(Instant src, Type type, JsonSerializationContext ctx) {
return new JsonPrimitive(DateTimeFormatter.ISO_INSTANT.format(src));
}
@Override
public Instant deserialize(JsonElement json, Type type, JsonDeserializationContext ctx) {
return Instant.parse(json.getAsString());
}
}
One adapter per type you use, registered on the builder. spring.gson.date-format applies only to
java.util.Date and Calendar, which is the legacy API. It does nothing for java.time, and that
mismatch is the usual source of confusion here.
What stops working
Four things, and none of them is a bug. They are consequences worth knowing before you commit.
Actuator needs Jackson. Endpoints serialise their own payloads through Jackson directly, not
through your MVC converters. Remove Jackson entirely and Actuator’s JSON endpoints fail. So a build
with Actuator keeps Jackson on the classpath and uses preferred-json-mapper=gson for controllers
only, two mappers in one application.
Spring Data REST needs Jackson. It builds HAL representations with Jackson-specific machinery. There is no Gson equivalent.
@JsonIgnore and friends do nothing. Every Jackson annotation is inert under Gson, and silently
so. Gson’s equivalents are different:
public class Note {
private String title;
@Expose(serialize = false)
private String internalNote; // needs excludeFieldsWithoutExposeAnnotation()
private transient String secret; // transient is honoured by default
}
transient is the zero-configuration option. @Expose requires the builder to be told to respect it,
at which point every field you do want needs the annotation, an all-or-nothing switch that catches
people.
Null fields disappear. Gson omits nulls by default; Jackson includes them. So a field that was
"summary": null becomes absent, and a client distinguishing “null” from “not present” breaks.
spring.gson.serialize-nulls=true restores Jackson’s behaviour, and it is worth setting deliberately
rather than discovering the difference from a client bug.
Other behavioural differences
Fields, not getters. Gson reflects over fields; Jackson uses getters and setters by default. A
computed getFullName() with no backing field appears in Jackson’s output and not in Gson’s.
No constructor needed. Gson can instantiate a class with no accessible no-arg constructor by using
Unsafe. Convenient, and it means field initialisers and constructor validation are skipped. An
object can arrive in a state its constructor would have rejected.
Records work from Gson 2.10. Earlier versions cannot deserialise them at all. If your DTOs are records, check the version rather than assuming.
Unknown properties are ignored, quietly. Jackson fails on unknown properties unless configured otherwise; Gson never does. Whether that is safer depends on whether you would rather reject a malformed request or accept it silently.
Verifying the switch actually happened
Do not assume the property took effect. Ask the application:
@Component
class MapperReport implements ApplicationRunner {
private final List<HttpMessageConverter<?>> converters;
MapperReport(RestTemplateBuilder ignored, List<HttpMessageConverter<?>> converters) {
this.converters = converters;
}
@Override
public void run(ApplicationArguments args) {
converters.stream()
.map(c -> c.getClass().getSimpleName())
.filter(n -> n.contains("Json") || n.contains("Gson"))
.forEach(n -> System.out.println("json converter: " + n));
}
}
GsonHttpMessageConverter means the switch worked. MappingJackson2HttpMessageConverter means it did
not, and the reason is almost always that Jackson is still on the classpath without the
preferred-json-mapper property.
A response test is the other half:
@Test
void serialisesInstantAsIso() throws Exception {
mockMvc.perform(get("/api/notes/1"))
.andExpect(jsonPath("$.createdAt").value("2026-03-14T09:41:30Z"));
}
That fails loudly if an adapter is missing, rather than shipping {"seconds":…} to a client.
Should you switch?
Reasons that hold up:
- A smaller dependency tree. Gson is one jar; Jackson is core, databind, annotations plus modules. On a small service or an Android-shared library that is a real difference.
- Consistency with existing code. A team already using Gson everywhere has a reasonable case for one mapper.
Reasons that do not:
- Performance. Jackson is generally faster on large payloads. If throughput is the concern, measure your own payloads rather than switching on reputation.
- Simplicity. You will write
java.timeadapters Jackson gives you, and possibly run both libraries anyway.
The default is fine. Switch for a specific reason, and know which parts of the framework are not coming with you.
Frequently asked questions
Why is Jackson still being used after I added Gson?
With both on the classpath Jackson takes
precedence. Set spring.mvc.converters.preferred-json-mapper=gson, or exclude
spring-boot-starter-json.
How do I remove Jackson completely?
Exclude spring-boot-starter-json from the web starter. Note
that Actuator and Spring Data REST need Jackson, so a build using either must keep it.
Why is my Instant serialised as seconds and nanos?
Gson has no java.time support and is
reflecting over the object’s fields. Register a JsonSerializer/JsonDeserializer per type.
Does spring.gson.date-format cover LocalDate?
No. It applies to java.util.Date and Calendar
only. java.time types need adapters.
Why did my null fields vanish from the response?
Gson omits nulls by default. Set
spring.gson.serialize-nulls=true for Jackson’s behaviour.
Do Jackson annotations still work?
No, and silently. @JsonIgnore, @JsonProperty and the rest
are inert. Use transient, or @Expose with
excludeFieldsWithoutExposeAnnotation().
Why is a computed getter missing from the JSON?
Gson serialises fields; Jackson serialises properties. A getter with no backing field produces nothing under Gson.
Can Gson handle records?
From version 2.10. Earlier versions cannot deserialise them.
Should I define a Gson bean or a GsonBuilderCustomizer?
A customiser. It layers onto the
auto-configured builder; your own Gson bean replaces the auto-configuration wholesale.
Is Gson faster than Jackson?
Generally not on large payloads. Choose it for dependency size or consistency, not for speed, and measure if throughput matters.
Where should I go next?
The Spring Boot REST API guide covers the controllers whose output this configures, and Actuator is one of the components that keeps using Jackson regardless.