What I Learned at Work this Week: A Nullable Option in a Java Record
We’ve been working on developing a new API, which means we’re receiving and transforming lots of different types of data. I’ve started to understand the pattern to building and testing a DTO (data transfer object), which can be helpful in validating the data we’re dealing with and providing a set of methods for dealing with it.
I’ve written a little about Immutable DTOs in the past, but if you don’t have time to read an old article, think of them basically as classes. They put data into a certain shape and give it methods. Java loves keeping things organized, and DTOs help us do that. But if our class has a lot of properties, we might use a lot of time and space writing getters, setters, and a Builder if we’re being fancy. This is why I recently got the advice to try a record
instead.
Java Record
I was working on a product that reads a file from an SFTP and I needed a DTO that would contain the information about that SFTP. So I started with a class that looked like this:
public class SftpSettingsDto {
private String host;
private String port;
private String user;
...
// a constructor
public SftpSettingsDto(
String host,
String port,
String user,
...
) {
this.host = host;
...
}
public String getHost() { return host; }
...
public static final class Builder {
// you get the picture
With a record, we can do the same thing with a lot less code:
public record SftpSettingsDto(
String host,
int port,
String username,
String password,
String path,
String filename,
EncryptionDto encryption) {
// custom methods, if we need
}
This will have build in getters, setters, and a constructor. Great! But I ran into a problem: The proto I was working with defined encryption
as optional
. So whenever we attempted to instantiate an SftpSettingsDto
with null
encryption, it would fail. I could have added conditions to the construction logic if I had kept my old DTO, because I was defining all the methods. But here, everything was abstract so I didn’t have access to a Builder or even the constructor.
I was surprised at how difficult it was to find a solution, but eventually I did find an article that gave an answer. Part of the reason I write this blog is to make it easier to find answers to questions that I had to struggle with. So here it is: JUST USE NULLABLE.
This works:
import javax.annotation.Nullable;
public record SftpSettingsDto(
String host,
int port,
String username,
String password,
String path,
String filename,
@Nullable EncryptionDto encryption) {
Side Effects
Adding @Nullable
allowed us to instantiate an SftpSettingsDto
record with or without an encryption
value. Here’s a look at one of the methods that uses the DTO:
public static ContractProtos.SftpSettings toProto(SftpSettingsDto sftpSettingsDto) {
return ContractProtos.SftpSettings.newBuilder()
.setHost(sftpSettingsDto.host())
.setPort(sftpSettingsDto.port())
.setUsername(sftpSettingsDto.username())
.setPassword(sftpSettingsDto.password())
.setPath(sftpSettingsDto.path())
.setFilename(sftpSettingsDto.filename())
.setTimezone(sftpSettingsDto.timezone())
// pay attention here:
.setEncryption(EncryptionDto.toProto(sftpSettingsDto.encryption()))
.build();
}
The toProto
method takes our DTO and transforms it into a protobuf format, which is necessary for sending a response after we’ve received an API call. Primitive data types or Strings are really easy to translate, but because encryption
itself is a class, we have to break it down further during this step. Let’s take a look at the EncryptionDto.toProto
method and keep in mind that sftpSettingsDto.encryption()
can be null:
public static ContractProtos.Encryption toProto(EncryptionDto encryptionDto) {
return ContractProtos.Encryption.newBuilder()
.setType(encryptionDto.getType().getContractProtoEncryptionType())
.build();
Encryption
comes from ContractProtos
and uses a Builder because it’s not a record
. It has a property called type
which specifies the format of the encryption key. This is set up as en enum, so first we get
the raw value from the DTO object and then transform it into a data type that will work with ContractProtos
. If that’s confusing, you can read more about how I’m handling enums here.
Either way, we just established that SftpSettings
considers encryption
to be nullable, so we’re setting ourselves up for a potential NullPointerException
when we run getContractProtoEncryptionType
. If we go back up to the method in SftpSettings
, we see that all the encryption stuff is wrapped up in a chain that it would be a pain to throw a conditional into. Not impossible, but it would be ugly. Instead, we can just do this:
public static ContractProtos.Encryption toProto(EncryptionDto encryptionDto) {
try {
return ContractProtos.Encryption.newBuilder()
.setType(encryptionDto.getType().getContractProtoEncryptionType())
.build();
} catch (NullPointerException e) {
return null;
}
}
If we catch the NullPointerException
, we can return null
, which is totally valid because the original proto that caused all this defines encryption
as optional
. And we can do something similar in the method that turns the object from a proto into a DTO:
public static EncryptionDto fromProto(ContractProtos.Encryption encryptionProto) {
try {
return new EncryptionDto.Builder()
.withType(EncryptionType.fromProto(encryptionProto.getType()))
.build();
} catch (IllegalStateException e) {
return null;
}
}
In this case, we might see an IllegalStateException
because EncryptionType.fromProto
can’t accept null. We can catch it and say “I know, just call it null
and move on.”
Platonic uses of Catch
It may be argued that this isn’t an ideal solution because it makes errors into “expected” behavior. A null
encryption value isn’t an “error,” but a valid way to construct an SftpSettings object. I’d be curious what an alternative solution would look like and whether it would be easier or harder to read and understand. This isn’t the best code I’ve ever written, but it does work! And that’s something we can all be happy about.
Sources
- The DTO Pattern, Baledung
- Optional as a Record Parameter in Java, Baledung
- Java Enums, W3 Schools