If you have an android application it is most likely that it consumes some sort of service, probably web. If your app implementation follows the Clean Architecture, that service's consume logic will be in the data layer, and there will be a repository implementation that will map the service data model response into a domain entity.
But, what about that "service to domain" data transformation logic? If you own the back-end and know exactly their responses, that mapping is one to one. However, if you don’t, what to do if a mandatory property is missing? Set a default value, throw an exception… Unfortunately, that doesn’t sound like a data layer responsibility.
In this example, both entity properties are required. What if the service response has no value for them?
Kotlin Null Safety
We love Kotlin for several reasons, one of them is Null Safety. Compiling fails if we attempt to make a call on a possibly null reference without the proper nullability check. We can be sure that we won’t get any null pointer exceptions at runtime, or can’t we?
The Gson Unsafety
Let’s say we get this kind of payload as a service response:
{
property_1 = "string",
property_2 = ["value"]
}
Using Retrofit + GsonConverterFactory, we define a data model class like this
data class ServiceResponseModel(
@SerializedName("property_1") val property1: String,
@SerializedName("property_2") val property2: List<String>
)
As you can see, the property types are not nullable, but what if the payload does not contain that information? Here’s a test that exposes the issue:
@Test
fun serviceModelShouldNotHaveNullValues() {
val serviceDataModel: ServiceResponseModel =
Gson().fromJson("{ }", ServiceResponseModel::class.java)
Assert.assertNotEquals(null, serviceDataModel.property1)
Assert.assertNotEquals(null, serviceDataModel.property2)
}
This test fails with the following error:
java.lang.AssertionError: Values should be different. Actual: null
And this will actually compile and throw NullPointerException
when running:
val serviceDataModel: ServiceResponseModel =
Gson().fromJson("{ }", ServiceResponseModel::class.java)
serviceDataModel.property1.toUpperCase()
But Why?
Gson
uses reflection, skips the language rules and builds the object with the property set null.
How to solve the problem
We could use default values for the properties in the response model and the previous test passes, the properties are not null.
data class ServiceResponseModel(
@SerializedName("property_1") val property1: String = "",
@SerializedName("property_2") val property2: List<String> = emptyList()
)
We could also use a better library like moshi, but we would get an exception when deserializing the JSON payload to the data model. That’s so much better than the NullPointerException
in our Domain but still does not resolve the deeper issue.
The deeper issue: Architectural Implications
To resolve the exception issue, the Data layer needs to set the service response default values when missing. But is that a Data layer responsibility? What if the property is "id", or "name", can that be an empty string? What if the property is a non-primitive object, how to build a default (null) object? Maybe if there are some missing properties, we should throw an exception…
It’s a matter of trust
If we trust the service providers, this is a matter of communication, to establish the exact contract between the service and the client. We know which data properties will always be part of the response and which ones will be optional.
But, if we need flexibility or we are using a third-party service, we should be careful, play safe, and do not assume that all properties will be there.
Clean Architecture FTW
We can resolve the issue in a very clean way, using repository interactors in the Domain layer. The key is to move all the logic related to the properties values check into another layer, between Domain and Data. These interactors will set default values and throw exceptions if a needed property is missing.
- In Data, all response model properties should be nullable, and the repository should return an
ARepositoryEntity
instance. - In Data Interactor, the Repository Interactor implementation maps the entity with nullable properties to the Domain entity.
- In Domain, Use Cases use Repository Interactors and only care about the Domain entities.
The Clean Cost
The cost is clear, the extra layer, extra interactors, extra entities. But the benefits are huge: real nullability safety (in case of using Gson
) and more granularity handling data services responses errors. This is particularly worthy of the effort when you bave no control over the backend services, or you need the extra flexibility.
Thanks to all the reviewers and Hannah Gambino for the illustration.
Be sure to follow @doximity_tech if you'd like to be notified about new blog posts.