Java Records — Rename Breaks Jackson Deserialization
Renaming a Java Record component without @JsonProperty causes Jackson 'no Creators' error.
- Java Records are immutable data carriers that auto-generate constructor, accessors, equals, hashCode, toString from the component list
- All components are private final fields — no mutability, no hidden state
- Records cannot extend classes, but can implement interfaces and be generic
- Serialization works via canonical constructor — component names must match JSON properties exactly
- Performance: Records have no reflection overhead at construct time; accessor methods are direct field reads
- Biggest mistake: Adding mutable collections (e.g., ArrayList) to a Record — reference is final, contents are not
Imagine every time you wanted to write someone's name on a sticky note, you had to first build an entire filing cabinet, label every drawer, and install a lock — just to hold one sticky note. That's what creating a plain data class in Java used to feel like. A Java Record is the sticky note: you describe what data you want to hold, and Java builds the filing cabinet automatically behind the scenes.
Every Java developer has written it: the humble data-carrier class. You need to pass a user's name and email around your application, so you create a class, write two private fields, a constructor, two getters, equals(), hashCode(), and toString(). That's roughly 30 lines of ceremony for two pieces of data. Multiply that by every DTO, value object, and API response model in a real codebase and you're maintaining thousands of lines that do nothing except hold data. Java 16 shipped Records as a permanent language feature to fix exactly this problem.
Records aren't just syntactic sugar — they encode a design intent. When you declare a Record, you're telling every developer who reads your code: 'This type is a transparent, immutable carrier of data. It has no hidden state. It won't surprise you.' That's a contract, not just a shortcut. Prior to Records, you could approximate this with Lombok's @Value or manual immutable classes, but neither was a first-class language construct that the JVM and tools like serialization frameworks, pattern matching, and sealed interfaces understand natively.
By the end of this article you'll know exactly what a Record generates under the hood, when to reach for one versus a regular class, the real-world patterns where Records shine (DTOs, API responses, compound map keys, configuration objects), and the subtle traps that catch even experienced developers. You'll also have crisp answers ready for the interview questions that consistently come up when Records are on the job description.
What Is a Java Record?
A Java Record is a restricted form of class that acts as a transparent carrier for immutable data. Declare it with the record keyword followed by a component list in parentheses. The compiler generates a canonical constructor, accessor methods (with the same name as the component), equals(), hashCode(), and toString(). You cannot add instance fields beyond the component list, and all components are implicitly private final. This design enforces immutability and transparency — what you see in the declaration is exactly what you get.
user.username() not user.getUsername(). This is a deliberate break from JavaBeans conventions.Anatomy of a Record: What the Compiler Generates
When you write a Record, the compiler generates a lot of code for you. Here is exactly what gets produced for a simple record like record Point(int x, int y) {}: - A final class that extends java.lang.Record - A private final field for each component - A canonical constructor that initialises all fields - Public accessor methods with the same name as the component (e.g., x()) - equals() that compares component values - hashCode() that derives from components - toString() that lists components in the order they appear - No default constructor, no setters, no ability to add instance fields. The compiler marks the class as final and the accessor methods with the @Override annotation on the implicit accessors for component access.
- Tuple: data bundled together, positionally matched to components.
- Named type: unlike generic tuples, the record name carries semantic meaning.
- Behaviour allowed: you can add instance methods (but not instance fields).
- Canonical constructor is the single point of creation — all data flows through it.
When to Use Records vs Traditional Classes
Records are not a universal replacement for classes. They are designed for transparent data carriers. Use Records when: - The class is purely a data structure: fields are final, no behaviour except derived from data. - You need equals/hashCode based on all fields (value equality). - The class does not have complex inheritance requirements (Records cannot extend). Use a traditional class when: - You need mutable fields or setters. - You need to extend another class. - You need a default constructor or different constructor signatures. - You need to control serialization separately from component names. - You need lazy initialisation or caching fields. - You need to use JPA entities (Hibernate requires a no-arg constructor and often setters).
Records and Serialization: Jackson, JSON, and More
Java Records work well with popular serialization libraries, but there are gotchas. Jackson 2.12+ supports Records natively by using the canonical constructor to create instances. Key points: - Jackson uses the component parameter names to match JSON keys. Ensure you compile with -parameters flag or use the ParameterNamesModule. - If you rename a component, JSON property mapping breaks — use @JsonProperty to preserve the old name. - Records are immutable, so after deserialization you cannot modify them. This is good for thread safety but can be inconvenient if you need to adjust fields. - For XML serialization (JAXB), Records work but require an XmlJavaTypeAdapter because JAXB expects setters. - For custom serialization, implement the Serialization interface? Records are not serializable by default — you can implement Serializable explicitly, but be careful about serialized form changes when components change.
Records with Pattern Matching and Sealed Classes
Java 17+ introduced pattern matching for instanceof and switch expressions, and sealed classes. Records integrate seamlessly with pattern matching because their state is fully described by their components. You can destructure a Record directly in a pattern: - if (obj instanceof Point(int x, int y)) { ... } extracts x and y. - In switch expressions, you can match on record patterns with nested patterns. - Sealed interfaces allow precise modelling of algebraic data types, where Records are the concrete implementations. This combo is extremely powerful for functional-style programming: you define a set of possible data shapes (sealed interface), each implemented by a Record, then switch exhaustively on the type.
- Sealed interface: the sum type — lists all possible shapes (alternatives).
- Records: the product types — each shape has a fixed set of components.
- Switch with record patterns: exhaustive matching, no else-if chains.
- The compiler ensures all cases are covered — add a new Record, and switch must handle it or compilation fails.
Common Pitfalls and How to Avoid Them
Despite their simplicity, Records have traps that can bite in production: 1. Mutable components: A Record component that is a reference to a mutable object (e.g., List, Date) is not protected from mutation. The reference is final, but the object contents can be changed. Defensive copies in the canonical constructor can help. 2. Inheritance: Records cannot extend any class (they implicitly extend java.lang.Record) and cannot be extended. If you need polymorphism, use sealed interfaces with Records. 3. Reflection-based frameworks: Libraries that rely on setters or no-arg constructors (e.g., some ORMs, old serializers) break with Records. Check compatibility before using. 4. Equals/hashCode performance: For Records with many fields, the generated hashCode loops over all fields. For large records used as map keys, this can be slower than a well-tuned custom implementation. 5. Lombok conflicts: Avoid combining @Data or @Value with Records — they generate redundant code. 6. Serialization versioning: Adding or removing a component changes the serialized form without a warning. Plan for versioning using JSON schema evolution or @JsonProperty.
Deserialization Failure After Record Component Rename
- Record component names define the serialization contract — treat them as API surface.
- Any rename must be accompanied by @JsonProperty on the new component to preserve backward compatibility.
- Always include a deserialization test in CI that exercises the JSON mapping of each Record DTO.
ParameterNamesModule()) or use Java 8+ -parameters compiler flag.Key takeaways
Interview Questions on This Topic
Why were Records introduced in Java? What problem do they solve?
Frequently Asked Questions
That's Java 8+ Features. Mark it forged?
4 min read · try the examples if you haven't