C++/WinRTでCStringのJSONをJsonObjectに変換しGetNamedStringのキー欠如をhresult_errorで捕捉する方法

MFCで受け取ったCStringのJSONをC++/WinRTのJsonObjectに変換し、GetNamedString/GetNamedNumberで必須キーが欠けたときに確実にエラー表示したい――この要件は、文字列変換と例外型(winrt::hresult_error)の理解で整理するとスムーズに実装できます。

目次

今回の課題を一言で整理すると

やりたいことは大きく2つです。

  • CStringに入っているJSON文字列を、C++/WinRTで扱えるWindows::Data::Json::JsonObjectへ変換する
  • 変換後に必須キー(例:Referencecode)が無い場合、例外を捕まえてオペレータにメッセージを表示する

ここで引っかかりやすいのが「WinRT APIが投げる例外の型」です。GetNamedString/GetNamedNumberは、標準C++のstd::invalid_argumentではなく、winrt::hresult_error(および派生)として例外が飛びます。つまり、catch側の型が違うと捕まえられません。

なぜcatchに入らないのか:WinRT例外はwinrt::hresult_error

JsonObject::GetNamedString / GetNamedNumberでキーが存在しない、または型が違う場合、WinRTの例外として処理されます。MFC/標準C++の感覚で下記のように書くと、意図したcatchに入りません。

catch (char* errorMessage) { ... }
catch (std::invalid_argument const& ex) { ... }

WinRT APIの例外を捕まえるなら、まずはこれが基本形です。

catch (winrt::hresult_error const& ex)
{
    // ex.code()  : HRESULT
    // ex.message(): hstring(人間向けメッセージ)
}

現場で混乱しやすいポイントを表にまとめます。

状況起きること正しい捕捉よくある誤解
キーが存在しないGetNamedString等が例外を投げるcatch(winrt::hresult_error)std::invalid_argumentで捕まると思ってしまう
型が違う(数値のはずが文字列等)同様にWinRT例外catch(winrt::hresult_error)「変換に失敗しただけ」と思い込んで見落とす
JSON文字列が壊れているTryParseがfalse(Parseなら例外)TryParseで分岐 or catch(winrt::hresult_error)キー欠如と同じ扱いにして原因が不明瞭になる

前提:MFC(デスクトップ)でC++/WinRTを使うときの最低限

MFCアプリでC++/WinRTを呼ぶ場合、スレッドでWinRTを使う前に初期化が必要になるケースがあります。多くのプロジェクトでは、WinRTを使うスレッドの入口で初期化しておくのが安全です。

#include <winrt/Windows.Foundation.h>

void InitWinRTForThisThread()
{
    winrt::init_apartment(); // 既に初期化済みなら例外にならない設計
}

特に、ワーカースレッドでJSON解析→UIスレッドに通知、という構成の現場では「どのスレッドでWinRT APIを触っているか」が事故ポイントになります。WinRT関連処理をどのスレッドに寄せるか(UIスレッドで完結させるのか、ワーカースレッドで完結させるのか)を最初に決めておくと、後でハマりにくくなります。

CString(JSON文字列)をJsonObjectへ変換する:実装の基本形

変換は「CString →(UTF-16の)std::wstring → winrt::hstring → JsonObject::TryParse」が分かりやすく、保守もしやすいです。

#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Data.Json.h>

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

JsonObject ParseJsonFromCString(const CString& data)
{
    JsonObject obj{};

    // Unicodeビルド(CStringがワイド文字)想定:
    std::wstring ws(data.GetString());
    hstring json = winrt::to_hstring(ws);

    if (!JsonObject::TryParse(json, obj))
    {
        throw std::runtime_error("Failed to parse JSON.");
    }

    return obj;
}

「CStringをそのまま渡せないの?」という点で混乱しがちなので、文字列変換の考え方を表にしておきます。

