A record pattern tests whether a value is an instance of a record and, if it is, extracts the record components into variables in the same step, as in obj instanceof Money(double amount, String currency). Record patterns are final since Java 21 (JEP 440) and work in instanceof and in switch.
We use record patterns to read the data out of records without casts and accessor calls, to match nested data such as an order that contains a money amount, and to write a switch over a sealed hierarchy that the compiler checks for missing cases.
The following example takes a Money record apart with instanceof.
record Money(double amount, String currency) {}
Object value = new Money(120.0, "EUR");
String text = "";
if (value instanceof Money(double amount, String currency)) {
text = amount + " " + currency;
}
String formatted = text; // "120.0 EUR"
Notice that amount and currency are ready to use inside the if block, with no cast to Money and no call to amount(). We build up from instanceof to nested patterns, var and generic records, and finish with a switch over order events, the rules for null and MatchException, and the version history.
1. What Is a Record Pattern?
A pattern combines a test with variables that receive values only when the test succeeds. Java 16 added the first kind, the type pattern Money m, which tests the type and binds the whole object to m. A record pattern also binds the components.
A record pattern is the record type followed by a list of patterns, one per component in header order. Each component pattern is a type pattern such as double amount, a var pattern, an unnamed pattern _ or another record pattern. The variable names do not have to match the component names.
| Pattern | Example | Binds | Matches null |
|---|---|---|---|
| Type pattern | value instanceof Money m | The whole record as m | No |
| Record pattern | value instanceof Money(double a, String c) | Each component | No |
| Record pattern with var | value instanceof Money(var a, var c) | Each component, types inferred | No |
| Nested record pattern | event instanceof Paid(Money(var a, var c), var m) | Components of components | No |
To get the components, the pattern calls the accessor methods of the record, such as amount() and currency(). Only records can be taken apart this way, because the compiler knows their components from the header.
2. Record Patterns with instanceof
Before record patterns, a type pattern gave us the record, and every component needed an accessor call. The type pattern version and the record pattern version do the same work.
Object payment = new Money(80.0, "USD");
String before = "";
if (payment instanceof Money m) {
before = m.amount() + " " + m.currency();
}
String withTypePattern = before; // "80.0 USD"
Object paid = new Money(80.0, "USD");
String after = "";
if (paid instanceof Money(double amount, String currency) && amount > 50) {
after = "large payment in " + currency;
}
String withRecordPattern = after; // "large payment in USD"
The pattern variables are in scope wherever the compiler knows that the match succeeded. That includes the right side of &&, as in the amount > 50 check, and the code after an if that returns when the match fails.
static String currencyOf(Object value) {
if (!(value instanceof Money(var amount, var currency))) {
return "none";
}
return currency;
}
String euro = currencyOf(new Money(5.0, "EUR")); // "EUR"
String none = currencyOf("five euros"); // "none"
3. Nested Record Patterns
A record component can be another record, and a record pattern can contain another record pattern for it. The outer pattern matches only when the type test and every nested pattern match, so one condition replaces a chain of casts and null checks.
The following example models two events of a food delivery app. A Paid event holds a Money total and a payment method.
record Address(String city, String zip) {}
sealed interface OrderEvent permits Placed, Paid, Delivered, Cancelled {}
record Placed(String orderId, Address address) implements OrderEvent {}
record Paid(Money total, String method) implements OrderEvent {}
record Delivered(String orderId, int minutes) implements OrderEvent {}
record Cancelled(String orderId, String reason) implements OrderEvent {}

