Back to Blog
Java

Java Record toString: Default Format and Customization

Understand the default java record tostring format, how to override it for custom output, and how to handle sensitive fields and array components.

Java RecordstoStringData ClassesObject MethodsLogging
An illustration showing a Java record with its generated toString output displayed as a formatted string, representing the automatic string representation of record components.

Java records generate a toString() method automatically, so you rarely write one by hand. The compiler produces a default implementation that includes the record class name and every component in declaration order. The default java record tostring output follows a fixed format, which makes it predictable for logging and debugging. When that format does not fit your needs, you can override the method inside the record body.

The Default toString Format

When you declare a record, the compiler generates a toString() method according to a format defined by the Java Language Specification. The output looks like this:

public record Person(String name, int age) {}
Person p = new Person("Alice", 30); System.out.println(p.toString());

The output is:

Person[name=Alice, age=30]

The format is RecordName[component1=value1, component2=value2, ...]. The record name is the simple class name, not the fully qualified name. Components appear in the order they were declared in the record header, and each one is rendered as name=value.

This format is consistent across all records compiled by a standard Java compiler, which makes it predictable for logging and debugging. You do not need to write any code to get this behavior.

How the Generated Method Works

The default toString() is compiled directly into the record class. It is not implemented through reflection, and it does not inspect fields at runtime. The compiler knows the record components at compile time and generates a method that concatenates the component values directly.

This matters for two reasons. First, the generated method is fast because there is no reflective lookup involved. Second, the output is deterministic: the same record instance always produces the same string, and the format does not depend on runtime state.

For a record with many components, the generated method still performs a single pass over the values. The cost is proportional to the number of components and the length of their string representations.

Overriding toString for Custom Output

The default format is useful, but it is not always what you want. You can override toString() inside the record body just like you would in a regular class.

public record Person(String name, int age) { @Override public String toString() { return name + " (" + age + " years old)"; } }

Now the output is:

Alice (30 years old)

The override replaces the generated method entirely. The record still provides the generated equals() and hashCode(), so changing toString() does not affect equality behavior.

A common reason to override is to produce a format that matches existing log parsers or monitoring tools. If your team already parses log lines in a specific shape, a custom toString() lets records fit that format without changing the rest of the code.

Hiding Sensitive Data in toString

Records include every component in the default toString(). If a record holds a password, API key, token, or other sensitive value, that value appears in logs, stack traces, and exception messages whenever the record is printed.

public record Credentials(String username, String password) {}

Printing an instance of this record exposes the password:

Credentials[username=admin, password=secret123]

The fix is to override toString() and omit the sensitive component:

public record Credentials(String username, String password) { @Override public String toString() { return "Credentials[username=" + username + "]"; } }

This is a security consideration that applies to any class, but records make it easy to forget because the default output already includes everything. If you log entire record instances, review whether any component contains data that should not be written to logs.

The Array Component Gotcha

A record can have an array as a component. The default toString() calls the array's own toString(), which produces the identity-based representation rather than the contents.

public record Matrix(int[][] values) {}
Matrix m = new Matrix(new int[][] {{1, 2}, {3, 4}}); System.out.println(m);

The output is something like:

Matrix[values=[[I@1b6d3586]

That hashcode-looking suffix is the default Object.toString() behavior for arrays. It is not useful for debugging. To fix this, override toString() and use Arrays.deepToString() for the array component:

import java.util.Arrays; public record Matrix(int[][] values) { @Override public String toString() { return "Matrix[values=" + Arrays.deepToString(values) + "]"; } }

For a one-dimensional array, Arrays.toString() is sufficient. For nested arrays, use Arrays.deepToString(). This is a common mistake because the default output looks valid but contains no useful information about the array contents.

Choosing Between Default and Custom toString

The default toString() is the right choice in most cases. It is concise, deterministic, and requires no maintenance. Use it when:

  • The record is part of a domain model where the default format is acceptable for logs and debugging.
  • No component contains sensitive data.
  • No component is an array that needs readable output.

Override toString() when:

  • The output must match an external format, such as a log parser or a serialized string format.
  • A component contains sensitive data that should not be logged.
  • An array component needs its contents shown rather than its identity hash.
  • The default format is too verbose for records with many components, and a shorter form improves log readability.

The decision is specific to each record. A record used internally for computation may keep the default, while a record that crosses a system boundary may need a custom implementation.

Maintainability Considerations

A custom toString() adds code that must be kept in sync with the record components. If you add a component later, the custom method will not include it automatically, and the output can silently become incomplete. The default implementation always reflects the current component list because the compiler regenerates it.

When you do override toString(), keep the implementation simple. Use string concatenation or String.format for a small number of components. For records with many components, build the string with a StringBuilder to keep the code readable. Avoid putting business logic in toString(); it should only produce a representation of the current state.

The generated equals() and hashCode() remain unchanged when you override toString(). That means two records with the same components are still equal even if their string representations differ. Do not rely on toString() output for equality checks or as a key in a map.

java record tostring: Practical Usage and Code Examples | RYUSLOG DEV