CStringの状態おすすめ変換注意点
Unicodeビルド(一般的)data.GetString() → std::wstring → winrt::to_hstringJSONは通常UTF-16扱いでOK
MBCS/ANSIビルドマルチバイト→ワイド変換してからhstring変換コードページで文字化けの原因になりやすい
CStringにUTF-8が入っている(設計上の混在)UTF-8→UTF-16へ明示変換してからTryParseJSON自体はUTF-8のことが多いので、入力経路の仕様を要確認

実務的には「JSON文字列がどのエンコーディングで入ってくるか」を仕様として固定するのが重要です。ログや通信・ファイル経由だとUTF-8、UI経由だとUTF-16、のように混在すると、再現性の低い不具合になります。

必須キーが欠けたら止めたい:winrt::hresult_errorをcatchして通知する

質問の趣旨は「Referencecodeをわざと存在しないキー名で取得し、例外が起きたらオペレータに表示したい」です。やるべきことは明快で、catchの型をwinrt::hresult_errorにするだけです。

#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Data.Json.h>
#include <iostream>

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

void ReadFieldsOrNotify(const JsonObject& obj)
{
    try
    {
        auto messageId = obj.GetNamedString(L"MessageId");
        auto station   = obj.GetNamedString(L"Station");
        auto program   = obj.GetNamedString(L"Program");
        auto order     = obj.GetNamedString(L"Order");

        // 必須扱い:無ければ例外
        auto reference = obj.GetNamedString(L"Referencecode");
        double qty     = obj.GetNamedNumber(L"OrderQuantity");
        double qtyCur  = obj.GetNamedNumber(L"OrderQuantityCurrent");

        // ここでMFCのメンバーへ反映するなど
        // MessageId = messageId.c_str(); 等
    }
    catch (winrt::hresult_error const& ex)
    {
        // WinRT例外の情報
        winrt::hresult hr = ex.code();
        winrt::hstring msg = ex.message();

        // ログ例(コンソール or OutputDebugStringなどに置き換え)
        std::wcerr << L"JSON読み取りエラー: " << msg.c_str() << L"\n";

        // MFCのダイアログ例(プロジェクトに合わせて)
        // CStringW m(msg.c_str());
        // AfxMessageBox(m, MB_ICONERROR);

        // さらに上に投げるなら、ここでthrow;してもよい
    }
}

ここで大事なのは、オペレータに見せるメッセージを「現場の言葉」にすることです。ex.message()は環境依存の英語メッセージになることもあるため、業務システムでは「どのキーが無いのか」「どの装置/ステーション/注文なのか」を付けて表示するのが定石です。

「どのキーで落ちたか」を確実に出す:現場向けのひと工夫

GetNamedStringが投げる例外だけだと、どのフィールド取得で落ちたかがオペレータにも開発者にも分かりにくいことがあります。そこで、読み取り中のキー名を保持し、catchで文脈付きメッセージにする方法が効きます。

void ReadWithContext(const winrt::Windows::Data::Json::JsonObject& obj)
{
    const wchar_t* current = L"";

    try
    {
        current = L"MessageId";
        auto messageId = obj.GetNamedString(current);

        current = L"Station";
        auto station = obj.GetNamedString(current);

        current = L"Program";
        auto program = obj.GetNamedString(current);

        current = L"Order";
        auto order = obj.GetNamedString(current);

        current = L"Referencecode";
        auto reference = obj.GetNamedString(current);

        current = L"OrderQuantity";
        double qty = obj.GetNamedNumber(current);

        current = L"OrderQuantityCurrent";
        double qtyCur = obj.GetNamedNumber(current);
    }
    catch (winrt::hresult_error const& ex)
    {
        CStringW opMsg;
        opMsg.Format(L"受信データの読み取りに失敗しました。\n不足/不正の可能性がある項目: %s\n詳細: %s",
                     current,
                     ex.message().c_str());

        // 例:オペレータ通知(運用ルールに合わせて)
        // AfxMessageBox(opMsg, MB_ICONERROR);

        // 例:ログ(機械側の解析用)
        // OutputDebugStringW(opMsg);
    }
}

