Home About Contact
Kotlin , JSON

org.json.JSONObject で JSON データを読むときに欠落しているパラメータを上手に扱いたい

JSONは柔軟にデータを表現できて便利だが、 org.json.JSONObject を使ってプログラムでそれを読むときに、 プログラムからみて想定外のデータ構造(たとえば、キー name に対する値があるはずだが、無い場合もある、のような場合)に 例外が発生してプログラムがそこでストップすることになる。 それは困るので、例外をキャッチして、デフォルト値を代わりに適用するなどというコードを書くわけだが、 そのようなコードは非常に読みづらい。これをなんとかしてスマートに書く方法はないのか? ということであれこれ試行錯誤した結果をここに書き残す。

使用言語は kotlin で kotlinc-jvm version 1.4.21 を使用します。

準備 kotlin script で org.json ライブラリを使いたい

kotlinc -cp hoge.jar すれば hoge ライブラリを使えるらしい。 ならば org.json の jar をこちら github の JSON-java から ダウンロードして使おう。

main.kts でスクリプトを書いて:

kotlinc -cp json-20201115.jar -script main.kts

のようにすれば、org.json を使用したコードを実行することができます。

本題 パラメータ欠損があるこんな JSON をスマートに読む

{
    "penList" : [
        {"name": "red-bold", "color": "240,0,0", "width": 6},
        {"name": "black-normal", "color": "0,0,0"},
        {"name": "light", "width": 3},
        {"color": "102,102,102"}
    ]
}

複数ペンの情報が入ったJSONです。 ペンオブジェクトは name, color, width の属性が想定されていますが、必ずしもすべての属性が揃っているとは限らない、というデータです。

まずは、これをペンオブジェクトを読み込んで出力するところまで書いてみます。

main.kts

import org.json.*

val jsonData = """{
    "penList" : [
        {"name": "red-bold", "color": "240,0,0", "width": 6},
        {"name": "black-normal", "color": "0,0,0"},
        {"name": "light", "width": 3},
        {"color": "102,102,102"}
    ]
}"""

val rootObj = JSONObject(jsonData)
val penListArray =  rootObj.getJSONArray("penList")

0.until( penListArray.length() ).forEach { index->
    val penObject = penListArray[index] as JSONObject
    println( "- ${penObject}" )
}

これを実行:

$ kotlinc -cp json-20201115.jar -script main.kts
- {"color":"240,0,0","name":"red-bold","width":6}
- {"color":"0,0,0","name":"black-normal"}
- {"name":"light","width":3}
- {"color":"102,102,102"}

うまくいきました。

次に penObject をそのまま出力するのではなく、name, color, width を厳密に取得して出力するコードに変更:

0.until( penListArray.length() ).forEach { index->
    val penObject = penListArray[index] as JSONObject
    val name = penObject.get("name") as String
    val color = penObject.get("color") as String
    val width = penObject.get("width") as Int
    println( "- ${name}, ${color}, ${width}" )
}

これを実行:

$ kotlinc -cp json-20201115.jar -script main.kts
- red-bold, 240,0,0, 6
org.json.JSONException: JSONObject["width"] not found.
    at org.json.JSONObject.get(JSONObject.java:572)

はい、失敗。
予想通り、最初のペンオブジェクトはすべてパラメータが揃っているので、正常に出力できましたが、次のペンオブジェクトは、width パラメータが欠損しているので、 例外が発生してしまいました。

それでは 例外をキャッチするために try で囲むという雑な対応をすれば:

0.until( penListArray.length() ).forEach { index->
    val penObject = penListArray[index] as JSONObject
    try {
        val name = penObject.get("name") as String
        val color = penObject.get("color") as String
        val width = penObject.get("width") as Int
        println( "- $name, $color, $width" )
    }
    catch(e: Exception){}
}

例外でプログラムはストップしないけれども、すべてパラメータが揃ったペンオブジェクトだけしか出力できません。

$ kotlinc -cp json-20201115.jar -script main.kts
- red-bold, 240,0,0, 6

ならば、パラメータごとに全部例外を個別にキャッチしていけば、なんとかなるにはなる:

0.until( penListArray.length() ).forEach { index->
    val penObject = penListArray[index] as JSONObject

    var name: String? = null
    var color: String? = null
    var width: Int? = null

    try {
        name = penObject.get("name") as String
    }
    catch(e: Exception){
        name = "unknown"
    }

    try {
        color = penObject.get("color") as String
    }
    catch(e: Exception){
        color = "0,0,0"
    }

    try {
        width = penObject.get("width") as Int
    }
    catch(e: Exception){
        width = 1
    }

    println( "- $name, $color, $width" )
}

実行すると:

$ kotlinc -cp json-20201115.jar -script main.kts
- red-bold, 240,0,0, 6
- black-normal, 0,0,0, 1
- light, 0,0,0, 3
- unknown, 102,102,102, 1

意図する結果が得られた。

