IoTプロトコル

JSON

軽量なデータ記述形式。

概要

JSON(JavaScript Object Notation)は、人間が読み書きしやすく、かつ機械がパースしやすいテキストベースのデータ記述形式です。JavaScriptのオブジェクトリテラル記法を起源とし、2001年頃からDouglas Crockford氏が推進。2013年にECMA-404、2017年にIETFのRFC 8259として標準化されました。

JSONが定義するデータ型はシンプルで、オブジェクト({})、配列([])、文字列、数値、真偽値(true/false)、nullの6種類のみです。この単純さがJSONの普及の核心であり、仕様書が数ページに収まるほどコンパクトな設計となっています。

IoT・組み込み分野では以下の用途でJSONが広く使われています。

  • REST API・MQTTのメッセージペイロード
  • デバイス設定ファイル
  • センサーデータのシリアライズ
  • OTAマニフェスト
  • デバッグログのstructured logging

一方で、JSONはテキスト形式のため、バイナリ形式(CBOR/Protocol Buffers)に比べてペイロードサイズが大きく、パース処理がCPU・メモリを消費します。超制約デバイスでは代替フォーマットの検討も必要です。


歴史・背景

1999〜2000年頃、WebブラウザとサーバーのAjax通信でXMLが使われていましたが、XMLのパースは冗長で複雑でした。Douglas Crockfordは既存のJavaScript文法の一部を「データ交換フォーマット」として定義したものがJSONです(「発明」ではなく「発見」と本人は言っています)。

2005年のAjaxブームとともにJSONが広まり、2006年にjQuery 1.0がJSONを組み込みサポート、2009年にNode.jsの登場でサーバー・クライアント両方でJavaScript/JSONが使えるエコシステムが整いました。

IoT分野では2012〜2014年頃からAWS IoT・Google Cloud IoTがJSONをメインのペイロード形式として採用し、事実上のIoTデータ標準となりました。


技術仕様

基本文法

{
  "device_id": "sensor_001",
  "location": "factory1/line2",
  "timestamp": "2025-01-15T12:00:00.000Z",
  "measurements": {
    "temperature": 25.3,
    "humidity": 60.5,
    "pressure": 1013.25
  },
  "alerts": ["temperature_high"],
  "online": true,
  "battery_percent": null
}

データ型

説明
オブジェクト{"key": "value"}キーと値のペア
配列[1, 2, 3]順序付きリスト
文字列"hello"UTF-8、ダブルクォート必須
数値25.3, 1234, -5e3整数・浮動小数点
真偽値true, false小文字必須
nullnull値なし

JSONの制限

JSONは仕様として以下をサポートしていません。

  • コメント///* */ は無効)
  • 末尾カンマ[1, 2,] は無効)
  • バイナリデータ(Base64エンコードが必要)
  • 日付型(ISO 8601文字列が慣習)
  • 整数とfloatの区別(すべて「数値」)
  • 64ビット整数の完全表現(JavaScriptのNumber限界:2^53)

JSON Pointer(RFC 6901)

JSONデータの特定値をパスで指定する仕様。

JSONドキュメント:
{
  "sensors": [
    {"id": "s1", "temp": 25.3},
    {"id": "s2", "temp": 22.1}
  ]
}

JSONポインタ表記:
/sensors/0/temp     → 25.3
/sensors/1/id       → "s2"

JSON Schema

JSONデータの構造を定義・検証するスキーマ言語(非RFC、draft標準)。

{
  "$schema": "http://json-schema.org/draft-07/schema",
  "type": "object",
  "required": ["device_id", "temperature"],
  "properties": {
    "device_id": {
      "type": "string",
      "pattern": "^sensor_[0-9]{3}$"
    },
    "temperature": {
      "type": "number",
      "minimum": -40,
      "maximum": 125
    }
  }
}

動作原理

パースと生成の仕組み

JSONパーサーはテキストを字句解析(tokenize)して構文木(AST)を構築し、各言語のネイティブ型に変換します。

// C言語でのJSON生成(cJSON使用)
#include "cJSON.h"

char* create_sensor_json(float temp, float humidity, const char *device_id) {
    cJSON *root = cJSON_CreateObject();
    
    cJSON_AddStringToObject(root, "device_id", device_id);
    cJSON_AddNumberToObject(root, "temperature", temp);
    cJSON_AddNumberToObject(root, "humidity", humidity);
    cJSON_AddStringToObject(root, "timestamp", get_iso8601_time());
    
    char *json_str = cJSON_PrintUnformatted(root);  // コンパクト出力
    cJSON_Delete(root);
    
    return json_str;  // 呼び出し元でfree()が必要
}

// 使用例
char *json = create_sensor_json(25.3f, 60.5f, "sensor_001");
mqtt_publish("sensors/data", json, strlen(json));
free(json);
// C言語でのJSONパース(cJSON使用)
void parse_config_json(const char *json_str) {
    cJSON *root = cJSON_Parse(json_str);
    if (root == NULL) {
        ESP_LOGE(TAG, "JSONパースエラー");
        return;
    }
    
    cJSON *interval = cJSON_GetObjectItem(root, "report_interval_sec");
    if (cJSON_IsNumber(interval)) {
        config.report_interval = (uint32_t)interval->valuedouble;
    }
    
    cJSON *threshold = cJSON_GetObjectItem(root, "alert_threshold");
    if (cJSON_IsNumber(threshold)) {
        config.alert_threshold = (float)threshold->valuedouble;
    }
    
    cJSON_Delete(root);
}

ストリームパース(メモリ節約)

大きなJSONを一度にRAMに展開せず、ストリームとして処理します。

// Raspberry PiでのJJSONストリームパース(jsmn使用)
#include "jsmn.h"

