What I Learned at Work this Week: A Nullable Option in a Java Record

Mike Diaz
4 min readJul 27, 2024

--

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

--

--

Mike Diaz
Mike Diaz

Responses (4)