ant2009
ant2009

Reputation: 22526

Handling possible null values from an API if you are not sure if a value could be null

moshi 1.11.0

I have the following data class that is being populated from the sportsapidata API

@JsonClass(generateAdapter = true)
data class PlayerEntity(
        @Json(name = "player_id")
        val playerId: Int,
        val firstname: String,
        val lastname: String,
        val birthday: String,
        val age: Int,
        val weight: Int,
        val height: Int,
)

The question is, the data from the API could be null for some values and wondering what is the best practice when creating the data class.

Below the data is showing null for weight and height, but in other cases it could be for other values as well.

  "data": [
    {
      "player_id": 2497,
      "firstname": "Jay-Alistaire Frederick",
      "lastname": "Simpson",
      "birthday": "1988-12-01",
      "age": 32,
      "weight": 85,
      "height": 180,
    },
    {
      "player_id": 2570,
      "firstname": "Simranjit",
      "lastname": "Singh Thandi",
      "birthday": "1999-10-11",
      "age": 21,
      "weight": null,
      "height": null,
    }]

Would it be better to have nullable types for all the values so that null can be assigned to them

@JsonClass(generateAdapter = true)
data class PlayerEntity(
        @Json(name = "player_id")
        val playerId: Int?,
        val firstname: String?,
        val lastname: String?,
        val birthday: String?,
        val age: Int?,
        val weight: Int?,
        val height: Int?,
)

Upvotes: 1

Views: 6203

Answers (3)

Mihae Kheel
Mihae Kheel

Reputation: 2651

For me the easiest way to work with Nullable fields specially if working on a huge data set was to use this plugin to generate all necessary classes I need for such JSON data. To manage Non-Null and Nullable fields you will just need to select Advance and follow this configuration. Don't forget to select the right annotation under Annotaion tab. enter image description here

Upvotes: 4

Mafor
Mafor

Reputation: 10681

The problem comes from the fact, that the PlayerEntity mixes two responsibilities:

  • It's a data transfer object. As such it should adhere to whatever the API contract is. If it allows all the fields to be null, then the fields should be nullable.
  • it's a part of the domain model (I assume so given the Entity postfix). As such it should adhere to your domain rules. Definitely you don't want to have nullable fields just because the API allows them.

This kind of mixed responsibilities is a kind of a shortcut. It is acceptable as long as the two models are fairly similar and you don't sacrifice your domain consistency.

Is your domain logic still functional if the weight field is not set? I guess so. Then go ahead, make it nullable. That's what the nullable types are for. They clearly communicate that the value might be missing. I wouldn't use any default value here: it would require you to remember to check if the field value is equal to the default value every time you use it (wight = 0 does not make much sense in your domain I guess). Nullable types will make the compiler remind you about the checks.

Is your domain logic still functional if the playerId field is not set? I guess no. Then it should not be nullable, you need to reject the value. The simples way of rejecting it would making the filed non-nullable. The library (moshi in your case) will throw some ugly error that you will need to deal with.

How about some more complex scenario, let's say the age? As mentioned in one of the comments, it could be calculated from the birthday. But what if the API sometimes returns birthday, sometimes age, sometimes both and yet sometimes none of them? Let's say you are actually interested in the age field but you can still live without it. Well, the logic is getting a bit complex and you definitely don't want to deal with it every time you access the age field. In that case, consider splitting the PlayerEntity into PlayerEntity and PlayerDto, introducing a kind of an anti-corruption-layer (a mapper simply speaking). The key point is to keep your domain pure and to deal with all the uncertainties at the boundaries. If you prefer not to have two Player classes, you might consider creating a custom-type-adapter.

UPDATE:

Regarding a claim from one of the comments:

A null value = an absent value. The class constructor itself will asign the default value if it receives null, this is a Kotlin thing, it doesn't matter if it is moshi, jackson or gson.

It's definitely not the case, null and absent are not the same. Neither for Kotlin itself, nor for Moshi. Consider the following snippet:

data class Data(var field: String? = "Test")

@Test
fun test() {

    val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
    val jsonAdapter = moshi.adapter(Data::class.java);

    println("Kotlin constructor, missing field: ${Data()}")
    println("Kotlin constructor, null: ${Data(null)}")
    println("Moshi, missing field: ${jsonAdapter.fromJson("{}")}")
    println("Moshi, null: ${jsonAdapter.fromJson("""{"field": null}""")}")
}
Kotlin constructor, missing field: Data(field=Test)
Kotlin constructor, null: Data(field=null)
Moshi, missing field: Data(field=Test)
Moshi, null: Data(field=null)

If the field was non-nullable, an attempt to deserialize {"field": null} would throw an exception, even though the field has a default value.

Upvotes: 6

Freek de Bruijn
Freek de Bruijn

Reputation: 3622

If properties can be missing, I would make them nullable. Why would you want to assign a default value of 0 to weight and height when null is a more accurate representation of the actual data? The playerId property could be an exception, because the player entity might not be very usable without the identifier.

It boils down to what you want to do with the player entities. If nullable properties make the entities more complicated to use, you can of course use default values. Kotlin has support for both nullable types and default values.

Finally, Moshi supports both reflection and codegen for Kotlin. Both approaches have pros and cons. Using reflection, you can leave out the JsonClass annotations and your code would look like this:

// build.gradle.kts
implementation("com.squareup.moshi:moshi:1.11.0")
implementation("com.squareup.moshi:moshi-kotlin:1.11.0")


// Kotlin code
import com.squareup.moshi.Json
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory


fun main() {
    val jsonPlayers = """
        {
            "data": [
                {
                    "player_id": 2497,
                    "firstname": "Jay-Alistaire Frederick",
                    "lastname": "Simpson",
                    "birthday": "1988-12-01",
                    "weight": 85,
                    "height": 180
                },
                {
                    "player_id": 2570,
                    "firstname": "Simranjit",
                    "lastname": "Singh Thandi",
                    "birthday": "1999-10-11",
                    "age": 21,
                    "weight": null,
                    "height": null
                }
            ]
        }
        """

    val moshi = Moshi.Builder()
            .addLast(KotlinJsonAdapterFactory())
            .build()

    val playerListAdapter = moshi.adapter(PlayerList::class.java)
    val players = playerListAdapter.fromJson(jsonPlayers)

    println("Players:")
    players?.data?.forEach { println(it) }
}


data class PlayerList(
        val data: List<PlayerEntity>
)


data class PlayerEntity(
        @Json(name = "player_id")
        val playerId: Int,
        val firstname: String?,
        val lastname: String?,
        val birthday: String?,
        val age: Int = 0,
        val weight: Int?,
        val height: Int?
)

Upvotes: 2

Related Questions