JSONは柔軟にデータを表現できて便利だが、 org.json.JSONObject を使ってプログラムでそれを読むときに、 プログラムからみて想定外のデータ構造(たとえば、キー name に対する値があるはずだが、無い場合もある、のような場合)に 例外が発生してプログラムがそこでストップすることになる。 それは困るので、例外をキャッチして、デフォルト値を代わりに適用するなどというコードを書くわけだが、 そのようなコードは非常に読みづらい。これをなんとかしてスマートに書く方法はないのか? ということであれこれ試行錯誤した結果をここに書き残す。
使用言語は kotlin で kotlinc-jvm version 1.4.21 を使用します。
kotlinc -cp hoge.jar すれば hoge ライブラリを使えるらしい。 ならば org.json の jar をこちら github の JSON-java から ダウンロードして使おう。
main.kts でスクリプトを書いて:
kotlinc -cp json-20201115.jar -script main.kts
のようにすれば、org.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
意図する結果が得られた。
ただ、実際のケースでは、パラメータが大量にあったり、そのメンテナンスで、パラメータ数が増減すると面倒なことになる。
結局、該当のパラメータの値を取得するときに、例外を投げないで、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 = ...
あとのことは、関心がない。
それでは、このように記述できるようにコードを修正しよう。
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データの読み取りが楽になるかも。