Object event = new Paid(new Money(120.0, "EUR"), "card");
String result = "";
if (event instanceof Paid(Money(var amount, var currency), String method)) {
result = method + " payment of " + amount + " " + currency;
}
String nested = result; // "card payment of 120.0 EUR"
Without nesting, the same check needs a type pattern for Paid, a call to total(), a null check and two more accessor calls. Nesting saves the most code when the data is two or three levels deep, such as an order that contains an address that contains a city.
4. Using var and Generic Records
A component pattern can use var instead of a type, and the compiler takes the type from the record header. In Money(var amount, var currency), amount is a double and currency is a String, so the pattern stays short without losing type safety. We use var when the types are obvious from the record name and explicit types when they help the reader.
For generic records, the compiler also infers the type arguments of the record pattern from the type of the value we match.
record Pair<A, B>(A first, B second) {}
static String stockLine(Pair<String, Integer> pair) {
return switch (pair) {
case Pair(var item, var count) when count == 0 -> item + " sold out";
case Pair(var item, var count) -> item + " x" + count;
};
}
String soldOut = stockLine(new Pair<>("soup", 0)); // "soup sold out"
String inStock = stockLine(new Pair<>("cake", 4)); // "cake x4"
In stockLine(), the pattern Pair(var item, var count) is inferred as Pair<String, Integer>(String item, Integer count), so count == 0 compiles without a cast. A generic record pattern cannot match a type argument that would need an unchecked cast, such as Pair<String, Integer> against a value of type Object.
5. Record Patterns in switch
Record patterns are most useful in a switch, which tests one value against several patterns in order. Each case can add a guard with when, which is a boolean condition that uses the pattern variables. Switch expressions with patterns came with JEP 441 in Java 21, together with record patterns.
static String describe(OrderEvent event) {
return switch (event) {
case Placed(var id, Address(var city, var zip)) -> id + " placed for " + city;
case Paid(Money(var amount, var currency), var method) when amount > 100 ->
"large " + method + " payment " + amount + " " + currency;
case Paid(Money(var amount, var currency), var method) -> method + " payment " + amount;
case Delivered(var id, var minutes) when minutes > 45 -> id + " late by " + (minutes - 45) + " min";
case Delivered(var id, var minutes) -> id + " delivered";
case Cancelled(var id, var reason) -> id + " cancelled: " + reason;
};
}
String placed = describe(new Placed("A-1", new Address("Pune", "411001"))); // "A-1 placed for Pune"
String large = describe(new Paid(new Money(120.0, "EUR"), "card")); // "large card payment 120.0 EUR"
String small = describe(new Paid(new Money(20.0, "EUR"), "cash")); // "cash payment 20.0"
String late = describe(new Delivered("A-1", 60)); // "A-1 late by 15 min"
String cancelled = describe(new Cancelled("A-2", "no driver")); // "A-2 cancelled: no driver"
The switch has no default because OrderEvent is a sealed interface and every permitted record has a case without a guard. If we add a Refunded record to the permits list, describe() stops compiling with “the switch expression does not cover all possible input values”, so the new event cannot be forgotten. The sealed classes guide explains the permits clause.
The order of cases matters. A guarded case must come before the unguarded case for the same record, and a type pattern such as case Paid p placed before case Paid(Money m, String s) is a compile error, because the second case can never match.
error: this case label is dominated by a preceding case label
case Paid(Money m, String s) -> "x";
^
6. Null Values and MatchException
A null value never matches a record pattern. With instanceof, the result is false. A switch without a case null throws NullPointerException for a null selector, before it looks at any pattern.
Object nothing = null;
boolean matchesNull = nothing instanceof Money(var amount, var currency); // false
String npe = describe(null); // NullPointerException
A null component behaves differently in the two places. A nested record pattern such as Money(var amount, var currency) does not match a null total, so instanceof returns false. In a switch that the compiler considers exhaustive, no case matches, and Java throws MatchException.
Paid unpaid = new Paid(null, "card");
boolean nestedMatch = unpaid instanceof Paid(Money(var amount, var currency), var method); // false
boolean typeMatch = unpaid instanceof Paid(Money total, var method); // true
String broken = describe(unpaid); // MatchException
The nested type pattern Money total matches null, as the second line shows, because it is unconditional for the component type. Validating components in the compact constructor with Objects.requireNonNull() keeps null out of the record and avoids the exception.
Java also throws MatchException when an accessor method throws during matching. The original exception is the cause.
record Reading(double celsius) {
public double celsius() {
if (Double.isNaN(celsius)) {
throw new IllegalStateException("sensor offline");
}
return celsius;
}
}
Object reading = new Reading(Double.NaN);
boolean warm = reading instanceof Reading(double c) && c > 20; // MatchException: java.lang.IllegalStateException: sensor offline
7. Ignoring Components with the Unnamed Pattern
Many cases need only one or two components. Since Java 22, the unnamed pattern _ matches a component without binding it, which makes it clear that the value is not used.
static String city(OrderEvent event) {
return switch (event) {
case Placed(_, Address(var town, _)) -> town;
case Paid _, Delivered _, Cancelled _ -> "unknown";
};
}
String pune = city(new Placed("A-3", new Address("Pune", "411001"))); // "Pune"
String unknown = city(new Delivered("A-3", 20)); // "unknown"
The unnamed pattern _ is allowed only inside a record pattern or in a type pattern such as Paid _, and a case label with several patterns cannot declare variables.
8. A Real-World Example with Order Events
A delivery platform pushes order events to a notification service, which decides who gets a message. The rules depend on the event type and on values inside it, which is the case record patterns handle best.
static Optional<String> notification(OrderEvent event) {
return switch (event) {
case Paid(Money(var amount, _), _) when amount >= 500 -> Optional.of("fraud-team: review payment " + amount);
case Delivered(var id, var minutes) when minutes > 60 -> Optional.of("customer: voucher for order " + id);
case Cancelled(var id, var reason) -> Optional.of("customer: order " + id + " cancelled, " + reason);
case Placed _, Paid _, Delivered _ -> Optional.empty();
};
}
Optional<String> fraud = notification(new Paid(new Money(750.0, "EUR"), "card")); // Optional[fraud-team: review payment 750.0]
Optional<String> voucher = notification(new Delivered("B-7", 75)); // Optional[customer: voucher for order B-7]
Optional<String> quiet = notification(new Delivered("B-8", 30)); // Optional.empty
Before Java 21, the same rules took an if chain with instanceof, casts and accessor calls, plus a final else that accepted any new event type without a compile error, so a missing case showed up only in a test or in production.
9. Record Patterns Version History
Record patterns went through two previews before they became final, and one form was dropped on the way.
| Java version | Change | JEP |
|---|---|---|
| Java 19 | First preview of record patterns | JEP 405 |
| Java 20 | Second preview, inference for generic records, record patterns in enhanced for loops, named record patterns removed | JEP 432 |
| Java 21 | Final, record patterns in enhanced for loops removed | JEP 440 |
| Java 22 | Unnamed pattern _ final | JEP 456 |
| Java 25 | Primitive types in patterns, third preview | JEP 507 |
On Java 21 and later, record patterns need no compiler flags. Code from Java 20 tutorials that writes for (Point(var x, var y) : points) no longer compiles, so we match inside the loop body instead. The Java 21 features overview lists the related changes of that release.
10. Record Patterns FAQs
Code from the Java 19 and 20 previews differs from Java 21 in loops and named patterns, so the version questions come first, followed by classes and the default case.
10.1. Which Java version made record patterns final?
Java 21, with JEP 440. Java 19 and Java 20 had them as a preview that needed –enable-preview.
10.2. Can we use record patterns with classes that are not records?
No. A record pattern needs a record type, because the compiler must know the components and their accessors. For a normal class, we use a type pattern such as obj instanceof Customer c and call its getters.
10.3. Do record patterns work in enhanced for loops?
No. Java 20 previewed them in the header of an enhanced for loop, and Java 21 removed that form. We write for (Point p : points) and match p inside the loop.
10.4. Do we need a default case when switching over records?
Not when the selector type is a sealed interface or a record and the cases cover every permitted type. With a selector of type Object, a default case is required.
10.5. Can a record pattern bind the whole record and its components at the same time?
No. Java 19 previewed named record patterns such as Point(var x, var y) p, and Java 20 removed them. When we need the whole record, we use a type pattern such as Paid paid and call its accessors.
11. Conclusion
A record pattern checks the type of a value and extracts the record components in one step. Patterns nest, take var for inferred types, work with generic records, and combine with when guards in a switch.
With a sealed interface of records, a switch with record patterns needs no default, and the compiler reports every missing case. We keep null out of record components, because a null never matches a record pattern and a null component in an exhaustive switch ends in MatchException. The Java features by version page shows how pattern matching grew from Java 16 to Java 25.
12. References
- JEP 440, Record Patterns
- JEP 441, Pattern Matching for switch
- Oracle, Record Patterns
- JLS 25, Patterns
- MatchException Javadoc
Happy Learning !!