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::Number | GetNamedNumber(L”OrderQuantity”) | double |
| {“OrderQuantity”: 12.34} | JsonValueType::Number | GetNamedNumber(L”OrderQuantity”) | double |
| {“OrderQuantity”: “10”} | JsonValueType::String | GetNamedString(L”OrderQuantity”) | winrt::hstring |
| {“OrderQuantity”: “12.34”} | JsonValueType::String | GetNamedString(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<int>(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& 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<int>(std::lround(d)); // 四捨五入
}
この実装のメリットは、次のような点にあります。
- JSON の型が
NumberかStringかに応じて最適な読み方を選べる。 nullやObject、Arrayなど、想定外の型が来たときにも分岐を追加しやすい。- 型チェックを行うことで、「仕様が変わっていたのに気付かない」という事故を防ぎやすくなる。
さらに堅牢にしたい場合は、想定外の型が来たときにログを出す・例外を投げるなどの対応も検討しましょう。
実務で使えるヘルパー関数:JsonObject から int を安全に取得する
似たような変換処理を毎回書くのは面倒なので、共通ヘルパーを 1 つ用意しておくとコードがすっきりします。以下は、数値/文字列の両方に対応した、典型的なヘルパー例です。
int GetNamedInt(
Windows::Data::Json::JsonObject const& obj,
winrt::hstring const& 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<double>(std::numeric_limits<int>::min());
double max_int = static_cast<double>(std::numeric_limits<int>::max());
if (d < min_int) {
return std::numeric_limits<int>::min();
}
if (d > max_int) {
return std::numeric_limits<int>::max();
}
// ここでは四捨五入で int に変換
return static_cast<int>(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<Order> ParseOrder(std::wstring const& 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::Parse | JSON 文字列をパースして 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 変換で悩むことはほとんどなくなります。既存コードであいまいな変換ロジックが散らばっている場合は、本記事のヘルパー関数をベースにリファクタリングしてみると、バグの温床を一気に潰すことができるはずです。

コメント