ネストしたJSONから値を取り出すには
パース済みの JSON から深い階層の値を取り出す基本形を各言語で示す。
途中のキーが欠けていても落とさず既定値へ倒す書き方までを扱う。
公開:
各言語見出しの横のバッジは検証状態を表す。実行確認済みはコードを実際に実行して確認したもの、静的確認は構文と公式 API ドキュメントで確認したものである。
Python 実行確認済み
import json
data = json.loads('{"server": {"http": {"port": 8080}}}')
port = data.get("server", {}).get("http", {}).get("port", 3000)
host = data.get("server", {}).get("http", {}).get("host", "localhost")
print(port, host)dict.get の第 2 引数に空の dict を渡して繋ぐと、途中のキーが無くても KeyError にならずに最後の既定値まで到達する。
data["server"]["http"] のように添字で辿ると、1 段でも欠けた時点で例外になる。
うまくいかない時: Python の dict アクセスで KeyError が解消できない / Python で「'NoneType' object has no attribute」が解消できない
JavaScript 実行確認済み
const data = JSON.parse('{"server": {"http": {"port": 8080}}}');
const port = data.server?.http?.port ?? 3000;
const host = data.server?.http?.host ?? "localhost";
console.log(port, host);オプショナルチェーン ?. は途中が undefined や null になった時点で評価を打ち切り、undefined を返す。
既定値は ?? で与える。
|| だと 0 や空文字まで既定値に置き換わってしまうため、数値や文字列を読むときは ?? を使う。
TypeScript 実行確認済み
type Config = { server?: { http?: { port?: number; host?: string } } };
const data = JSON.parse('{"server":{"http":{"port":8080}}}') as Config;
const port = data.server?.http?.port ?? 3000;
const host = data.server?.http?.host ?? "localhost";
console.log(port, host);各階層を省略可能(?)にした型を当てると、?. を省いた書き方をコンパイラが弾いてくれる。
ただし JSON.parse の戻り値は any なので、as で付けた型は実際の中身を検証しない。
外部由来の JSON なら zod などで実行時に検証する方が安全である。
うまくいかない時: TypeScript で「Object is possibly 'null'」が解消できない / TypeScript で「Property does not exist on type」が解消できない
Go 静的確認
package main
import (
"encoding/json"
"fmt"
)
type Config struct {
Server struct {
HTTP struct {
Port int `json:"port"`
Host string `json:"host"`
} `json:"http"`
} `json:"server"`
}
func main() {
c := Config{}
c.Server.HTTP.Port = 3000
c.Server.HTTP.Host = "localhost"
if err := json.Unmarshal([]byte(`{"server":{"http":{"port":8080}}}`), &c); err != nil {
panic(err)
}
fmt.Println(c.Server.HTTP.Port, c.Server.HTTP.Host)
}構造体へ流し込む前に既定値を入れておく。
json.Unmarshal は JSON に present なフィールドだけを書き換え、欠けているフィールドには触れないため、これで既定値が残る。
入れ子は構造体を入れ子にするか、ポインタにして nil で欠落を判定する。
Rust 静的確認
use serde_json::Value;
fn main() -> Result<(), serde_json::Error> {
let data: Value = serde_json::from_str(r#"{"server":{"http":{"port":8080}}}"#)?;
let port = data.pointer("/server/http/port").and_then(Value::as_u64).unwrap_or(3000);
let host = data
.pointer("/server/http/host")
.and_then(Value::as_str)
.unwrap_or("localhost");
println!("{port} {host}");
Ok(())
}serde_json の pointer は JSON Pointer(RFC 6901)形式のパスで一気に辿れる。
戻り値は Option なので、as_u64 や as_str で型を確かめてから unwrap_or で既定値に倒す。
構造が決まっているなら Value ではなく Deserialize 付き構造体の方が安全である。
Java 静的確認
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Main {
public static void main(String[] args) throws Exception {
JsonNode root = new ObjectMapper()
.readTree("{\"server\":{\"http\":{\"port\":8080}}}");
int port = root.path("server").path("http").path("port").asInt(3000);
String host = root.path("server").path("http").path("host").asText("localhost");
System.out.println(port + " " + host);
}
}Jackson の path は、キーが無いときに null ではなく MissingNode を返すため、途中で切れても連鎖を書き続けられる。
asInt / asText に既定値を渡せばそのまま倒せる。
get だと null が返り、次の呼び出しで NullPointerException になる。
C# 実行確認済み
using System.Text.Json.Nodes;
var root = JsonNode.Parse("""{"server":{"http":{"port":8080}}}""")!;
var port = (int?)root["server"]?["http"]?["port"] ?? 3000;
var host = (string?)root["server"]?["http"]?["host"] ?? "localhost";
Console.WriteLine($"{port} {host}");JsonNode のインデクサはキーが無いと null を返すので、?. で繋いで最後に ?? で既定値を与える。
JsonNode から int? や string? への明示的キャストが用意されており、型が合わなければ実行時に例外になる。
つまずき
設定ファイルや API レスポンスは、階層の途中がまるごと無いことがある。
添字や get() をそのまま繋ぐと、値が無い場所ではなく「途中のキーが無い」時点で落ちるため、原因が分かりにくい例外になる。
Python の KeyError、JavaScript の "Cannot read properties of undefined"、Java の NullPointerException はいずれもこれである。
各言語が用意している「欠けていても続行できる」書き方を最初から使うとよい。
既定値は最後にまとめて与える
階層ごとに既定値を散らすと、どの段が欠けたときにどうなるのかを追えなくなる。
途中は「欠けていたら打ち切る」だけにして、既定値は取り出しの最後に 1 回だけ与える形に揃えると、読む側が結果を 1 行で判断できる。
JavaScript の ?? や Rust の unwrap_or はこの形を素直に書ける。
取り出せたことと型が正しいことは別
動的型の言語では、値が取り出せてもそれが数値とは限らない。
port に文字列 "8080" が入っていても取り出し自体は成功するので、後段の計算で初めて壊れる。
構造が決まっているなら、値を 1 つずつ辿るのではなく、型付きの構造体やスキーマ検証へ一度に流し込む方が事故が少ない。