外部の Web API から JSON を取得して C# のクラスにデシリアライズし、呼び出し側から安全かつシンプルに扱いたい――というのは、.NET アプリ開発で非常に頻出のテーマです。本記事では、ありがちな HttpClient の使い方の落とし穴から、DI・IHttpClientFactory を使ったベストプラクティス、呼び出し側のコード例まで「実務でそのまま使える形」で整理します。
Web API の JSON をクラスにデシリアライズして返したい問題の全体像
今回のテーマをもう少し具体的に整理すると、次のような状況です。
HttpClientで外部 Web API にアクセスして JSON を取得したい- 取得した JSON を
Contact.Rootクラスにデシリアライズして返却したい - メソッド内部でのデシリアライズは動作するが、呼び出し側でどう書けばよいか迷う
- ついでに
HttpClientのベストプラクティスやクラス設計も整理したい
ありがちな「とりあえず動く」コードは次のようなイメージです。
public async Task<Contact.Root> GetContactDataAsync()
{
using var client = new HttpClient();
var json = await client.GetStringAsync("https://api.example.com/contacts/123");
var result = JsonSerializer.Deserialize<Contact.Root>(json);
return result!;
}
確かに動きますが、実務レベルでは以下のような問題があります。
HttpClientを毎回newしていて非効率・危険- 手動で JSON 文字列を扱っており、コードが冗長・例外処理も抜けがち
- 非同期メソッドの呼び出し側の書き方が分かりにくい
- クラス名や責務の分割が曖昧で、メンテナンスが難しくなりやすい
まずはこれらの課題を整理し、どのように改善していくかを俯瞰してみましょう。
よくある実装の課題と改善方針
| 課題 | 解決策・ポイント |
|---|---|
HttpClient を毎回 new しているソケット枯渇・パフォーマンス劣化の原因 | アプリ全体で 1 インスタンスを共有するのが基本 推奨: DI + IHttpClientFactory で管理する 簡易対応: static readonly HttpClient を使い回す |
| JSON を文字列で取得して手作業でデシリアライズ | System.Net.Http.Json の GetFromJsonAsync<T> を使用する 1 行で「HTTP 要求 + デシリアライズ」まで完了する JsonSerializerOptions は 1 回だけ作成し、再利用する |
| クラス名・メソッド名の責務が曖昧 | 「何をするサービスか」が分かる名詞系にする(例: ContactService) Web API 呼び出し専用のサービスクラスに責務を集約する |
| 非同期メソッドの呼び出し方が分からない | 呼び出し元も async にして await で結果を受け取る 同期メソッドから直接 async を呼ばないように構成する |
| 環境ごとに API ベース URL を切り替えたい | HttpClient.BaseAddress を設定ファイルから注入する 開発/検証/本番など、環境ごとに差し替えやすい設計にする |
以降では、これらの課題を一つずつ解消しながら、最終的に「呼び出し側は 1 行で済む」形まで仕上げていきます。
JSON を受け取るクラス設計:Contact.Root を整える
まず、Web API 側から返ってくる JSON の形を明確にしましょう。たとえば以下のような JSON を返す API を想定します。
{
"id": 123,
"name": "山田太郎",
"email": "[email protected]",
"phone": "090-xxxx-xxxx"
}
この JSON に対応する C# クラスは次のように設計できます。
public class Contact
{
public class Root
{
public int Id { get; set; }
public string? Name { get; set; }
public string? Email { get; set; }
public string? Phone { get; set; }
}
}
ポイントは次の通りです。
- JSON のキー名と C# プロパティ名を揃えておくと、設定なしでもデシリアライズが通りやすい
- null の可能性がある項目は
string?としておく(.NET 6+ の nullable 対応) - ネスト構造が複雑なら
Rootの中にさらにクラスを定義していく
つぎに、この Contact.Root を返す Web API 呼び出しクラスを作っていきます。
HttpClient を正しく使い回す設計
HttpClient の使い方を誤ると、ソケット枯渇や DNS キャッシュの問題など、現場では厄介な不具合に繋がります。代表的なパターンを比較してみましょう。
| パターン | 説明 | メリット | デメリット |
|---|---|---|---|
毎回 new HttpClient() | メソッドごとに using var client = new HttpClient(); | コードが短く直感的 | 接続が大量に生成・破棄されソケット枯渇の原因になる パフォーマンスが悪い |
static readonly HttpClient | アプリ全体で 1 つのインスタンスを使い回す | ソケット枯渇を防げる 実装がシンプル | DNS 変更を拾いにくい 設定を変えたい場合に拡張性が低い |
IHttpClientFactory + DI | ASP.NET Core 標準の HttpClient 管理機構を利用 | 接続プールが自動管理される リトライ・タイムアウトなどのポリシーを集中管理できる テスト・モックがしやすい | DI コンテナ前提の構成が必要 |
Web アプリや API サーバのように DI コンテナを使える環境では、基本的に IHttpClientFactory を使うのがおすすめです。コンソールアプリなど最小構成の場合は、まず static HttpClient で使い回し、必要になったら DI を導入する、というステップでも良いでしょう。
GetFromJsonAsync<T> で「取得+デシリアライズ」を 1 行にする
.NET 5 以降では、System.Net.Http.Json 名前空間に便利な拡張メソッドが用意されています。
GetFromJsonAsync<T>ReadFromJsonAsync<T>
これを使うと、次の 2 ステップを 1 行で書けます。
- HTTP GET で JSON を取得する
- JSON を
Contact.Rootにデシリアライズする
using System.Net.Http.Json;
Contact.Root? contact =
await httpClient.GetFromJsonAsync<Contact.Root>("contacts/123");
さらに、JsonSerializerOptions を一度だけ作成し、再利用するのがベストです。
private static readonly JsonSerializerOptions _jsonOptions =
new(JsonSerializerDefaults.Web)
{
// 必要に応じてカスタマイズ
// PropertyNameCaseInsensitive = true,
};
JsonSerializerDefaults.Web を使うことで、Web API の一般的なスタイル(camelCase)に合わせた設定が一括で適用されます。
改善版 ContactService クラスの実装
ここまでの内容を踏まえて、Web API 呼び出しを担当する ContactService クラスを設計します。
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
public class ContactService
{
private readonly HttpClient _client;
// JSON の設定は 1 回だけ作成して使い回す
private static readonly JsonSerializerOptions _options =
new(JsonSerializerDefaults.Web);
// HttpClient は DI などから注入する
public ContactService(HttpClient client)
{
_client = client;
}
/// <summary>
/// Web API から Contact データを取得し、Contact.Root として返す
/// </summary>
public async Task<Contact.Root> GetContactDataAsync(
int id,
CancellationToken token = default)
{
// 例: BaseAddress = https://api.example.com/
string requestUri = $"contacts/{id}";
// HTTP + JSON デシリアライズを 1 行で
Contact.Root? result =
await _client.GetFromJsonAsync<Contact.Root>(
requestUri,
_options,
token
).ConfigureAwait(false);
if (result is null)
{
// API 仕様に応じて例外を投げるか、null 許容にするかを決める
throw new InvalidOperationException(
$"API から期待したデータを取得できませんでした。ID = {id}");
}
return result;
}
}
このクラスのポイントを整理します。
HttpClientはコンストラクタで受け取り、メソッド内ではnewしない- メソッドの責務は「API を叩いて Contact.Root を返すこと」だけに絞る
ConfigureAwait(false)を使い、ライブラリ側でコンテキストの復元をしない- 何らかの理由でデシリアライズが null になった場合は例外として検知しやすくする
最小構成での呼び出し例(DI を使わない場合)
まずは DI や IHttpClientFactory を使わない、最小構成の例を見てみます。
using System;
using System.Net.Http;
using System.Threading.Tasks;
public class Program
{
// アプリ全体で共有する HttpClient
private static readonly HttpClient sharedClient = new()
{
BaseAddress = new Uri("https://api.example.com/")
};
public static async Task Main()
{
var service = new ContactService(sharedClient);
Contact.Root contact = await service.GetContactDataAsync(123);
Console.WriteLine($"ID : {contact.Id}");
Console.WriteLine($"Name : {contact.Name}");
Console.WriteLine($"Mail : {contact.Email}");
Console.WriteLine($"Phone: {contact.Phone}");
}
}
呼び出し側のコードはたったこれだけです。
HttpClientはstatic readonlyで 1 つだけ生成ContactServiceに渡して使うだけ- 利用側は
await service.GetContactDataAsync(123);するだけで、デシリアライズ済みのContact.Rootが手に入る
ここまででも、当初の「メソッド本体でのデシリアライズは動くが、呼び出し側が分からない」という問題はほぼ解決しています。
ASP.NET Core での DI + IHttpClientFactory 利用例
実務では ASP.NET Core など DI が標準な環境で使うケースが多いでしょう。この場合、IHttpClientFactory を使うことで、より安全で柔軟な構成が取れます。
サービス登録(Program.cs)
var builder = WebApplication.CreateBuilder(args);
// ContactService 用の HttpClient(Typed Client)を登録
builder.Services.AddHttpClient<ContactService>(c =>
{
c.BaseAddress = new Uri("https://api.example.com/");
c.DefaultRequestHeaders.Accept.Add(
new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
// タイムアウトなどもここで設定可能
// c.Timeout = TimeSpan.FromSeconds(10);
});
var app = builder.Build();
この設定によって、ASP.NET Core の DI から ContactService を解決すると、自動的に適切に構成された HttpClient が注入されます。
コントローラからの呼び出し例
[ApiController]
[Route("api/[controller]")]
public class ContactsController : ControllerBase
{
private readonly ContactService _contactService;
public ContactsController(ContactService contactService)
{
_contactService = contactService;
}
[HttpGet("{id:int}")]
public async Task<ActionResult<Contact.Root>> Get(int id, CancellationToken token)
{
try
{
var contact = await _contactService.GetContactDataAsync(id, token);
return Ok(contact);
}
catch (HttpRequestException ex)
{
// ネットワークエラーなど
return StatusCode(StatusCodes.Status503ServiceUnavailable, ex.Message);
}
catch (InvalidOperationException ex)
{
// デシリアライズ失敗など
return BadRequest(ex.Message);
}
}
}
コントローラ側からすると、普通のサービスクラスを呼び出す感覚で Web API クライアントを利用できる点が最大のメリットです。
ConfigureAwait(false) の意味と使いどころ
ConfigureAwait(false) は、ライブラリコードを書くときにはほぼ「お約束」のように登場するメソッドです。
- 目的:
await後に「元のコンテキスト(UI スレッドなど)」に戻らないようにする - コンソールアプリや ASP.NET Core など、コンテキストを意識しないアプリと相性が良い
- 余計なコンテキスト切り替えを防ぎ、パフォーマンスとデッドロック回避に役立つ
逆に、WPF / WinForms など UI スレッドが重要なアプリでは、UI レイヤー側(画面コード)では ConfigureAwait(false) を付けずに await し、ライブラリ層(今回の ContactService など)では ConfigureAwait(false) を付ける、という住み分けにすると分かりやすくなります。
環境ごとに API ベース URL を切り替える設計
開発・ステージング・本番などで API の URL が違う、というのはよくある話です。そのたびにコードを書き換えるのではなく、設定から注入できるようにしておきましょう。
設定ファイル(例: appsettings.json)にベース URL を定義
{
"ContactApi": {
"BaseUrl": "https://api.example.com/"
}
}
オプションクラスを作成
public class ContactApiOptions
{
public string BaseUrl { get; set; } = string.Empty;
}
DI への登録と HttpClient 設定
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<ContactApiOptions>(
builder.Configuration.GetSection("ContactApi"));
builder.Services.AddHttpClient<ContactService>((sp, client) =>
{
var options = sp.GetRequiredService<IOptions<ContactApiOptions>>().Value;
client.BaseAddress = new Uri(options.BaseUrl);
});
こうしておけば、環境別の設定ファイル(appsettings.Development.json など)を差し替えるだけで、コードを書き換えずに API の接続先を切り替えられます。
エラー処理と戻り値設計のポイント
Web API 呼び出しでは、ネットワークエラーや API 側のエラー、JSON の仕様変更など、様々な失敗パターンが起こり得ます。GetFromJsonAsync<T> を使う場合でも、次のような点に注意しましょう。
HTTP ステータスコードの扱い
GetFromJsonAsync<T> は、ステータスコードが非成功(4xx/5xx)の場合、HttpRequestException を投げます。そのため、呼び出し側で try-catch して適切にハンドリングする必要があります。
try
{
var contact = await _contactService.GetContactDataAsync(id, token);
// 正常処理
}
catch (HttpRequestException ex)
{
// ログを出力してユーザーにエラーを通知するなど
}
API が空のレスポンスを返した場合
API の仕様によっては、ボディが空だったり null に近いデータを返す場合もあります。先ほどのサンプルでは、result is null の時に InvalidOperationException を投げるようにしましたが、次のようなバリエーションも考えられます。
Task<Contact.Root?>を戻り値にして、null を許容する- 見つからなかった時だけ専用の例外(例:
ContactNotFoundException)を投げる - API 側のエラーコードをラップして返す DTO を設ける
チームのコーディング規約や API の仕様に合わせて選択しましょう。
テストしやすい Web API クライアントにする工夫
ContactService のような Web API クライアントは、単体テストでモック化・スタブ化できるようにしておくと非常に便利です。HttpClient は HttpMessageHandler を差し替えることで擬似レスポンスを返せるため、DI と相性が良い設計になっています。
カスタム HttpMessageHandler の例
public class FakeHttpMessageHandler : HttpMessageHandler
{
private readonly HttpResponseMessage _response;
public FakeHttpMessageHandler(HttpResponseMessage response)
{
_response = response;
}
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
return Task.FromResult(_response);
}
}
単体テストでの利用例
[Fact]
public async Task GetContactDataAsync_正常系()
{
// Arrange
var json = @"{
""id"": 123,
""name"": ""山田太郎"",
""email"": ""[email protected]"",
""phone"": ""090-xxxx-xxxx""
}";
var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json")
};
var handler = new FakeHttpMessageHandler(response);
var httpClient = new HttpClient(handler)
{
BaseAddress = new Uri("https://api.example.com/")
};
var service = new ContactService(httpClient);
// Act
var contact = await service.GetContactDataAsync(123);
// Assert
Assert.Equal(123, contact.Id);
Assert.Equal("山田太郎", contact.Name);
}
このように、HttpClient や ContactService を DI で注入できるようにしておけば、テストコードからはモックのハンドラを差し込むだけで、外部の実 API に依存しない形で動作確認ができます。
UI 層からの呼び出しパターン例
最後に、実際の UI からどのように呼び出すかの具体例をいくつか挙げておきます。
コンソールアプリ
すでに紹介した通り、async Task Main を使えば素直に await できます。
public static async Task Main(string[] args)
{
var service = new ContactService(sharedClient);
var contact = await service.GetContactDataAsync(123);
Console.WriteLine(contact.Name);
}
WPF / WinForms からの呼び出し
ボタンクリックイベントなど UI スレッドから呼び出す場合は、イベントハンドラ自体を async void にして await するのが一般的です。
private readonly ContactService _contactService;
public MainWindow()
{
InitializeComponent();
_contactService = new ContactService(sharedClient);
}
private async void FetchButton_Click(object sender, RoutedEventArgs e)
{
try
{
var contact = await _contactService.GetContactDataAsync(123);
NameTextBlock.Text = contact.Name;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "エラー", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
UI 層では ConfigureAwait(false) を付けずに await しておけば、復帰後も UI スレッド上で実行されるため、安心して UI 要素を更新できます。
まとめ:安全・簡潔・テストしやすい Web API 呼び出しにするために
Web API から取得した JSON をクラスにデシリアライズして返す、という一見シンプルな要件でも、HttpClient のライフサイクル管理や JSON デシリアライズ、非同期処理、DI など、考えるべきポイントは多く存在します。
| 観点 | ポイント |
|---|---|
| HttpClient の扱い | 毎回 new せず、DI または static で使い回す。可能なら IHttpClientFactory を利用。 |
| JSON デシリアライズ | GetFromJsonAsync<T> / ReadFromJsonAsync<T> を活用し、文字列扱いをなくす。 |
| クラス設計 | Web API 呼び出し専用の ContactService を作り、責務を集約する。 |
| 非同期処理 | 呼び出し側も async / await でつなぎ、同期メソッドから直接待ち合わせない。 |
| 設定と環境切り替え | ベース URL はコードにベタ書きせず、設定ファイルや DI で注入する。 |
| テスト性 | HttpMessageHandler を差し替えられる形にし、単体テストで API をモックできるようにする。 |
この記事で紹介したパターンをベースに、自身のプロジェクトの API クライアントを見直してみると、
- コード量が減り、可読性が上がる
- エラーが起きた時の原因切り分けがしやすくなる
- テストや将来の仕様変更にも強くなる
といった効果が期待できます。特に、「HttpClient + JSON デシリアライズ + 非同期メソッド」 の組み合わせはあらゆる Web API 連携の基本形なので、一度しっかりパターン化しておくと、今後の開発効率と品質が大きく向上します。

コメント