JSONデータの走査 - Arduino用JSONライブラリの比較 - Arduino_JSON vs ArduinoJson
今回のお題
相変わらずチマチマと製作中の MP3 プレイヤーで、アーティスト別に SD カードに保存された全アルバムから、「JPOP」や「ROCK」などのカテゴリでお気に入りを選択し、プレイリストとして JSON データを SD カードに保存する機能を実装しようとしています。
例えば、JSON データはこんな感じです。
[
{
"name" : "JPOP",
"path" : {
"BoA" : [
"BEST OF SOUL"
],
"Superfly" : [
"Amazing"
]
}
},
{
"name" : "ROCK",
"path" : {
"サザンオールスターズ" : {
"2018" : [
"海のOh, Yeah!!"
],
"2014" : [
"KAMAKURA",
"SOUTHERN ALL STARS",
"バラッド3 ~the album of LOVE~"
]
},
"RADWIMPS" : [
"FOREVER DAZE",
"余命10年 〜Original Soundtrack〜"
],
"ONE OK ROCK" : [
"35xxxv",
"DETOX",
"人生 x 僕="
]
}
}
]"path" としているのは、ディレクトリ名を表す「キー」や「値」を / で繋げてアルバムへのパスとするためです。
ということで、SD カードに保存された JSON データを走査し、お気に入りのアルバムへのパスを抽出することが、今回のお題です。
試した JSON ライブラリの概要
Arduino IDE のライブラリマネージャで json を検索すると、様々なライブラリがヒットしますが、今回は次の2つをピックアップしました。
Arduino_JSON
Arduino Libraries に収められた Arduino 謹製のライブラリです。DaveGamble/cJSON のバージョン 1.7.14 を元に Arduino での使用を容易にする、Javascript ライクな(というか意図的に寄せた)ラッパーとして作られています。AVR 系用に独自に追加されたコードが含まれているので、8ビット系の CPU でもそれなりに動作すると思われます。
ただし、元の cJSON に存在した セキュリティ問題への対策 に追従できていません。現バージョン(0.2.0)はセキュリティが問題となるアプリケーションには使わないか、最新版 へのパッチを当てるのが吉です。
cJSON 1.7.19 へのパッチについて
https://github.com/embedded-kiddie/Arduino_JSON/tree/patch にパッチを当てたバージョンを作成しました。拡張された AVR 系のコードを含めて本家のテストコードを実機で検証したいのですが、何せ AVR 系は手持ちがないので PR できていません。ご参考まで。
最もよく使う JSONVar クラス は、「オブジェクト」、「配列」、「数値や文字列など」の3つのタイプで構成されていると考えれば理解し易いと思います。まずはよく使うメソッドです。
-
JSONVar parse(str)
str(const char*またはconst String&) から JSON オブジェクトを生成します。 -
String stringify(const JSONVar& value)
JSON オブジェクトを文字列化します。 -
String typeof(const JSONVar& value)
"object"、"array"、"boolean"、"number"、"string"、"null"など、JSON オブジェクトのタイプを返します。 -
int length(void)
オブジェクト{"key1" : value1, ...}や 配列[...]の要素数を返します。 -
JSONVar keys(void)
オブジェクト中のキーの集合を配列として返します。 -
size_t printTo(Print& output)
Printable クラス を継承するデバイスに JSON オブジェクトを文字列化して出力します。 -
オーバライドされた “
=” 演算子とキャスト演算子
JSONVarの中身は cJSON 構造体 で管理されており、JSON オブジェクトの「キー」は文字列、「値」は文字列か数値(整数または浮動小数点)を取り得ます。特に キャスト演算子 は、「値」をchar*やint、doubleなど適切な型に変換するため使います。
また次のメソッドはβ版のためか、機能する条件がかなり限られているようです。
-
bool hasOwnProperty(key)
key(const char*またはconst String&) が含まれているか否かを返します。 -
bool hasPropertyEqual(key, value)
key(const char*またはconst String&) にvalue(const char*、const String&またはconst JSONVar&) と一致する値が設定されているかを返します。 -
JSONVar filter(key, value)
key(const char*またはconst String&) とvalue(const char*、const String&またはconst JSONVar&) が含まれる JSON データを部分的に抽出します。
ドキュメントが存在せず、また 例題 が全メソッドを網羅していないので不完全にはなりますが、上記をテストするスケッチを添付しておきます。
Arduino_JSON の各メソッドをテストするスケッチ
#include <Arduino_JSON.h>
#include <SD.h>
const char *input = R"(
{
"code": "200",
"message": 0,
"list": [
{
"main": {
"temp": 3.23,
"pressure": 1014,
"humidity": 58
},
"weather": {
"main": "Clear",
"description": "clear sky"
},
"dt": "2020-02-12 09:00:00"
},
{
"main": {
"temp": 6.09,
"pressure": 1015,
"humidity": 48
},
"weather": {
"main": "Clear",
"description": "clear sky"
},
"dt": "2020-02-12 12:00:00"
}
],
"city": {
"name": "London",
"coord": {
"lat": 51.5085,
"lon": -0.1257
}
}
}
)";
void Test_hasOwnProperty(JSONVar &obj) {
Serial.println("===== Test_hasOwnProperty =====");
Serial.println(obj.hasOwnProperty("code")); // 1
Serial.println(obj.hasOwnProperty("main")); // 0
Serial.println(obj["list"][0].hasOwnProperty("main")); // 1
}
void Test_hasPropertyEqual(JSONVar &obj) {
JSONVar var = "London";
Serial.println("===== Test_hasPropertyEqual =====");
Serial.println(obj.hasPropertyEqual("code", "200")); // 1
Serial.println(obj["city"].hasPropertyEqual("name", "London")); // 1
Serial.println(obj["city"].hasPropertyEqual("name", var)); // 1
}
void Test_filter(JSONVar &obj) {
JSONVar var;
var["name"] = "London";
Serial.println("===== Test_filter =====");
Serial.println(JSON.stringify(obj.filter("code", "200"))); // OK
Serial.println(JSON.stringify(obj.filter("city", var))); // null
Serial.println(JSON.stringify(obj["city"].filter("name", "London"))); // OK
}
void Test_printTo(JSONVar &obj) {
Serial.println("===== Test_printTo =====");
if (!SD.begin()) {
Serial.println("SD initialization failed.");
return;
}
File myFile = SD.open("/test.txt", FILE_WRITE);
if (!myFile) {
Serial.println("Error opening test.txt");
return;
}
size_t len = obj.printTo(myFile);
myFile.close();
char* buf = (char*)malloc(len + 1);
if (!buf) {
Serial.println("malloc failed.");
return;
}
myFile = SD.open("/test.txt", FILE_READ);
len = myFile.read((uint8_t*)buf, len);
myFile.close();
buf[len] = '\0';
JSONVar tmp = JSON.parse(buf);
Serial.println(JSON.stringify(tmp));
free((void*)buf);
}
void setup() {
Serial.begin(115200);
while (millis() < 1500);
Serial.println("\nStart.");
JSONVar myObject = JSON.parse(input);
String type = JSON.typeof(myObject);
if (type == "undefined") {
Serial.println("Parsing input failed.");
return;
}
Test_hasOwnProperty (myObject);
Test_hasPropertyEqual (myObject);
Test_filter (myObject);
Test_printTo (myObject);
Serial.println("Done.");
}
void loop() {}ArduinoJson
Arduino 用 JSON ライブラリでは最もよく使われていると思われます。
まずセキュリティについてですが、cve.org で検索すると、バージョン 4.5 以前の CVE-2015-4590 がヒットしますが、現在は 6 または 7 が主要なバージョンなので、現時点で心配する必要はなさそうです 😉
そのバージョンについては、6 は省メモリに特化、7 は使い勝手を改善した版に位置付けられています。特に 7 は 6 に比べて 相当にデカくなった ので、8 ビット系のマイコンでは 6 がお薦めとのことです。
本記事では 7 を取り上げますが、ArduinoでJsonを生成、解析する に 6 の詳しい解説があり、7 を使う場合にも一読すると概要が掴み易いと思います。また API や例題のドキュメント が完備しているためココでの解説は割愛しますが、以下は知っておくと良いでしょう 🤟
-
大元のクラスである JsonDocument と、その構成要素として3つのクラス JsonArray、JsonObject、JsonVariant がある。
-
上記3クラスには、メモリの消費量を半分程度に抑えられる read-only の派生クラス JsonArrayConst、JsonObjectConst、JsonVariantConst がある。
-
JsonArrayとJsonObjectにはイテレータbegin() / end()が、またJsonObjectではJsonPairが使える。 -
JsonDocumentとJsonVariantには、型を調べるテンプレートis<T>()と、適切な型にキャストするためのas<T>()がある。
ArduinoJson Assistant
ターゲットボードと JSON データを入力すると、予想されるメモリ量や各要素にアクセスする C++ コードを生成してくれるツールです。filter を試したり、as<T>() の使い方が分かるので、一度試してみると良いと思います 👍
JSON データの走査
今回のお題では JSON データを頭からお尻まで走査する必要があるため、前章で紹介した2つのライブラリでそのようなスケッチを作成しました。本来「キー」や「値」を処理するところはシリアルモニタへの出力としています。
本来は処理時間も計測すべきですが…、ターゲットはバカっ速な ESP32 なので問題とならないと踏んで省略です 😅
また再帰呼び出しで実装しているため、スタックの消費量は多めです。
Arduino_JSON 版
再帰的関数の引数を参照型にしたかったのですが、上手くいかず、コピーで引き回してます。そのため ArduinoJson 版 に比べ、ヒープメモリの消費量が2倍ほどになっちゃいました 😅
#include <Arduino_JSON.h>
// JSON サンプルデータ
const char *input = R"(
// ここに JSON データを貼り付けてください
)";
// 整形用インデントの出力
inline void Indent(int depth) {
while (depth-- > 0) {
Serial.print(" ");
}
}
// JSON データの走査
void Traverse(JSONVar obj, int depth = 0) {
String type = JSON.typeof(obj);
if (type == "array") {
for (int i = 0; i < obj.length(); i++) {
Traverse(obj[i], depth); // 配列の各要素を走査
}
}
else if (type == "object") {
for (int i = 0; i < obj.keys().length(); i++) {
Indent(depth);
obj.keys()[i].printTo(Serial); // 何か処理する代わりにシリアルモニタに出力
Serial.println();
Traverse(obj[obj.keys()[i]], depth + 1); // 次のオブジェクトを走査
}
}
else if (type != "undefined") {
Indent(depth);
obj.printTo(Serial); // 何か処理する代わりにシリアルモニタに出力
Serial.println();
}
}
void setup() {
Serial.begin(115200);
while (millis() < 1500);
JSONVar obj = JSON.parse(input);
String type = JSON.typeof(obj);
if (type == "undefined") {
Serial.println("Parsing input failed!");
return;
}
Traverse(obj); // JSON データの走査
Serial.println(JSON.stringify(obj)); // ライブラリによるシリアライズ
}
void loop() {}ArduinoJson 版
再帰呼び出しする関数への引数を JsonVariant ではなく JsonVariantConst とすることで、ヒープメモリの消費量が Arduino_JSON 版 の半分程度になっています。
#include <ArduinoJson.h>
// JSON サンプルデータ
const char *input = R"(
// ここに JSON データを貼り付けてください
)";
// JSON オブジェクトの型
typedef enum {
JSON_ARRAY,
JSON_OBJECT,
JSON_VARIANT,
} JsonType_t;
// JSON オブジェクトの型判定
JsonType_t TypeOf(JsonVariantConst doc) {
if (doc.is<JsonArrayConst>()) {
return JSON_ARRAY;
}
else if (doc.is<JsonObjectConst>()) {
return JSON_OBJECT;
}
return JSON_VARIANT;
}
// 整形用インデントの出力
inline void Indent(int n) {
while (n-- > 0) {
Serial.print(" ");
}
}
// JSON データの走査
void Traverse(JsonVariantConst doc, int depth = 0) {
switch (TypeOf(doc)) {
case JSON_ARRAY:
for (JsonVariantConst value : doc.as<JsonArrayConst>()) {
Traverse(value, depth); // 配列の各要素を走査
}
break;
case JSON_OBJECT:
for (JsonPairConst kv : doc.as<JsonObjectConst>()) {
Indent(depth); // 何か処理する代わりにシリアルモニタに出力
Serial.println(kv.key().c_str()); // 「キー」は JsonString 型
Traverse(kv.value(), depth + 1); // 次のオブジェクトを走査
}
break;
case JSON_VARIANT:
default:
Indent(depth); // 何か処理する代わりにシリアルモニタに出力
serializeJson(doc, Serial); // 「値」は JsonVariant::as<T>() で取得
Serial.println();
break;
}
}
void setup() {
// Initialize serial port
Serial.begin(115200);
while (millis() < 1500);
JsonDocument doc;
DeserializationError error = deserializeJson(doc, input);
if (error) {
Serial.print("deserializeJson() failed: ");
Serial.println(error.c_str());
return;
}
Traverse(doc); // JSON データの走査
serializeJsonPretty(doc, Serial); // ライブラリによるシリアライズ
}
void loop() {}今回の比較結果 まとめ
Arduino 本家のライブラリには、そのシンプルな API 故に期待を寄せていたのですが、β版に止まっていることもあり、ArduinoJson に軍配が挙がると思います。Arduino 本家にも頑張って欲しいところですが、人気の程からしても順当というところでしょう。
おまけ
- JSON 関連
-
JSON Editor Online
元は Google Chrome の拡張機能だったようですが、現在はオンラインツールとなっています。JSON データの誤りを修正する手助けをしてくれたり、Javascript の filter を試せたりと、とっても便利なツールです。 -
C++ でも PHP のヒアドキュメント的なことができる (raw string literal)
C++ でも出来るって事、知りませんでした…
-
- セキュリティ関連
-
JVNDB-2025-013381 Dave Gamble の cJSON における境界外読み取りに関する脆弱性(CVE-2025-57052)
2025年9月に公表された cJSON の深刻な脆弱性です。幸い Arduino_JSON には含まれていないソースコードが対象です。 -
cJSON の CVE 関連プルリク
全てではありませんが、Arduino_JSON にも関連する脆弱性を修正するプルリクのリストです。
-