Home / Spring Boot / Web and REST / JSON with Jackson: Customization and Pitfalls

JSON with Jackson: Customization and Pitfalls

Spring Boot converts objects to JSON and back with Jackson. Most API bugs about dates, null fields, unknown properties and infinite recursion are solved by knowing a handful of annotations and settings. Note: Spring Boot 3.x uses Jackson 2 (packages com.fasterxml.jackson.databind), and Spring Boot 4 uses Jackson 3 (packages tools.jackson.*, annotations stay in com.fasterxml.jackson.annotation). The ideas on this page are the same; imports and a few class names differ.

How it works

@RestController returns ProductResponse
   ▼
HttpMessageConverter (MappingJackson2HttpMessageConverter)
   ▼
ObjectMapper  (a Spring bean configured by JacksonAutoConfiguration)
   ├─ modules auto-registered: JavaTimeModule (java.time), Jdk8Module (Optional), ParameterNamesModule (records/constructors)
   ├─ settings from spring.jackson.* properties
   └─ Jackson2ObjectMapperBuilderCustomizer beans
   ▼
JSON text in the HTTP response

Request: JSON ─► ObjectMapper.readValue ─► your @RequestBody object (needs a constructor/record/setters)

Global settings

spring.jackson.default-property-inclusion=non_null
spring.jackson.serialization.write-dates-as-timestamps=false
spring.jackson.deserialization.fail-on-unknown-properties=false
spring.jackson.time-zone=UTC
spring.jackson.property-naming-strategy=SNAKE_CASE

Most used annotations

import com.fasterxml.jackson.annotation.*;

@JsonInclude(JsonInclude.Include.NON_NULL)                 // skip null fields for this class
public class UserDto {

    @JsonProperty("user_name")                             // different JSON name
    private String name;

    @JsonAlias({"mail", "e-mail"})                         // accept several names when reading
    private String email;

    @JsonIgnore                                            // never in JSON
    private String passwordHash;

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy")
    private LocalDate birthDate;

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'", timezone = "UTC")
    private Instant createdAt;

    @JsonIgnoreProperties(ignoreUnknown = true)            // (class level in real code)
    private Map<String, Object> extra;

    // getters and setters
}
AnnotationUse
@JsonPropertyRename a field or force it to be included
@JsonIgnore / @JsonIgnorePropertiesHide fields (passwords, internal ids)
@JsonInclude(NON_NULL / NON_EMPTY)Skip null or empty values
@JsonAliasAccept more names on input
@JsonFormatDate and enum formats
@JsonCreator + @JsonPropertyChoose the constructor or factory used to create the object
@JsonTypeInfo + @JsonSubTypesPolymorphic types (Dog and Cat behind Animal)
@JsonViewDifferent field sets for different API views

Records and immutable objects

public record CreateOrderRequest(String customerId, List<OrderLine> lines) {
    public record OrderLine(String sku, int qty) { }
}
// Jackson 2.12+ supports records. No annotations needed when the parameter names are available.

Custom serializer and deserializer

// Money is written as "12.50 INR" and read back
public class MoneySerializer extends JsonSerializer<Money> {
    @Override
    public void serialize(Money value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        gen.writeString(value.amount().setScale(2, RoundingMode.HALF_UP) + " " + value.currency());
    }
}

public class MoneyDeserializer extends JsonDeserializer<Money> {
    @Override
    public Money deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        String[] parts = p.getValueAsString().split(" ");
        return new Money(new BigDecimal(parts[0]), parts[1]);
    }
}

@JsonComponent                          // Spring Boot 3.x. In Boot 4 this is @JacksonComponent
public class MoneyJsonComponent {
    public static class Ser extends MoneySerializer { }
    public static class Deser extends MoneyDeserializer { }
}

// Alternative: annotate the class directly
// @JsonSerialize(using = MoneySerializer.class) @JsonDeserialize(using = MoneyDeserializer.class)
public record Money(BigDecimal amount, String currency) { }

Customize the shared ObjectMapper (Boot 3.x)

@Configuration
public class JacksonConfig {
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
        return builder -> builder
                .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
                .featuresToEnable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
                .serializationInclusion(JsonInclude.Include.NON_NULL);
    }
}

Polymorphism

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
    @JsonSubTypes.Type(value = CardPayment.class, name = "card"),
    @JsonSubTypes.Type(value = UpiPayment.class, name = "upi")
})
public interface Payment { }
public record CardPayment(String cardNumber) implements Payment { }
public record UpiPayment(String vpa) implements Payment { }
// {"type":"upi","vpa":"ram@bank"}  becomes a UpiPayment

Typical problems

ProblemCauseFix
Infinite recursion / StackOverflowErrorBidirectional JPA relations (Customer has orders, order has customer)Return DTOs. Otherwise @JsonManagedReference / @JsonBackReference or @JsonIgnore on one side
Dates shown as numbersWRITE_DATES_AS_TIMESTAMPS is onSet spring.jackson.serialization.write-dates-as-timestamps=false
400 Unrecognized fieldClient sends an unknown property and fail-on-unknown is trueFix the client, or set fail-on-unknown-properties=false (understand the risk)
LazyInitializationException while writing JSONSerializer touches a lazy field after the session closedMap to a DTO inside the service
Big decimals become floating numbers in JavaScriptJS numbers lose precisionSend money as string, or use minor units (paise)

Use ObjectMapper directly

@Service
public class JsonService {
    private final ObjectMapper mapper;                         // the Spring-configured instance
    public JsonService(ObjectMapper mapper) { this.mapper = mapper; }

    public String toJson(Object o) throws JsonProcessingException { return mapper.writeValueAsString(o); }

    public <T> T fromJson(String json, Class<T> type) throws JsonProcessingException { return mapper.readValue(json, type); }

    public List<UserDto> fromJsonList(String json) throws JsonProcessingException {
        return mapper.readValue(json, new TypeReference<List<UserDto>>() { });
    }
}

Common mistake: Creating a new ObjectMapper with new ObjectMapper() in your code. It ignores all Spring Boot settings (date format, modules, naming). Inject the Spring-managed ObjectMapper.

Interview questions

How do I hide a field from the JSON response?

Use @JsonIgnore on the field or, better, do not include it in the response DTO.

Why did Jackson fail with 'no Creators, like default constructor'?

It cannot create your class. Add a default constructor and setters, use a record, or annotate a constructor with @JsonCreator.

What changes in Spring Boot 4?

Jackson 3 becomes the default: packages move to tools.jackson, some class names change (for example @JacksonComponent), and exceptions are unchecked. Annotations from com.fasterxml.jackson.annotation stay the same.