JSON の解析
Thing-IF SDK v2では、JSON の解析を行うための独自ライブラリ「jkii」を使用できます。コマンドを受け取った後、アクションのパラメータを解析する際などに利用可能です。
このライブラリを使うと、JSON 文字列から、指定されたパスに書かれている値を、指定されたデータ型(文字列や数値)で取得できます。
たとえば、以下のような JSON から color
の rgb
にある 2 つ目(配列の [ 1 ] 要素)の値を取得したい場合、「パス /color/rgb/[1]
の値を、int として取得」という条件をライブラリに指定することで、int 型の 128 を戻り値として得ることができます。
{
"color": {
"alpha": 255,
"rgb": [
0,
128,
255
]
}
}
jkii ライブラリは様々な機能がありますが、ここでは代表的な使い方のみを示します。機能の詳細は jkii Documentation や ソースコード をご覧ください。
特定フィールドの取得
jkii ライブラリを使って JSON 文字列から特定フィールドの値を取得する例を挙げます。
まず、{"key1" : 123}
という JSON から、フィールド key1
の値(123)を int 型で取得する例を挙げます。
const char json_string[] = "{\"key1\" : 123}";
/* jkii resource */
jkii_token_t tokens[8];
jkii_resource_t resource = {tokens, 8};
/* Initialize fields. */
jkii_field_t fields[2];
memset(fields, 0x00, sizeof(fields));
fields[0].path = "/key1";
fields[0].type = JKII_FIELD_TYPE_INTEGER;
fields[1].path = NULL;
/* Parse the JSON string. */
jkii_parse_err_t result = jkii_parse(
json_string,
sizeof(json_string) / sizeof(json_string[0]),
&fields,
&resource);
/* Check if parsing the string failed. */
if (result != JKII_ERR_OK) {
return;
}
/* Check if parsing the field failed. */
if (fields[0].result != JKII_FIELD_ERR_OK ) {
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);
ここでは以下の処理を行っています。
jkii_token_t
の配列を用意しjkii_resource_t
に配列と要素数を設定して解析用リソースを用意しています。要素数は解析対象により必要数が変わります。十分なサイズを用意してください。- フィールドの解析方法を
jkii_field_t
配列に格納します(解析方法の指定の詳細は フィールドの解析情報 をご覧ください)。今回の例では、配列全体を一旦をゼロクリアした後、1 件目の要素に「JSON パス/key1
の値を int 型で取得」という解析方法を保存しています。また、2 件目の要素で NULL ターミネートしています。予期しないバグの原因になるため、始めに配列全体のゼロクリアを必ず実行してください。 jkii_parse
関数を実行して JSON の解析を行います。- 解析結果は、
jkii_field_t
配列の各要素のfield_copy
メンバーより取得できます。
関数の実行が成功すると、関数は JKII_ERR_OK
を返します。また jkii_field_t
配列のフィールドの result
メンバーに JKII_FIELD_ERR_OK
が記録されます。
複数のフィールドの同時読み込み
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];
/* jkii resource */
jkii_token_t tokens[32];
jkii_resource_t resource = {tokens, 32};
/* Initialize fields. */
jkii_field_t fields[3];
memset(fields, 0x00, sizeof(fields));
fields[0].path = "/key1";
fields[0].type = JKII_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 = JKII_FIELD_TYPE_BOOLEAN;
fields[2].path = NULL;
/* Parse the JSON string. */
jkii_parse_err_t result = jkii_parse(
json_string,
sizeof(json_string),
&fields,
&resource);
/* Check if parsing the string failed. */
if (result != JKII_ERR_OK) {
return;
}
/* Check if parsing either field failed. */
if (fields[0].result != JKII_FIELD_ERR_OK ||
fields[1].result != JKII_FIELD_ERR_OK) {
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);
ここでは以下の処理を行っています。
jkii_token_t
の配列を用意しjkii_resource_t
に配列と要素数を設定して解析用リソースを用意しています。要素数は解析対象により必要数が変わります。十分なサイズを用意してください。- フィールドの解析方法を
jkii_field_t
配列に格納します。ここでは以下の設定を行っています。/key1
を文字列として取得します。バッファbuf
を入力パラメータfield_copy.string
で指定して API に入力すると、API は解析結果の文字列をその領域に書き出します。/key2/key2-2/[1]
は、key2
以下、key2-2
の配列から、配列要素 [ 1 ] を取り出すものです(ここではtrue
が取得される想定です)。型は boolean を指定しています。
jkii_parse
関数を実行して JSON の解析を行います。- 解析結果は、
jkii_field_t
のfield_copy
メンバーにそれぞれ取得されます。ここでは、2 件取得したため、それぞれの取得が成功したことを確認後、結果を出力しています。
もし、一部のフィールドの取得に失敗した場合は、関数は JKII_ERR_PARTIAL
を返し、失敗したフィールドの result
メンバーがエラーを表します。
フィールドの解析情報
JSON のフィールドを解析する際には、jkii_field_t
構造体の配列に解析方法を記述します。ここでは、解析方法の詳細を説明します。
まず、jkii_field_t
は配列として宣言します。配列は実際に取得したいフィールド数より 1 つ多い要素数を確保し、最後に path
メンバーを NULL に指定して終端の目印とします。
設定するフィールドは以下のとおりです。設定前に memset
関数によって全要素をゼロクリアしておく必要があります。ゼロクリアを忘れると、予期しないバグが発生する原因になります。
メンバー | 入出力 | 説明 |
---|---|---|
path | 入力 | 取得したいフィールドのパスを JSON の階層に従って指定します。パスのセパレータは / で、/ から始まる必要があります。[ ] によって配列の要素を指定できます。たとえば {"light":{"color":[0,128,255]}} で 0 を取得するには /light/color/[0] を指定します。フィールド名に [ 、] 、/ 、\ 含む場合は、 \ でエスケープして \[ のように指定します。 |
result | 出力 | フィールドごとに解析結果のステータスが返されます。成功した場合は JKII_FIELD_ERR_OK です。その他の値は jkii Documentation - jkii_field_err_t をご覧ください。 |
type | 入力, 出力 |
フィールドの型を指定します。入力時は期待されるフィールドの型を指定します。出力時は実際に解析された型を格納して返します。型の詳細は下記の データ型 をご覧ください。 |
start | 出力 | 解析できた JSON 文字列の開始位置を返します。指定した JSON 文字列の先頭バイト位置を 0、次のバイトを 1 …として返します。文字列の場合は " の次の位置を差します。文字以外の場合にも設定されます。 |
end | 出力 | 解析できた JSON 文字列の終了位置の次のバイトを返します。たとえば、{"abc":"XYZ"} のキー: abc が見つかった場合、end は c の次の位置: " を表す 5 となります。文字以外の場合にも設定されます。 |
field_copy | 入力, 出力 |
内部は union で以下のメンバーを持っています。
|
filed_copy_ buff_size |
入力 | field_copy.string で、入力パラメータとして結果格納用のバッファを指定する場合、セットしたバッファサイズをバイト単位で指定します。下記の 文字列の扱い をご覧ください。 |
データ型
jkii ライブラリで使用できるデータ型は以下の表のとおりです。
入力時に type
メンバーでこれらの値を指定すると、path
メンバーで指定されたフィールドが、type
の型であることを期待します。一致している場合はその値を取得できます。不一致の場合は result
メンバーで JKII_FIELD_ERR_TYPE_MISMATCH
エラーを返します。
なお、JKII_FIELD_TYPE_INTEGER
や JKII_FIELD_TYPE_OBJECT
等の実際のデータ型を期待しているフィールドが、JSON の null だった場合、データ型の不一致エラーとなります。
type
メンバーに JKII_FIELD_TYPE_ANY
を指定した場合、JSON での実際の値に合わせたデータ型が返ります。
データ型 | 説明 |
---|---|
JKII_FIELD_TYPE_ANY | 入力時には任意の型を表すデータ型として指定できます。出力時には、実際にマッチした JSON のフィールドの型がセットされて返ります。Object や配列にマッチさせることも可能です。 |
JKII_FIELD_TYPE_INTEGER | 入力時、出力時とも、int 型のデータを表します。field_copy.int_value として値を取得します。 |
JKII_FIELD_TYPE_LONG | 入力時、出力時とも、long 型のデータを表します。field_copy.long_value として値を取得します。 |
JKII_FIELD_TYPE_DOUBLE | 入力時、出力時とも、double 型のデータを表します。field_copy.double_value として値を取得します。 |
JKII_FIELD_TYPE_BOOLEAN | 入力時、出力時とも、boolean 型のデータを表します。field_copy.boolean_value として値を取得します。 |
JKII_FIELD_TYPE_NULL | 入力時に指定すると、そのフィールドが JSON の null であることを期待します。入力で KII_JSON_FIELD_TYPE_ANY を指定して JSON 文字列の null にマッチした場合、出力値としてこの値を返します。 |
JKII_FIELD_TYPE_STRING | 入力時、出力時とも、文字列型のデータを表します。field_copy.string として値を取得します。下記の 文字列の扱い もご覧ください。 |
JKII_FIELD_TYPE_OBJECT | 入力時、出力時とも、Object 型のデータを表します。入力時には指定されたフィールドが Object であることを期待します。出力時には Object として取得できたことを表します。field_copy.string として値を取得でき、{"key2-1":123,"key2-2":[false,true,false]} のような文字列となります。下記の 文字列の扱い もご覧ください。 |
JKII_FIELD_TYPE_ARRAY | 入力時、出力時とも、配列のデータを表します。入力時には指定されたフィールドが配列であることを期待します。出力時には配列として取得できたことを表します。field_copy.string として値を取得でき、[false,true,false] のような文字列となります。下記の 文字列の扱い もご覧ください。 |
文字列の扱い
jkii ライブラリで、フィールドの解析結果を文字列として出力する場合、その文字列用のバッファは次の 2 通りの方法で扱うことができます。これは、文字列、Object、配列の解析結果を出力する場合に共通する仕様です。
領域を入力パラメータとして用意する
文字列の格納に必要な領域を呼び出し元で用意する方法です。API を呼び出すと、解析した結果をその領域にコピーします。コピーした文字列は NULL ターミネートされています。
この方法を使用するには、配列中の各要素の
field_copy.string
メンバーに、確保したバッファへのポインタを設定します。さらに、file_copy_buff_size
メンバーに確保したバッファサイズをバイト単位で格納します。バッファサイズには終端文字も含みます。元の JSON 文字列の領域を使用する
API の実行結果の文字列を、元の JSON 文字列中のポインタとして返す方法です。元の JSON 文字列の領域を共有しているため、取得された値の文字列は NULL ターミネートされていません。フィールドの
start
メンバーとend
メンバーから結果の文字列値のバイト数が決まります。この方法を使用するには、配列中の各要素の
field_copy.string
メンバーを NULL に設定した状態で解析 API を呼び出します。
トークン用領域の確保
ライブラリ内で JSON 文字列を解析する際、JSON のトークンの解析に必要な領域を呼び出し元で確保する必要があります。
必要なバッファサイズは、JSON の複雑さに依存します。JSON のトークン 1 個につき、jkii_token_t
の配列要素 1 個分が必要です。ここでのトークンとは JSON のキーとその値(単純な値、Object、配列全体、配列要素)を表します。
たとえば、{"key1":"value", "key2":{"key3":2}}
のような JSON には、以下の 7 個のトークンが存在します。
- JSON 全体
key1
value
key2
{"key3":2}
key3
2
解析用の領域を確保する方法には、次の 2 通りの方法があります。いずれかを選択してください。
確保済みの領域を設定する
これは、上記のサンプルで行っている方法です。
jkii_token_t
の配列を確保し jkii_resource_t
に配列へのポインターとその要素数を設定して確保し、それを jkii_parse
の呼び出し時に渡します。
コールバック関数を使って必要な領域だけを確保する
静的に大きな領域を取れない場合、jkii_parse_with_allocator
に設定するコールバック関数を使って必要なバッファを動的に確保できます。
typedef jkii_resource_t* (*JKII_CB_RESOURCE_ALLOC)(size_t required_size);
typedef void (*JKII_CB_RESOURCE_FREE)(jkii_resource_t* resource);
コールバック関数の実装例を以下に示します。
static jkii_resource_t* cb_alloc(size_t required_size)
{
jkii_resource_t* res =
(jkii_resource_t*)malloc(sizeof(jkii_resource_t));
if (res == NULL) {
return NULL;
}
jkii_token_t *tokens =
(jkii_token_t*)malloc(sizeof(jkii_token_t) * required_size);
if (tokens == NULL) {
free(res);
return NULL;
}
res->tokens = tokens;
res->tokens_num = required_size;
return res;
}
static void cb_free(jkii_resource_t* resource) {
free(resource->tokens);
free(resource);
}
jkii ライブラリで、事前に必要なトークンの数を見積もってこのコールバック関数を呼び出します。required_size
パラメータとして渡される必要なトークンの個数を jkii_token_t
構造体のサイズと乗じて必要なサイズの領域を確保します。最終的に、確保した領域へのポインタとトークンの個数を、jkii_resource_t
構造体に格納します。
上記のサンプルで jkii_parse
の代わりに jkii_parse_with_allocator
を使用すると以下のようになります。
/* Parse a JSON string. */
jkii_parse_err_t res = jkii_parse_with_allocator(
json_string,
strlen(json_string),
fields,
cb_alloc,
cb_free);
/* Check if parsing the string failed. */
if (result != JKII_ERR_OK) {
return;
}