// jsmn は動的メモリを使わない軽量トークナイザー
jsmn_parser parser;
jsmntok_t tokens[128];  // スタック割り当て

jsmn_init(&parser);
int num_tokens = jsmn_parse(&parser, json_str, strlen(json_str),
                              tokens, 128);

// トークンを順に処理
for (int i = 1; i < num_tokens; i++) {
    if (jsoneq(json_str, &tokens[i], "temperature") == 0) {
        float temp = atof(json_str + tokens[i+1].start);
        process_temperature(temp);
    }
}

用途・ユースケース

MQTTペイロード

# Python paho-mqtt でJSON送信
import json
import paho.mqtt.client as mqtt
from datetime import datetime, timezone

def publish_sensor_data(client, device_id, temp, humidity):
    payload = {
        "device_id": device_id,
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "measurements": {
            "temperature": round(temp, 2),
            "humidity": round(humidity, 2)
        },
        "metadata": {
            "firmware": "v1.2.0",
            "battery_percent": get_battery_level()
        }
    }
    
    json_str = json.dumps(payload, separators=(',', ':'))  # コンパクト
    client.publish(f"sensors/{device_id}/data", json_str, qos=1)

デバイス設定ファイル(NVS/SPIFFS)

{
  "wifi": {
    "ssid": "MyNetwork",
    "password": "secret123"
  },
  "mqtt": {
    "broker": "mqtt.example.com",
    "port": 8883,
    "use_tls": true,
    "client_id": "esp32_001"
  },
  "sensors": {
    "report_interval_sec": 60,
    "temperature_alert_max": 80.0,
    "enabled": ["temperature", "humidity"]
  }
}

REST APIレスポンス処理

# requests でJSON APIを叩く
import requests

def get_device_config(device_id):
    response = requests.get(
        f"https://api.example.com/devices/{device_id}/config",
        headers={"X-API-Key": API_KEY},
        timeout=5
    )
    response.raise_for_status()
    
    config = response.json()  # 自動デシリアライズ
    
    return {
        "interval": config["report_interval_sec"],
        "threshold": config["alert_threshold"],
        "enabled_sensors": config["sensors"]["enabled"]
    }

AWS IoT Rules でのJSONクエリ

AWS IoT CoreのルールエンジンはSQL-likeなクエリでJSONを処理します。

-- 温度が80度超のメッセージのみDynamoDBに保存
SELECT device_id, measurements.temperature, timestamp
FROM 'sensors/+/data'
WHERE measurements.temperature > 80

-- ネストしたデータの抽出
SELECT 
    device_id,
    measurements.temperature AS temp,
    metadata.firmware AS fw_version
FROM 'sensors/#'

実装・開発のポイント

組み込み向けJSONライブラリ比較

ライブラリ言語メモリ使用特徴
cJSONC動的割り当てArduino/ESP32で広く使用
jsmnCスタック割り当て超軽量、トークナイザーのみ
ArduinoJsonC++静的割り当て可Arduinoエコシステム向け、使いやすい
nlohmann/jsonC++STL使用ヘッダーオンリー、リッチなAPI
ujsonPythonMicroPython向け
jsonPythonCPython標準ライブラリ
// ArduinoJsonの使用例(ESP32/Arduino)
#include <ArduinoJson.h>

void sendData() {
    // 静的メモリ割り当て(ヒープ不使用)
    StaticJsonDocument<256> doc;
    
    doc["device_id"] = "esp32_001";
    doc["temperature"] = readTemperature();
    
    JsonObject sensors = doc.createNestedObject("sensors");
    sensors["temp_ok"] = true;
    sensors["humidity"] = readHumidity();
    
    char buffer[256];
    serializeJson(doc, buffer);
    
    mqttClient.publish("sensors/data", buffer);
}

// 受信データのパース
void onMessage(char* payload, unsigned int length) {
    StaticJsonDocument<128> doc;
    DeserializationError err = deserializeJson(doc, payload, length);
    
    if (err) {
        Serial.println("パースエラー: " + String(err.c_str()));
        return;
    }
    
    int interval = doc["report_interval_sec"] | 60;  // デフォルト60
    setReportInterval(interval);
}

バイナリデータのBase64エンコード

JSONはバイナリを直接扱えないため、Base64エンコードが慣習です。

import base64
import json

# バイナリデータ(センサーの生データ)をBase64でJSON化
raw_bytes = bytes([0x01, 0x02, 0xA3, 0xFF])
encoded = base64.b64encode(raw_bytes).decode('ascii')

payload = {
    "device_id": "sensor_001",
    "raw_data": encoded,  # "AQKj/w=="
    "encoding": "base64"
}

数値精度の注意点

JSONの数値はIEEE 754 double(64ビット浮動小数点)で表現されますが、整数値でも2^53を超えると精度を失います。

// 問題: Unix時間をミリ秒で表現する場合
{
  "timestamp_ms": 1736942400000  // 安全(2^53未満)
}

// 問題: デバイスIDが大きな整数の場合
{
  "device_serial": 9223372036854775807  // 危険:精度を失う可能性
  // 解決策: 文字列にする
  "device_serial": "9223372036854775807"
}

他技術との比較

項目JSONCBORProtocol BuffersXML
形式テキストバイナリバイナリテキスト
可読性××
サイズ小(50〜70%)最小
パース速度最速
スキーマ任意(JSON Schema)任意必須(.proto)任意(XSD)
普及度
組み込み適合×

JSONはその読みやすさとエコシステムの充実さで組み込みデバイスのデータ交換において広く使われますが、帯域や消費電力に厳しい環境ではCBOR/Protocol Buffersへの移行を検討します。REST APIMQTTWebSocketCoAPすべてのIoTプロトコルとの組み合わせが可能で、今後もデファクトスタンダードとして使われ続けます。

関連用語

参考リンク