JSON の解析

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

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

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

データ型

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

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

なお、JKII_FIELD_TYPE_INTEGERJKII_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;
}