WinRT JsonObjectで整数intを安全に読み取る方法【Windows::Data::Json徹底解説】

WinRT(C++/WinRT)で JSON を扱うとき、「数値を int として受け取りたいのに JsonObject からは double しか取れない」「API によっては数値が文字列で返ってくる」など、地味にハマりやすいポイントがあります。本記事では Windows::Data::Json::JsonObject を使って、数値・文字列どちらの JSON 表現にも安全に対応しつつ、意図どおりの int に変換する実践的なパターンをまとめます。

目次

WinRT の Windows::Data::Json と JsonObject の基本

まず前提として知っておきたいのは、Windows::Data::Json の JsonObject は数値を double として扱うという点です。C++/WinRT でもこれは同じで、整数専用の Getter(GetNamedInt のようなもの)は用意されていません。

代表的なメソッドと型の対応を表にまとめると、次のようになります。

JSON の例JsonValueType取得メソッドC++ 側の型
{“OrderQuantity”: 10}JsonValueType::NumberGetNamedNumber(L”OrderQuantity”)double
{“OrderQuantity”: 12.34}JsonValueType::NumberGetNamedNumber(L”OrderQuantity”)double
{“OrderQuantity”: “10”}JsonValueType::StringGetNamedString(L”OrderQuantity”)winrt::hstring
{“OrderQuantity”: “12.34”}JsonValueType::StringGetNamedString(L”OrderQuantity”)winrt::hstring

このため、「int を取りたい」=「いったん double または文字列で受けてから、自分で int に変換する」という流れになります。

数値型 JSON を読む:GetNamedNumber から int へ変換

JSON が素直に数値型(未引用の 123 や 12.34)の場合は、GetNamedNumber を使うのが基本です。


using namespace winrt;
using namespace Windows::Data::Json;

JsonObject obj;
if (JsonObject::TryParse(jsonstr, obj)) {
    // 項目が存在しない場合は 0.0 を返すオーバーロード
    double d = obj.GetNamedNumber(L"OrderQuantity", 0.0);

    // そのまま double として使う
    double quantity_double = d;

    // 整数にしたい場合:0 方向への切り捨て
    int quantity_trunc = static_cast<int>(d);

    // 四捨五入して int にしたい場合
    int quantity_round = static_cast<int>(std::lround(d));
}

ポイントは次の通りです。

  • GetNamedNumber(name, defaultValue) を使うと、キーが存在しない場合に既定値(ここでは 0.0)を返してくれるため、欠落に強いコードになります。
  • 整数化するときの 丸め規則は自分で決める必要がある(切り捨てか四捨五入か、など)。

丸め方法ごとの挙動を整理しておくと、バグを防ぎやすくなります。

式説明例:1.7例:-1.7
static_cast<int>(d)0 方向に切り捨て1-1
std::floor(d)常に小さい方へ切り捨て1-2
std::ceil(d)常に大きい方へ切り上げ2-1
std::lround(d)四捨五入して long に変換(int にキャスト)2-2

業務アプリでは「四捨五入なのか切り捨てなのか」が仕様として重要になるケースが多いため、どの関数を使うか設計書に明記しておくことをおすすめします。

文字列型 JSON を読む:GetNamedString + std::stod / std::stoi

外部 API では、「数値だけど JSON 上は文字列」というレスポンスがよくあります。例えば次のようなパターンです。


{
  "OrderQuantity": "12.34",
  "OrderId": "A001"
}

この場合は GetNamedString で文字列を取り、C++ 標準ライブラリで数値にパースします。


using namespace winrt;
using namespace Windows::Data::Json;

JsonObject obj;
if (JsonObject::TryParse(jsonstr, obj)) {
    hstring hs = obj.GetNamedString(L"OrderQuantity", L"0");
    // hstring → std::wstring
    std::wstring ws{ hs.c_str() };

    // 小数として扱う場合
    double d = std::stod(ws);
    int n_trunc = static_cast&lt;int&gt;(d); // 切り捨て

    // 整数しか来ない契約なら、最初から int として読んでもよい
    // int n = std::stoi(ws);
}

