Serialization Problems in Spring Boot (Causes & Fixes)

Introduction

Serialization is the process of converting a Java object into JSON before it leaves your Spring Boot API. It looks automatic — until it isn’t: infinite recursion on bidirectional JPA relationships, lazy-loading exceptions from Hibernate proxies, wrong date formats, or fields silently missing from the response.

Most serialization bugs share a single root cause that isn’t obvious until you’ve been bitten by it a few times: returning JPA entities directly from controllers. This guide covers the specific failure modes that produces, and the DTO-based fix that resolves nearly all of them at once, along with real case studies of how these bugs typically surface in production.

Prerequisites

  • Jackson/JSON serialization basics
  • JPA/Hibernate entity relationships and lazy loading
  • Spring transaction management basics

Related reading:

Learning Objectives

  • Understand why bidirectional JPA relationships cause infinite recursion during serialization.
  • Diagnose and fix LazyInitializationException at the correct layer.
  • Configure Jackson date/time handling consistently.
  • Adopt the DTO pattern as the durable fix, not annotation patches.

Table of Contents

How Serialization Works in Spring Boot

By default, Spring Boot uses Jackson’s ObjectMapper to walk your object graph via getters/fields and produce JSON. Any object reachable from your controller’s return value gets serialized — including lazily-loaded JPA associations and back-references you may not have intended to expose.

Controller returns Entity
        │
        ▼
   ObjectMapper walks fields/getters
        │
        ▼
   JSON response ── may include unintended nested objects,
                     proxies, or circular references

Infinite Recursion on Bidirectional Relationships

@Entity
public class Author {
    @OneToMany(mappedBy = "author")
    private List<Book> books;
}

@Entity
public class Book {
    @ManyToOne
    private Author author;
}

The quick fix uses Jackson annotations:

@Entity
public class Author {
    @OneToMany(mappedBy = "author")
    @JsonManagedReference
    private List<Book> books;
}

@Entity
public class Book {
    @ManyToOne
    @JsonBackReference
    private Author author;
}

This works, but it’s a patch on the entity — the real fix is to stop serializing entities directly at all.

LazyInitializationException During Serialization

org.hibernate.LazyInitializationException: failed to lazily initialize a collection
of role: com.acme.Author.books, could not initialize proxy - no Session

This happens because the controller method returned before Jackson (running later, during response writing) tried to access author.getBooks(). Fetching the association eagerly within the transactional service method — or mapping to a DTO inside the transaction — avoids the issue entirely.

Date/Time Formatting Issues

spring:
  jackson:
    serialization:
      write-dates-as-timestamps: false
    date-format: yyyy-MM-dd'T'HH:mm:ss.SSSXXX
    time-zone: UTC
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'", timezone = "UTC")
private Instant createdAt;

The DTO Pattern (Real Fix)

public record AuthorResponse(Long id, String name, List<String> bookTitles) {
    public static AuthorResponse from(Author author) {
        return new AuthorResponse(
                author.getId(),
                author.getName(),
                author.getBooks().stream().map(Book::getTitle).toList()
        );
    }
}

@Transactional(readOnly = true)
public AuthorResponse getAuthor(Long id) {
    Author author = authorRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Author not found"));
    return AuthorResponse.from(author);
}

This removes recursion risk entirely, fixes lazy-loading exceptions, and gives full control over exactly which fields reach the client.

Null Field Handling

spring:
  jackson:
    default-property-inclusion: non_null

Production Case Studies

Case Study 1 – New Association Silently Leaking Data Into an Unrelated Endpoint

Symptoms: A public API endpoint’s response suddenly included unrelated internal data after a deploy with no changes to that endpoint’s code.

Investigation: A new @ManyToOne association had been added to an entity for an internal feature; because the entity was serialized directly in the public endpoint, the association’s data appeared automatically.

Root Cause: Entity returned directly from a controller, making it a de facto serialization contract nobody tracked.

Solution: Replaced the entity return type with a DTO, then added a lint rule blocking entity types from controller return signatures codebase-wide.

Case Study 2 – LazyInitializationException Only in Production Traffic

Symptoms: Passed all tests locally; threw LazyInitializationException intermittently in production.

Investigation: Test fixtures used eagerly-populated objects that never triggered lazy loading; production traffic hit a code path that accessed a lazy collection outside the transactional boundary.