ただ、実際のケースでは、パラメータが大量にあったり、そのメンテナンスで、パラメータ数が増減すると面倒なことになる。

try catch を隠蔽するヘルパーを用意する

結局、該当のパラメータの値を取得するときに、例外を投げないで、null を返してほしいだけ。 そうすれば エルビス演算子 ?: を使って null の場合はデフォルト値を使用する、というコードが書ける。

つまり getParam というヘルパー関数を追加:

fun <T> getParam(jsonObject: JSONObject, key: String): T? {
    try {
        val value = jsonObject.get(key) as T
        return value
    }
    catch(e: Exception){}

    return null
}

この関数内で例外をキャッチして、例外が発生しなければ実際の値、発生すれば null を返すようにする。

それではこれを使って書き直しましょう。

0.until( penListArray.length() ).forEach { index->
    val penObject = penListArray[index] as JSONObject

    val name  = getParam<String>(penObject, "name")?:"unknown"
    val color = getParam<String>(penObject, "color")?:"0,0,0"
    val width = getParam<Int>(penObject, "width")?:1
    println( "- $name, $color, $width" )
}

これでかなり処理したいことだけをコードで表現できるようになりました。

更に改良する

ここで処理したいことは、ペンの情報として name, color, width を取得したい、欠損していたらデフォルト値で補いたい ということです。 したがって、こちらとしては以下のようにコードできればうれしい。

val keyList = listOf("name", "color", "width")
val defaultValueList = listOf("unknown", "0,0,0", 1)
val penParamsList = ...

あとのことは、関心がない。

それでは、このように記述できるようにコードを修正しよう。

getParam 関数の改良

JSONデータを読むときに、この例では String, Int の二種類があるので、それを明示的に指定していた。 これを型推論により、明示的に指定しなくても済むようにしたい。

kotlin のコンパイラが型を推論できるようにするには、関数の引数にその型を含めればよい。 そこで、引数の最後にデフォルト値を指定するように変更:

fun <T> getParam(jsonObject: JSONObject, key: String, defaultValue: T): T {
    try {
        val value = jsonObject.get(key) as T
        return value
    }
    catch(e: Exception){}

    return defaultValue
}

これで、呼び出し元では:

val name  = getParam(penObject, "name", "unknown")

このように型を指定しないで済むようになります。 改良版の getParam 関数を使うことで、以下のようにコードできるようになります:

0.until( penListArray.length() ).forEach { index->
    val penObject = penListArray[index] as JSONObject

    val name  = getParam(penObject, "name", "unknown")
    val color = getParam(penObject, "color", "0,0,0")
    val width = getParam(penObject, "width", 0)
    println( "- $name, $color, $width" )
}

ここまでできればあとは、関心のある部分だけを抜き出した形でコードすればよい:

val keyList = listOf("name", "color", "width")
val defaultValueList = listOf("unknown", "0,0,0", 1)

0.until( penListArray.length() ).forEach { index->
    val penObject = penListArray[index] as JSONObject

    val penParams = keyList.zip(defaultValueList).map { pair->
        val key = pair.first
        val defaultValue = pair.second
        val value = getParam(penObject, key, defaultValue)
        value
    }
    println( "- ${penParams[0]}, ${penParams[1]}, ${penParams[2]}" )
}

さらに整理して:

val keyList = listOf("name", "color", "width")
val defaultValueList = listOf("unknown", "0,0,0", 1)

val penParamsList = 0.until( penListArray.length() ).map { index->
    val penObject = penListArray[index] as JSONObject
    keyList.zip(defaultValueList).map { getParam(penObject, it.first, it.second) }
}

penParamsList.forEach {
    println( "- ${it[0]}, ${it[1]}, ${it[2]}" )
}

これで完成です。

完成したコード

main.kts

import org.json.*

//
// ヘルパー関数
//
fun <T> getParam(jsonObject: JSONObject, key: String, defaultValue: T): T {
    try {
        val value = jsonObject.get(key) as T
        return value
    }
    catch(e: Exception){}

    return defaultValue
}


//
// データ
//
val jsonData = """{
    "penList" : [
        {"name": "red-bold", "color": "240,0,0", "width": 6},
        {"name": "black-normal", "color": "0,0,0"},
        {"name": "light", "width": 3},
        {"color": "102,102,102"}
    ]
}"""


//
// メインのコード
//
val rootObj = JSONObject(jsonData)
val penListArray =  rootObj.getJSONArray("penList")

val keyList = listOf("name", "color", "width")
val defaultValueList = listOf("unknown", "0,0,0", 1)

val penParamsList = 0.until( penListArray.length() ).map { index->
    val penObject = penListArray[index] as JSONObject
    keyList.zip(defaultValueList).map { getParam(penObject, it.first, it.second) }
}

penParamsList.forEach {
    println( "- ${it[0]}, ${it[1]}, ${it[2]}" )
}

まとめ

これで多少は JSONデータの読み取りが楽になるかも。