ここでの注意点です。

  • std::stod / std::stoi は、パースできない文字列が渡されると std::invalid_argument 例外を投げます。
  • 数値が大きすぎる場合は std::out_of_range 例外になります。
  • 例外を嫌う場合は、事前に正規表現や簡易チェックで「数字だけか」を確認する、もしくは std::from_chars(整数のみ・非例外・ロケール非依存)を使う手もあります。

整数しか来ない JSON で、なおかつパフォーマンスが重要なケースでは、次のような std::from_chars 版も選択肢になります。


int parse_int(std::wstring const&amp; ws, int default_value = 0)
{
    int value = 0;
    auto first = ws.data();
    auto last  = ws.data() + ws.size();

    std::from_chars_result result =
        std::from_chars(first, last, value);

    if (result.ec == std::errc{}) {
        return value; // 成功
    }
    return default_value; // 失敗時は既定値
}

JSON 側のフォーマットが安定しているなら、このような軽量なパーサを使うと高速で例外も発生しません。

数値か文字列か分からない場合:型を見て分岐する堅牢実装

実務では「一部の環境では数値、別の環境では文字列」といった揺れた JSON を扱わざるを得ないこともあります。その場合は、JsonValueType を確認して分岐するのが安全です。


using namespace winrt;
using namespace Windows::Data::Json;

JsonObject obj;
if (JsonObject::TryParse(jsonstr, obj)) {
    double d = 0.0;

    if (obj.HasKey(L"OrderQuantity")) {
        JsonValue v = obj.GetNamedValue(L"OrderQuantity");

        if (v.ValueType() == JsonValueType::Number) {
            d = v.GetNumber(); // 数値型
        }
        else if (v.ValueType() == JsonValueType::String) {
            auto hs = obj.GetNamedString(L"OrderQuantity");
            d = std::stod(std::wstring{ hs.c_str() }); // 文字列 → 数値
        }
        else {
            // それ以外(null, Object, Array, Boolean)は仕様に応じてハンドリング
        }
    }

    int n = static_cast&lt;int&gt;(std::lround(d)); // 四捨五入
}

この実装のメリットは、次のような点にあります。

  • JSON の型が Number か String かに応じて最適な読み方を選べる。
  • null や Object、Array など、想定外の型が来たときにも分岐を追加しやすい。
  • 型チェックを行うことで、「仕様が変わっていたのに気付かない」という事故を防ぎやすくなる。

さらに堅牢にしたい場合は、想定外の型が来たときにログを出す・例外を投げるなどの対応も検討しましょう。

実務で使えるヘルパー関数:JsonObject から int を安全に取得する

似たような変換処理を毎回書くのは面倒なので、共通ヘルパーを 1 つ用意しておくとコードがすっきりします。以下は、数値/文字列の両方に対応した、典型的なヘルパー例です。


int GetNamedInt(
    Windows::Data::Json::JsonObject const&amp; obj,
    winrt::hstring const&amp; name,
    int defaultValue = 0)
{
    using namespace Windows::Data::Json;

    if (!obj.HasKey(name)) {
        return defaultValue;
    }

    JsonValue v = obj.GetNamedValue(name);
    double d = 0.0;

    switch (v.ValueType()) {
    case JsonValueType::Number:
        d = v.GetNumber();
        break;

    case JsonValueType::String: {
        auto hs = obj.GetNamedString(name);
        std::wstring ws{ hs.c_str() };
        try {
            d = std::stod(ws);
        }
        catch (...) {
            // 不正な文字列だった場合は既定値を返す
            return defaultValue;
        }
        break;
    }

    // null やそれ以外は既定値扱い
    default:
        return defaultValue;
    }

    // int の範囲チェック(オーバーフロー対策)
    double min_int = static_cast&lt;double&gt;(std::numeric_limits&lt;int&gt;::min());
    double max_int = static_cast&lt;double&gt;(std::numeric_limits&lt;int&gt;::max());

    if (d &lt; min_int) {
        return std::numeric_limits&lt;int&gt;::min();
    }
    if (d &gt; max_int) {
        return std::numeric_limits&lt;int&gt;::max();
    }

    // ここでは四捨五入で int に変換
    return static_cast&lt;int&gt;(std::lround(d));
}

