JSON の解析
Thing-IF SDK では、JSON の解析を行うための独自ライブラリ「kii_json」を使用できます。コマンドを受け取った後、アクションのパラメータを解析する際などに利用可能です。
このライブラリを使うと、JSON 文字列から、指定されたパスに書かれている値を、指定されたデータ型(文字列や数値)で取得できます。
たとえば、以下のような JSON から color
の rgb
にある 2 つ目(配列の [ 1 ] 要素)の値を取得したい場合、「パス /color/rgb/[1]
の値を、int として取得」という条件をライブラリに指定することで、int 型の 128 を戻り値として得ることができます。
{
"color": {
"alpha": 255,
"rgb": [
0,
128,
255
]
}
}
kii_json ライブラリは様々な機能がありますが、ここでは代表的な使い方のみを示します。機能の詳細は Kii JSON Documentation や ソースコード をご覧ください。
特定フィールドの取得
kii_json ライブラリを使って JSON 文字列から特定フィールドの値を取得する例を挙げます。
まず、{"key1" : 123}
という JSON から、フィールド key1
の値(123)を int 型で取得する例を挙げます。
なお、ここでは下記 トークンの領域の初期化 に示す KII_JSON_FIXED_TOKEN_NUM
マクロが設定されていると仮定しています(リファレンス実装では設定済みです)。
const char json_string[] = "{\"key1\" : 123}";
kii_json_t kii_json;
kii_json_field_t fields[2];
kii_json_parse_result_t result;
/* Initialize the kii_json_t struct. */
memset(&kii_json, 0x00, sizeof(kii_json_t));
/* Initialize fields. */
memset(fields, 0x00, sizeof(fields));
fields[0].path = "/key1";
fields[0].type = KII_JSON_FIELD_TYPE_INTEGER;
fields[1].path = NULL;
/* Parse the JSON string. */
result = kii_json_read_object(
&kii_json,
json_string,
sizeof(json_string) / sizeof(json_string[0]),
fields);
/* Check if parsing the string failed. */
if (result != KII_JSON_PARSE_SUCCESS) {
return;
}
/* Check if parsing the field failed. */
if (fields[0].result != KII_JSON_FIELD_PARSE_SUCCESS) {
return;
}
/* Write the value of the first field to stdout. Expected output is "value=123". */
printf("value=%d\n", fields[0].field_copy.int_value);
ここでは以下の処理を行っています。
kii_json_t
構造体を初期化します。この構造体はライブラリ内部で初期化されるため、コード例のようにゼロクリアしておくだけで問題ありません。- フィールドの解析方法を
kii_json_field_t
配列に格納します(解析方法の指定の詳細は フィールドの解析情報 をご覧ください)。今回の例では、配列全体を一旦をゼロクリアした後、1 件目の要素に「JSON パス/key1
の値を int 型で取得」という解析方法を保存しています。また、2 件目の要素で NULL ターミネートしています。予期しないバグの原因になるため、始めに配列全体のゼロクリアを必ず実行してください。 kii_json_read_object
関数を実行して JSON の解析を行います。- 解析結果は、
kii_json_field_t
配列の各要素のfield_copy
メンバーより取得できます。
関数の実行が成功すると、関数は KII_JSON_PARSE_SUCCESS
を返します。また kii_json_field_t
配列のフィールドの result
メンバーに KII_JSON_FIELD_PARSE_SUCCESS
が記録されます。
複数のフィールドの同時読み込み
JSON に複数のフィールドが記述されている場合、解析対象のフィールドを同時に指定することで、複数フィールドの値を同時に取得できます。
以下に例を示します。この例は、以下の JSON から、フィールド key1
の値("abc")と key2
フィールド配下の key2-2
に格納されている配列の 2 つ目の要素(true
)を取得しています。
{
"key1": "abc",
"key2": {
"key2-1": 123,
"key2-2": [
false,
true,
false
]
}
}
実際にフィールド値を取得するコードは以下のとおりです。
const char json_string[] =
"{"
"\"key1\" : \"abc\","
"\"key2\" : {\"key2-1\" : 123, \"key2-2\" : [false, true, false]}"
"}";
char buf[256];
kii_json_t kii_json;
kii_json_field_t fields[3];
kii_json_parse_result_t result;
/* Initialize the kii_json_t struct. */
memset(&kii_json, 0x00, sizeof(kii_json_t));
/* Initialize fields. */
memset(fields, 0x00, sizeof(fields));
fields[0].path = "/key1";
fields[0].type = KII_JSON_FIELD_TYPE_STRING;
fields[0].field_copy.string = buf;
fields[0].field_copy_buff_size = sizeof(buf) / sizeof(buf[0]);
fields[1].path = "/key2/key2-2/[1]";
fields[1].type = KII_JSON_FIELD_TYPE_BOOLEAN;
fields[2].path = NULL;
/* Parse the JSON string. */
result = kii_json_read_object(
&kii_json,
json_string,
sizeof(json_string) / sizeof(json_string[0]),
fields);
/* Check if parsing the string failed. */
if (result != KII_JSON_PARSE_SUCCESS) {
return;
}
/* Check if parsing either field failed. */
if (fields[0].result != KII_JSON_FIELD_PARSE_SUCCESS ||
fields[1].result != KII_JSON_FIELD_PARSE_SUCCESS) {
return;
}
/* Write the value of the fields to stdout. Expected output is "value1=abc, value2=1". */
printf("value1=%s, value2=%d\n",
fields[0].field_copy.int_value,
fields[1].field_copy.boolean_value);
ここでは以下の処理を行っています。
- 初めの例と同様に、
kii_json_t
構造体を初期化します。 - フィールドの解析方法を
kii_json_field_t
配列に格納します。ここでは以下の設定を行っています。/key1
を文字列として取得します。バッファbuf
を入力パラメータfield_copy.string
で指定して API に入力すると、API は解析結果の文字列をその領域に書き出します。/key2/key2-2/[1]
は、key2
以下、key2-2
の配列から、配列要素 [ 1 ] を取り出すものです(ここではtrue
が取得される想定です)。型は boolean を指定しています。
kii_json_read_object
関数を実行して JSON の解析を行います。- 解析結果は、
kii_json_field_t
のfield_copy
メンバーにそれぞれ取得されます。ここでは、2 件取得したため、それぞれの取得が成功したことを確認後、結果を出力しています。
もし、一部のフィールドの取得に失敗した場合は、関数は KII_JSON_PARSE_PARTIAL_SUCCESS
を返し、失敗したフィールドの result
メンバーがエラーを表します。
フィールドの解析情報
JSON のフィールドを解析する際には、kii_json_field_t
構造体の配列に解析方法を記述します。ここでは、解析方法の詳細を説明します。
まず、kii_json_field_t
は配列として宣言します。配列は実際に取得したいフィールド数より 1 つ多い要素数を確保し、最後に path
メンバーを NULL に指定して終端の目印とします。
設定するフィールドは以下のとおりです。設定前に memset
関数によって全要素をゼロクリアしておく必要があります。ゼロクリアを忘れると、予期しないバグが発生する原因になります。
メンバー | 入出力 | 説明 |
---|---|---|
path | 入力 | 取得したいフィールドのパスを JSON の階層に従って指定します。パスのセパレータは / で、/ から始まる必要があります。[ ] によって配列の要素を指定できます。たとえば {"light":{"color":[0,128,255]}} で 0 を取得するには /light/color/[0] を指定します。フィールド名に [ 、] 、/ 、\ 含む場合は、 \ でエスケープして \[ のように指定します。 |
result | 出力 | フィールドごとに解析結果のステータスが返されます。成功した場合は KII_JSON_FIELD_PARSE_SUCCESS です。その他の値は リファレンスガイド をご覧ください。 |
type | 入力, 出力 |
フィールドの型を指定します。入力時は期待されるフィールドの型を指定します。出力時は実際に解析された型を格納して返します。型の詳細は下記の データ型 をご覧ください。 |
start | 出力 | 解析できた JSON 文字列の開始位置を返します。指定した JSON 文字列の先頭バイト位置を 0、次のバイトを 1 …として返します。文字列の場合は " の次の位置を差します。文字以外の場合にも設定されます。 |
end | 出力 | 解析できた JSON 文字列の終了位置の次のバイトを返します。たとえば、{"abc":"XYZ"} のキー: abc が見つかった場合、end は c の次の位置: " を表す 5 となります。文字以外の場合にも設定されます。 |
field_copy | 入力, 出力 |
内部は union で以下のメンバーを持っています。
|
filed_copy_ buffer_size |
入力 | field_copy.string で、入力パラメータとして結果格納用のバッファを指定する場合、セットしたバッファサイズをバイト単位で指定します。下記の 文字列の扱い をご覧ください。 |
データ型
kii_json ライブラリで使用できるデータ型は以下の表のとおりです。
入力時に type
メンバーでこれらの値を指定すると、path
メンバーで指定されたフィールドが、type
の型であることを期待します。一致している場合はその値を取得できます。不一致の場合は result
メンバーで KII_JSON_FIELD_PARSE_TYPE_UNMATCHED
エラーを返します。
なお、KII_JSON_FIELD_TYPE_INTEGER
や KII_JSON_FIELD_TYPE_OBJECT
等の実際のデータ型を期待しているフィールドが、JSON の null だった場合、データ型の不一致エラーとなります。
type
メンバーに KII_JSON_FIELD_TYPE_ANY
を指定した場合、JSON での実際の値に合わせたデータ型が返ります。
データ型 | 説明 |
---|---|
KII_JSON_FIELD_TYPE_ANY | 入力時には任意の型を表すデータ型として指定できます。出力時には、実際にマッチした JSON のフィールドの型がセットされて返ります。Object や配列にマッチさせることも可能です。 |
KII_JSON_FIELD_TYPE_INTEGER | 入力時、出力時とも、int 型のデータを表します。field_copy.int_value として値を取得します。 |
KII_JSON_FIELD_TYPE_LONG | 入力時、出力時とも、long 型のデータを表します。field_copy.long_value として値を取得します。 |
KII_JSON_FIELD_TYPE_DOUBLE | 入力時、出力時とも、double 型のデータを表します。field_copy.double_value として値を取得します。 |
KII_JSON_FIELD_TYPE_BOOLEAN | 入力時、出力時とも、boolean 型のデータを表します。field_copy.boolean_value として値を取得します。 |
KII_JSON_FIELD_TYPE_NULL | 入力時に指定すると、そのフィールドが JSON の null であることを期待します。入力で KII_JSON_FIELD_TYPE_ANY を指定して JSON 文字列の null にマッチした場合、出力値としてこの値を返します。 |
KII_JSON_FIELD_TYPE_STRING | 入力時、出力時とも、文字列型のデータを表します。field_copy.string_value として値を取得します。下記の 文字列の扱い もご覧ください。 |
KII_JSON_FIELD_TYPE_OBJECT | 入力時、出力時とも、Object 型のデータを表します。入力時には指定されたフィールドが Object であることを期待します。出力時には Object として取得できたことを表します。field_copy.string_value として値を取得でき、{"key2-1":123,"key2-2":[false,true,false]} のような文字列となります。下記の 文字列の扱い もご覧ください。 |
KII_JSON_FIELD_TYPE_ARRAY | 入力時、出力時とも、配列のデータを表します。入力時には指定されたフィールドが配列であることを期待します。出力時には配列として取得できたことを表します。field_copy.string_value として値を取得でき、[false,true,false] のような文字列となります。下記の 文字列の扱い もご覧ください。 |
文字列の扱い
kii_json ライブラリで、フィールドの解析結果を文字列として出力する場合、その文字列用のバッファは次の 2 通りの方法で扱うことができます。これは、文字列、Object、配列の解析結果を出力する場合に共通する仕様です。
領域を入力パラメータとして用意する
文字列の格納に必要な領域を呼び出し元で用意する方法です。API を呼び出すと、解析した結果をその領域にコピーします。コピーした文字列は NULL ターミネートされています。
この方法を使用するには、配列中の各要素の
field_copy.string
メンバーに、確保したバッファへのポインタを設定します。さらに、file_copy_buffer_size
メンバーに確保したバッファサイズをバイト単位で格納します。バッファサイズには終端文字も含みます。元の JSON 文字列の領域を使用する
API の実行結果の文字列を、元の JSON 文字列中のポインタとして返す方法です。元の JSON 文字列の領域を共有しているため、取得された値の文字列は NULL ターミネートされていません。フィールドの
start
メンバーとend
メンバーから結果の文字列値のバイト数が決まります。この方法を使用するには、配列中の各要素の
field_copy.string
メンバーを NULL に設定した状態で解析 API を呼び出します。
トークン用領域の確保
ライブラリ内で JSON 文字列を解析する際、JSON のトークンの解析に必要な領域を呼び出し元で確保する必要があります。
必要なバッファサイズは、JSON の複雑さに依存します。JSON のトークン 1 個につき、kii_json_token_t
の配列要素 1 個分が必要です。ここでのトークンとは JSON のキーとその値(単純な値、Object、配列全体、配列要素)を表します。
たとえば、{"key1":"value", "key2":{"key3":2}}
のような JSON には、以下の 7 個のトークンが存在します。
- JSON 全体
key1
value
key2
{"key3":2}
key3
2
解析用の領域を確保する方法には、次の 2 通りの方法があります。いずれかを選択してください。
コンパイラオプションであらかじめ設定する
これは、デフォルトの方法です。
マクロ KII_JSON_FIXED_TOKEN_NUM
によって、トークンの解析のための構造体の数をスタック上に確保します。マクロにはトークン数を指定します。スタックに余裕があるプラットフォームではこの方法を使うと簡単に必要なリソースを確保できます。
デフォルトの MAKEFILE では 128 が設定されているため、sizeof(kii_json_token_t) * 128
バイトの領域をスタック上に確保します。このトークン数は SDK と Thing Interaction Framework との間のやりとりを行うためには十分な値です。もし、トレイト定義で非常に複雑なパラメータのやりとりを指定する場合は、値の変更を検討してください。
このマクロを定義すると、Thing-IF SDK の 初期化 の際に init_kii_thing_if_with_onboarded_thing
または init_kii_thing_if
関数に渡すリソース確保のコールバックを NULL に設定できます。
設定値はプログラム全体で 1 つしか使用できませんが、スタック上に領域を確保するため、複数のタスクやスレッドから JSON 解析の API を同時に実行することができます。
コールバック関数を使って必要な領域だけを確保する
スタック上に大きな領域を取れない場合、コールバック関数を使って必要なバッファをヒープ上に確保できます。
マクロ KII_JSON_FIXED_TOKEN_NUM
が定義されていない場合、コールバック関数によって JSON 解析用のバッファを確保します。
コールバック関数の実装例を以下に示します。
static int resource_cb(kii_json_resource_t* resource, size_t required_size)
{
kii_json_token_t *tokens;
/* Reallocate the buffer for a JSON string. */
tokens = (kii_json_token_t*)realloc(resource->tokens,
sizeof(kii_json_token_t) * required_size);
/* If reallocation fails */
if (tokens == NULL) {
return 0;
}
/* Set the reallocated pointer and the number of tokens in the kii_json_resource_t struct. */
resource->tokens = tokens;
resource->tokens_num = required_size;
return 1;
}
kii_json ライブラリで、事前に必要なトークンの数を見積もってこのコールバック関数を呼び出します。required_size
パラメータとして渡される必要なトークンの個数を kii_json_resource_t
構造体の tokens
メンバーのサイズと乗じて、領域を必要なサイズに拡張します。最終的に、割り当て直したバッファへのポインタとトークンの個数を、kii_json_resource_t
構造体に格納します。
関数の戻り値では、確保できた場合は 1 を、できなかった場合は 0 を返します。0 を返した場合、JSON の解析そのものがエラーとなります。
なお、この実装を行った場合、JSON の解析 API の呼び出し完了後、その API に渡した kii_json_resource_t
構造体の、tokens
メンバーの領域を呼び出し元で解放する必要があります。
/* Parse a JSON string. */
result = kii_json_read_object(
&kii_json,
json_string,
sizeof(json_string) / sizeof(json_string[0]),
fields);
/* Free the buffer. */
free(kii_json.resource->tokens);