【问题标题】:Handling possible null values from an API if you are not sure if a value could be null如果您不确定某个值是否可能为空,则处理来自 API 的可能的空值
【发布时间】:2021-05-10 19:33:33
【问题描述】:
moshi 1.11.0

我有以下从 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,
)

问题是,来自 API 的数据对于某些值可能为空,并且想知道创建数据类时的最佳做法是什么。

下面的数据显示weightheight 为空,但在其他情况下也可能为其他值。

  "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,
    }]

最好为所有值设置nullable types,以便可以为它们分配空值

@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?,
)

【问题讨论】:

  • 如果某些东西真的可以为空,那么您就无能为力了。我会尽量避免拥有可为空的属性。我会尝试为它们分配默认值(何时以及是否有意义)。例如,如果age 为空,也许您可​​以从birthday 中推断出它(如果可用)。或者对于身高/体重的事情,如果它为空,您可以将其设置为0。这完全取决于您接下来对数据做什么。对于后一个示例,只需将属性声明为val weight: Int = 0
  • 只看moshi doc。我认为默认值不会起作用。因为只有在没有将 json 数据字符串序列化到 kotlin 类的值时才会使用默认值。
  • 空值 = 缺失值。类构造函数本身如果接收到null就会赋值默认值,这是Kotlin的东西,不管是moshi、jackson还是gson。

标签: kotlin moshi


【解决方案1】:

问题在于PlayerEntity 混合了两个职责:

  • 它是一个数据传输对象。因此,它应该遵守任何 API 合同。如果它允许所有字段为空,那么这些字段应该可以为空。
  • 它是域模型的一部分(我假设给定Entity 后缀)。因此,它应该遵守您的域规则。当然,您不希望仅仅因为 API 允许就拥有可为空的字段。

这种混合职责是一种捷径。只要这两个模型非常相似并且您不牺牲域的一致性,这是可以接受的。

如果未设置 weight 字段,您的域逻辑是否仍然有效?大概吧。然后继续,使它可以为空。这就是可空类型的用途。他们清楚地传达了价值可能会丢失。我不会在这里使用任何默认值:它需要您记住每次使用它时检查字段值是否等于默认值(我猜wight = 0 在您的域中没有多大意义)。可空类型将使编译器提醒您检查。

如果未设置 playerId 字段,您的域逻辑是否仍然有效?我觉得不是。那么它不应该是可空的,你需要拒绝这个值。拒绝它的简单方法将使提交的文件不可为空。该库(在您的情况下为 moshi)会抛出一些您需要处理的丑陋错误。

一些更复杂的场景怎么样,比如age?如其中一个 cmets 所述,它可以从 birthday 计算得出。但是,如果 API 有时返回 birthday,有时返回 age,有时两者都返回,但有时一个都不返回怎么办?假设您实际上对 age 字段感兴趣,但没有它您仍然可以生活。好吧,逻辑变得有点复杂,您肯定不想每次访问age 字段时都处理它。在这种情况下,考虑将PlayerEntity 拆分为PlayerEntityPlayerDto,引入一种anti-corruption-layer(简单来说就是映射器)。关键是要保持您的领域纯净并处理边界处的所有不确定性。如果您不想有两个 Player 类,您可以考虑创建一个 custom-type-adapter

更新:

关于其中一个 cmets 的索赔:

空值 = 缺失值。类构造函数本身如果接收到null就会赋值默认值,这是Kotlin的东西,不管是moshi、jackson还是gson。

肯定不是这样,null和absent是不一样的。既不是 Kotlin 本身,也不是 Moshi。考虑以下 sn-p:

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)

如果 field 不可为空,则尝试反序列化 {"field": null} 会引发异常,即使该字段具有默认值。

【讨论】:

    【解决方案2】:

    如果属性可以丢失,我会让它们可以为空。当null 是实际数据的更准确表示时,为什么要为体重和身高分配默认值0playerId 属性可能是一个例外,因为如果没有标识符,玩家实体可能不会很有用。

    归结为你想对玩家实体做什么。如果可为空的属性使实体使用起来更加复杂,您当然可以使用默认值。 Kotlin 支持可空类型和默认值。

    最后,Moshi supports both reflection and codegen for Kotlin。两种方法都有优点和缺点。使用反射,您可以省略 JsonClass 注释,您的代码将如下所示:

    // 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?
    )
    

    【讨论】:

      【解决方案3】:

      对我来说,使用 Nullable 字段的最简单方法是使用 plugin 来生成此类 JSON 数据所需的所有必要类。要管理 Non-Null 和 Nullable 字段,您只需选择 Advance 并遵循此配置。 不要忘记在 注释 标签下选择正确的注释。

      【讨论】:

        猜你喜欢
        • 2018-12-31
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-01-14
        • 2019-08-31
        • 1970-01-01
        • 2019-02-17
        • 1970-01-01
        相关资源
        最近更新 更多