このヘルパーを使えば、呼び出し側のコードはとても読みやすくなります。


JsonObject obj;
// ... TryParse などで obj を構築済みとする ...

int quantity = GetNamedInt(obj, L"OrderQuantity", 0);
int price    = GetNamedInt(obj, L"Price", 0);

「どのキーが整数として解釈されるか」が関数名と引数で明示されるため、後からコードを読むときの理解コストも下がります。

例:注文 API の JSON から数量と金額を読む

もう少し現実的な例として、注文情報を返す API の JSON を想定してみます。


{
  "OrderId": "A001",
  "OrderQuantity": 12.0,
  "UnitPrice": "480",
  "Amount": "5760.0"
}

このような JSON を C++/WinRT で扱う場合の例です。


using namespace winrt;
using namespace Windows::Data::Json;

struct Order
{
    std::wstring id;
    int quantity = 0;
    int unit_price = 0;
    int amount = 0;
};

std::optional&lt;Order&gt; ParseOrder(std::wstring const&amp; jsonstr)
{
    JsonObject obj;
    if (!JsonObject::TryParse(jsonstr, obj)) {
        return std::nullopt; // JSON として不正
    }

    Order order{};

    // Id は文字列として取得
    order.id = obj.GetNamedString(L"OrderId", L"").c_str();

    // 数量・単価・金額はヘルパーを使って int として取得
    order.quantity   = GetNamedInt(obj, L"OrderQuantity", 0);
    order.unit_price = GetNamedInt(obj, L"UnitPrice", 0);
    order.amount     = GetNamedInt(obj, L"Amount", 0);

    return order;
}

このようにしておけば、レスポンスの JSON が

  • "OrderQuantity": 12 のように数値型だったとしても
  • "OrderQuantity": "12" のように文字列型だったとしても

同じ GetNamedInt で扱えるため、バックエンドの細かい揺れに影響されにくいクライアントコードになります。

JsonObject / JsonValue 使用時の例外とエラーハンドリング

Windows::Data::Json の API には「例外を投げる版」と「安全版(Try 系)」が混在しています。代表的なパターンを整理しておきましょう。

メソッド動作エラー時の挙動
JsonObject::ParseJSON 文字列をパースして JsonObject を生成パース失敗で例外
JsonObject::TryParseパースを試み、成功/失敗を bool で返す失敗しても例外を投げず false
GetNamedNumber(name)指定キーの数値を取得キーがない・型が Number でないと例外
GetNamedNumber(name, defaultValue)キーの数値。キーがなければ defaultValueキーはあるが Number でないと例外
GetNamedString(name)指定キーの文字列を取得キーがない・型が String でないと例外
GetNamedString(name, defaultValue)キーの文字列。キーがなければ defaultValueキーはあるが String でないと例外

整数取得ヘルパーを自作する際は、上記の挙動を踏まえて

  • JsonObject 側のエラー(キー欠落・型不一致)
  • C++ 標準ライブラリ側のエラー(std::stod / std::stoi の例外)

の両方をどう扱うか設計する必要があります。

ユーザー入力や外部サービスなど「壊れた JSON も受け取りうる」ケースでは、例外をキャッチして既定値にフォールバックし、ログだけ残して処理継続するのが現実的なことが多いでしょう。

ロケールと小数点の注意点