この形にしておくと、キー欠如だけでなく「型が違う」「値がNaNになる」「配列なのに文字列として取ろうとした」など、JSONの揺れが原因のトラブルでも、一次切り分けが速くなります。

例外が発生する条件:キー欠如だけではない

GetNamedString/GetNamedNumberは「キーが無い」以外にも例外になります。典型例は下記です。

  • キーが存在しない(Referencecodeが無い、スペルが違う、大小文字が違う、余計な空白がある)
  • 型が違う(OrderQuantityが数値ではなく、”10″のような文字列で来た)
  • nullが入っている(Referencecode: null のように明示的に空)

テスト用に、わざと揺れたJSONを流すと挙動確認が早いです。

{
  "MessageId": "A001",
  "Station": "ST01",
  "Program": "P01",
  "Order": "O-123",
  "OrderQuantity": "10", 
  "OrderQuantityCurrent": 5
}

この例では、OrderQuantityが文字列なのでGetNamedNumberで落ちる可能性があります。「必須キーはあるのに落ちる」ケースの多くがこの型不一致です。

キー欠如を「例外なし」で検出したい場合:HasKeyと既定値

要件が「必須項目が欠けたら止める」なら例外キャッチで問題ありませんが、運用によっては「欠けていたら警告、ただし処理は継続」が必要なこともあります。その場合は、例外を使わずに分岐するほうが読みやすく、パフォーマンス的にも有利です。

HasKeyで存在チェックしてから読む

if (obj.HasKey(L"Referencecode"))
{
    auto rc = obj.GetNamedString(L"Referencecode");
    // Referencecode = rc.c_str();
}
else
{
    // 必須ならここで停止、任意なら警告にするなど
    // AfxMessageBox(L"Referencecode が含まれていません。", MB_ICONWARNING);
}

既定値付きGetNamedString/GetNamedNumberで「欠如=既定値」に落とす

auto rc = obj.GetNamedString(L"Referencecode", L"");
double qty = obj.GetNamedNumber(L"OrderQuantity", 0.0);

if (rc.empty())
{
// 欠如または空を警告扱いにする
}

if (qty == 0.0)
{
// 0を未設定として扱う業務ルールならここで判定
}

「必須/任意」をコードで表現すると、読み手にも運用側にも意図が伝わりやすくなります。おすすめの使い分けを表にします。

項目の扱いおすすめAPIメリット注意点
必須(無いと処理不能)GetNamedString / GetNamedNumber + catch(winrt::hresult_error)欠落を強制的に検出でき、異常系が明確例外をログ・通知に変換する設計が必要
任意(無くても進める)HasKey / 既定値付きGetNamedString例外を使わず分岐で読みやすい欠落を見落とさないよう警告や監視を用意
必須だが運用継続優先HasKey+不足時は警告&代替値ライン停止回避など運用要件に合わせやすい代替値で誤動作しない設計(安全側)が必須

型不一致も事前に検出する:JsonValueTypeで堅牢にする

「キーはあるが型が違う」という事故は、システム連携の現場で本当に多いです。ここを堅牢にするなら、まずIJsonValueを取り出して型を見てから取得する方法が効きます。

#include <winrt/Windows.Data.Json.h>

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

hstring GetRequiredStringStrict(const JsonObject& obj, const wchar_t* key)
{
    if (!obj.HasKey(key))
    {
        throw std::runtime_error("Missing required key.");
    }

    auto v = obj.GetNamedValue(key);
    if (v.ValueType() != JsonValueType::String)
    {
        throw std::runtime_error("Type mismatch (expected String).");
    }

    return obj.GetNamedString(key);
}

double GetRequiredNumberStrict(const JsonObject& obj, const wchar_t* key)
{
    if (!obj.HasKey(key))
    {
        throw std::runtime_error("Missing required key.");
    }

    auto v = obj.GetNamedValue(key);
    if (v.ValueType() != JsonValueType::Number)
    {
        throw std::runtime_error("Type mismatch (expected Number).");
    }

    return obj.GetNamedNumber(key);
}

