web-dev-qa-db-ja.com

Gsonを使用して、Kotlinデータクラスでnullを許容しないフィールドにnullを含めることができるのはなぜですか?

Kotlinではdata class

data class CountriesResponse(
    val count: Int,
    val countries: List<Country>,
    val error: String)

次に、それを使用してJSONを解析できます(例: "{n:10}")。この場合、オブジェクトval countries: CountriesResponse、これらの値を含むRetrofitFuelまたはGsonから受信:count = 0, countries = null, error = null

Kotlin + Gson-データクラスに対してnullの場合にemptyListを取得する方法 には、別の例があります。

後でcountriesを使用しようとすると、ここで例外が発生します:val size = countries.countries.size: "kotlin.TypeCastException:nullをnull以外の型kotlin.Intにキャストすることはできません。"コードを記述して?これらのフィールドにアクセスすると、Android Studioは?.と警告:Unnecessary safe call on a non-null receiver of type List<Country>

したがって、?データクラス内?実行時にアプリケーションがnullをnullにできない変数に設定できるのはなぜですか?

12
CoolMind

これは、Gsonが安全でない(Java.misc.Unsafeのような)インスタンス構築メカニズムを使用してクラスのインスタンスを作成し、コンストラクタをバイパスして、フィールドを直接設定するために発生します。

いくつかの調査については、このQ&Aを参照してください: Got Desrialization with Kotlin、Initializerブロックが呼び出されていません

結果として、Gsonはコンストラクションロジックとクラス状態の不変条件の両方を無視するため、これによって影響を受ける可能性のある複雑なクラスに使用することは推奨されません。セッターの値チェックも無視します。

Jackson(上記のQ&Aで説明)や kotlinx.serialization などのKotlin対応のシリアル化ソリューションを検討してください。

17
hotkey

これは、Gsonを含む多くのJsonパーサーが、存在しないフィールドを静かに飲み込むようにするという設計上の決定です。オブジェクトはnullに設定され、プリミティブは0に設定されています。これにより、APIエラーが完全にマスクされ、さ​​らに下流で暗号エラーが発生します。

このため、2段階の解析をお勧めします。

package com.example.transport
//this class is passed to Gson (or any other parser)
data class CountriesResponseTransport(
   val count: Int?,
   val countries: List<CountryTransport>?,
   val error: String?){

   fun toDomain() = CountriesResponse(
           count ?: throw MandatoryIsNullException("count"),
           countries?.map{it.toDomain()} ?: throw MandatoryIsNullException("countries"),
           error ?: throw MandatoryIsNullException("error")
       )
}

package com.example.domain
//this one is actually used in the app
data class CountriesResponse(
   val count: Int,
   val countries: Collection<Country>,
   val error: String)

はい、2倍の労力がかかりますが、APIエラーをすぐに特定し、修正できない場合にそれらのエラーを処理する場所を提供します。

   fun toDomain() = CountriesResponse(
           count ?: countries?.count ?: -1, //just to brag we can default to non-zero
           countries?.map{it.toDomain()} ?: ArrayList()
           error ?: MyApplication.INSTANCE.getDeafultErrorMessage()
       )

はい、より多くのオプションを使用して、より優れたパーサーを使用できますが、使用しないでください。あなたがしなければならないのは、パーサーを抽象化して、どれでも使えるようにすることです。今日、どれほど高度で構成可能なパーサーを見つけても、結局はそれがサポートしない機能が必要になるでしょう。そのため、私はGsonを最小公分母として扱います。

記事があります リポジトリパターンのより大きなコンテキストで使用(および拡張)されたこの概念を説明します。

0
Agent_L