Root Cause: Test data didn’t represent the lazy-loading behavior of real, minimally-populated production entities.

Solution: Moved DTO mapping inside the transactional service method, and added an integration test using genuinely lazy-loaded fixtures.

Architect’s Perspective: Trade-offs, Scale & War Story

Trade-off Analysis: DTO Layer Cost vs. Entity-Direct-Return Speed

Returning entities is faster to build initially. The DTO layer’s cost is an ongoing tax — the judgment call is when that tax is worth paying, typically the moment an API has any external consumer whose contract needs to be stable independent of internal schema changes.

At Scale: Contract Stability as the Real Driver

Coupling entity shape to API contract means every internal refactor is a potential breaking API change. DTOs decouple these two change cycles, letting the database evolve independently of the API contract.

When a Direct Entity Mapping Is Acceptable

For genuinely internal, single-team, single-deployable services with no external consumers, the DTO layer’s cost may not be justified — a narrow exception, not a general license.

Observability: Catching Serialization Regressions Before Production

Contract tests comparing serialized JSON output against a stored schema/snapshot per DTO catch accidental field leaks from entity changes before they reach production.

Production War Story

See Case Study 1 above — it’s the canonical example of why “no entities in API responses” needs to be an enforced lint rule, not a documented convention, since the person who introduces the leak is rarely aware the entity is also a serialization contract.

Common Mistakes

MistakeImpactBetter Practice
Returning JPA entities directly from controllersRecursion, lazy-loading exceptions, accidental data exposureMap to DTOs inside the transactional service method
@JsonIgnore as the only protection for sensitive fieldsSerialization-layer patch, not an authorization controlExclude sensitive fields at the DTO level
No global timezone configuredDate values shift depending on server localeSet explicit date-format and time-zone
Accessing lazy associations outside the transactionLazyInitializationException in production onlyMap to DTO inside the @Transactional method
Relying on @JsonManagedReference/@JsonBackReference permanentlyCouples JSON contract to JPA structure indefinitelyMigrate to DTOs as the durable fix

Best Practices Checklist

  • [ ] Controllers return DTOs/records, never JPA entities directly
  • [ ] DTO mapping happens inside the transactional service method
  • [ ] Explicit date/time format and timezone configured globally
  • [ ] non_null property inclusion configured where appropriate
  • [ ] Sensitive fields excluded at the DTO level, not just annotated with @JsonIgnore
  • [ ] Contract tests catch accidental field leaks from entity changes

Troubleshooting Decision Tree

Serialization error observed
│
├─ StackOverflowError / infinite JSON? → Bidirectional relationship, add DTO or @JsonManagedReference
├─ LazyInitializationException? → Access association inside @Transactional, or map to DTO there
├─ Wrong/inconsistent date format? → Configure spring.jackson.date-format + time-zone
└─ Unexpected fields exposed in response? → Replace entity return type with a DTO

Interview Questions

  1. Why does returning a JPA entity directly from a controller risk infinite recursion?
  2. What causes LazyInitializationException during JSON serialization specifically?
  3. Why is @JsonIgnore not sufficient as a security control for sensitive fields?
  4. How does mapping to a DTO inside a transactional method solve both recursion and lazy-loading issues at once?
  5. Why might Jackson serialize a date as a numeric array instead of an ISO string?

FAQs

Q: Is @JsonManagedReference/@JsonBackReference a good permanent fix?
It solves the immediate recursion error but still couples your JSON contract directly to your JPA entity structure. A DTO layer is the more maintainable long-term fix.

Q: Why does my API work fine in tests but throw LazyInitializationException in production?
Tests often use eager-loaded or fully-populated test fixtures that don’t trigger lazy loading, while production traffic accesses associations never touched during testing.

Continue Reading on SpringBootFixes

Conclusion

Nearly every serialization problem covered in this guide traces back to the same root decision: whether entities or DTOs cross the controller boundary. Annotation-level patches like @JsonManagedReference or @JsonIgnore treat symptoms; a DTO layer, built inside the transactional boundary, is the fix that resolves recursion, lazy-loading exceptions, and accidental data exposure all at once.

External References

Leave a Comment

Your email address will not be published. Required fields are marked *