JSON の解析

Thing-IF SDK では、JSON の解析を行うための独自ライブラリ「kii_json」を使用できます。コマンドを受け取った後、アクションのパラメータを解析する際などに利用可能です。

このライブラリを使うと、JSON 文字列から、指定されたパスに書かれている値を、指定されたデータ型(文字列や数値)で取得できます。

たとえば、以下のような JSON から colorrgb にある 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_tfield_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 が見つかった場合、endc の次の位置: " を表す 5 となります。文字以外の場合にも設定されます。
field_copy 入力,
出力
内部は union で以下のメンバーを持っています。
  • string:文字列型の値として取得またはバッファを指定します。下記の 文字列の扱い をご覧ください。
  • int_value:int 型の値として取得します。
  • long_value:long 型の値として取得します。
  • double_value:double 型の値として取得します。
  • boolean_value:boolean 型の値として、kii_json_boolean_t で取得します。値は KII_JSON_TRUE または KII_JSON_FALSE です。
filed_copy_
buffer_size
入力 field_copy.string で、入力パラメータとして結果格納用のバッファを指定する場合、セットしたバッファサイズをバイト単位で指定します。下記の 文字列の扱い をご覧ください。

データ型

kii_json ライブラリで使用できるデータ型は以下の表のとおりです。

入力時に type メンバーでこれらの値を指定すると、path メンバーで指定されたフィールドが、type の型であることを期待します。一致している場合はその値を取得できます。不一致の場合は result メンバーで KII_JSON_FIELD_PARSE_TYPE_UNMATCHED エラーを返します。

なお、KII_JSON_FIELD_TYPE_INTEGERKII_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 との間のやりとりを行うためには十分な値です。もし、スキーマ定義(Android, iOS)で非常に複雑なパラメータのやりとりが必要な場合は、値の変更を検討してください。

このマクロを定義すると、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);