Skip to content
CalliCoder

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.time adapters 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.