このように「キー欠如」と「型不一致」を分けて検出できると、オペレータへの案内文も変えられます(例:欠如なら“送信元設定の問題”、型不一致なら“送信元のバージョン違い”など)。また、ログに残す際にも原因が明確になります。

なお、この例はstd::runtime_errorを投げています。最終的な捕捉側では、WinRT例外と標準例外の両方を拾う設計にすると安心です。

try
{
    // JSON処理
}
catch (winrt::hresult_error const& ex)
{
    // WinRT例外
}
catch (std::exception const& ex)
{
    // 自前チェックの例外など
}

Visual Studioで止まる「最初のチャンス例外」との付き合い方

デバッグ中に、例外が投げられた瞬間にVisual Studioが一度止まることがあります。これは「最初のチャンス例外」として表示されることがあり、catchで捕まえる設計でも一時停止する設定になっている場合があります。

  • デバッガが止まっても、継続すればcatchに入って処理が続くことがある
  • 一方で、catchの型が間違っていると“未処理例外”になり、本当に落ちる

今回のケースは後者(catchの型違い)になりやすいので、まずはcatch(winrt::hresult_error)を追加し、「確実に捕まえられる状態」を先に作るのが近道です。そのうえで、デバッグの一時停止が邪魔なら例外設定を見直す、という順番が安全です。

運用を楽にする設計:必須キー一覧を固定し、ログの粒度を揃える

「Referencecodeが無いと困る」という話は、実は“JSONスキーマ(項目定義)が曖昧”という構造問題になっていることが多いです。現場で効く対策は次の3つです。

  • 必須キー/任意キーを一覧化し、コードにも反映する(必須はGetNamed〜+例外、任意はHasKey/既定値)
  • ログは「受信元」「MessageId」「欠如キー」「例外メッセージ」を揃えて出す(後追い調査が劇的に楽になる)
  • オペレータ表示は短く、復旧行動が分かる文面にする(“担当者へ連絡”“再送”など)

例えば、オペレータ向けには下記のように「何が起きたか+どうすればよいか」をセットにするのが効果的です。

  • 例:受信データに必須項目Referencecodeがありません。送信元装置の設定/バージョンを確認してください。
  • 例:数量項目OrderQuantityの形式が不正です(数値ではありません)。送信元と項目定義を確認してください。

よくある落とし穴(ハマりどころを先に潰す)

  • キー名のスペル/大小文字:WinRTのキー検索は文字列一致です。ReferencecodeとReferenceCodeは別物です。
  • キー名に空白が入っている:意図せず全角/半角スペースが混ざると別キー扱いになります。テストでわざとスペースを混ぜるのは良い検証ですが、実運用では“送信元が空白を入れる事故”も起こり得ます。
  • 数値が文字列で来る:GetNamedNumberで落ちます。送信元の実装言語やシリアライザ変更で起きやすいです。
  • nullの扱い:nullはStringでもNumberでもありません。JsonValueTypeの確認を入れると原因が明確になります。
  • 例外メッセージが英語:ex.message()は環境依存です。運用表示は日本語テンプレ+キー名にするのが無難です。

まとめ

  • CStringに入ったJSONは、winrt::to_hstringとJsonObject::TryParseでC++/WinRTのJsonObjectに変換できます。
  • GetNamedString/GetNamedNumberでキー欠如・型不一致が起きた場合、投げられるのは標準例外ではなくwinrt::hresult_errorです。
  • 「必須キーが無い=即エラー」にしたいなら、GetNamed〜を素で呼んでcatch(winrt::hresult_error)でオペレータ通知するのが分かりやすい設計です。
  • 例外を避けたい(欠損を許容したい)場合は、HasKeyや既定値付きGetNamed〜で分岐するほうが実務では扱いやすいです。
  • 現場では「どのキーで落ちたか」を出す工夫(currentキー名の保持、必須/任意の明文化、ログ項目の統一)が、保守コストを大きく下げます。

この記事を書いた人

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

コメント

コメントする

目次