JSON の数値は仕様上「小数点は . 固定」ですが、std::stod は実行時ロケールの影響を受ける可能性があります。

  • ロケールが ja_JP でも、通常は . が小数点として解釈されます。
  • しかし、カスタムロケールや特殊な設定の環境では思わぬ挙動になることがあります。

対策としては、次のような方針が考えられます。

  • そもそも JSON 側で 整数のみを文字列で送る契約にし、std::stoi か std::from_chars を使う。
  • どうしても小数文字列をパースする場合は、テスト環境を複数ロケールで用意して動作確認する。

特に「金額」「数量」のような業務上重要な値を扱うときは、ロケールに依存しない設計になっているかをチーム内で確認しておくと安心です。

JSON スキーマ設計で悩みを減らす

ここまでクライアント側の工夫を見てきましたが、最も根本的な解決策はやはり

「JSON 側は数値を数値型で送る」

というスキーマルールを決めることです。

  • 数量・金額・重さ・距離など、数値として意味を持つものは Number 型で送る。
  • 識別子・コード・電話番号のように「文字列として意味がある数値」は String 型で送る。

このルールさえ守られていれば、クライアント側では単純に GetNamedNumber(+必要なら四捨五入)だけで済み、型分岐や例外処理が大幅に簡略化されます。

既存システムでは「過去との互換性」のためにスキーマをすぐに変えられないことも多いですが、新規 API では最初からこの方針を採用しておくと、将来のメンテナンス性が大きく変わります。

Windows::Data::Json を使うか、別ライブラリを使うか

最後に少し視野を広げて、Windows::Data::Json を使い続けるかどうかという観点にも触れておきます。

Windows::Data::Json は WinRT の一部として標準で使える反面、

  • API としてはやや古く、C++ らしい型安全性はそこまで高くない
  • 汎用 JSON ライブラリと比べると、表現力やパフォーマンスに限界がある

といった側面もあります。プロジェクトによっては、次のような構成を取るケースもあります。

  • 「OS との境界(WinRT)」では Windows::Data::Json を 1 回だけ使う
  • 受け取った JSON 文字列を汎用 JSON ライブラリ(nlohmann/json など)で再パースして、アプリ内部ではそちらを使う

このようなアーキテクチャにすると、「Windows::Data::Json に依存する部分」を境界レイヤーに閉じ込められるため、アプリ全体の保守性が上がります。とはいえ、軽量なツールや UWP/WinUI アプリなどでは Windows::Data::Json だけで十分なことも多いので、規模やチームのスキルセットに合わせて選択するとよいでしょう。

まとめ:JsonObject から int を安全に読むためのチェックリスト

最後に、本記事で扱ったポイントをチェックリスト形式でまとめます。

  • JsonObject は数値を double で扱うため、int を直接取得する API はない。
  • JSON が数値型なら GetNamedNumber で double を取り、仕様に合わせて static_cast や std::lround で int に変換する。
  • JSON が文字列型なら GetNamedString で文字列を取り、std::stod / std::stoi / std::from_chars で数値化する。
  • 数値か文字列か分からない API は、JsonValue::ValueType() で型を見て分岐する堅牢実装にしておく。
  • 例外とエラー(キー欠落・型不一致・パース失敗)は、既定値やログ出力など方針を決めて一貫して扱う。
  • ロケールに依存しない設計を心がけ、特に金額や数量の丸め規則は仕様として明文化する。
  • 繰り返し出てくる変換処理は、共通ヘルパー関数(GetNamedInt など)として切り出し、呼び出し側のコードをシンプルに保つ。
  • 可能なら JSON スキーマ側で「数値は数値型」「文字列は文字列型」というルールを徹底し、クライアントの実装を簡素化する。

これらを押さえておけば、Windows::Data::Json::JsonObject を使った int 変換で悩むことはほとんどなくなります。既存コードであいまいな変換ロジックが散らばっている場合は、本記事のヘルパー関数をベースにリファクタリングしてみると、バグの温床を一気に潰すことができるはずです。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次