みなさんこんにちは!ヒロポンです!!
今回は業務系の C# でガチで踏みやすいやつ!!の話。
「外部 API のレスポンス JSON をデシリアライズしたら、DateTime がローカル時刻に化けてズレた」「decimal で扱ってた金額が double に化けて0.1円単位でズレた」「JSON のプロパティ名がcustomerName(CamelCase)で C# クラスは CustomerName(PascalCase)にしてたら全部 null で来た」みたいな業務系 JSON 処理の事故って、API 連携やったことある業務SEなら誰しも一回はやらかしますよね??
俺も2社目くらいの流通系SIer時代に、決済 API の amount フィールドが decimal のはずが double 化けで0.1円ズレ て、月次の集計で経理から「合計が合わない」って指摘されて夕方の運用報告で気付いて半日デバッガで追ってハマった事件をやらかしました。原因は完全に JObject 経由で (double)jobj["amount"] してた箇所で、(decimal) キャストに変えただけで解決した。
この記事では VS2019 / .NET Framework 4.7.2 / C# 7.3 環境で、Newtonsoft.Json を使った業務系 JSON 処理の 6つの定石(基本シリアライズ / JsonSerializerSettings / DateTime UTC 統一 / カスタム JsonConverter / JsonTextReader ストリーミング / decimal 化け回避)を、コード7本でまとめます。後ろの「現場メモ」で、業務系チームでルール化した時の話も書いてる。
3行で結論:
- .NET Framework 4.7.2 は Newtonsoft.Json 一択(System.Text.Json は Core 系・実用は Newtonsoft)
- 業務系のベース設定は
DateTimeZoneHandling.Utc+IsoDateFormat+NullValueHandling.IgnoreをJsonSerializerSettingsに固定- 金額・数量の
decimalは型付きクラスに直接デシリアライズ(JObject経由は double 化けの温床)
定石1: NuGet で Newtonsoft.Json を入れる + 最小コード
まずインストール。VS2019 のパッケージマネージャコンソールで:
PM> Install-Package Newtonsoft.Json
最小のシリアライズ・デシリアライズはこんな感じ:
// ✅ 定石1: 最小シリアライズ・デシリアライズ
using Newtonsoft.Json;
public class OrderDto
{
public int OrderId { get; set; }
public string CustomerName { get; set; }
public decimal Amount { get; set; }
public DateTime CreatedAt { get; set; }
}
// シリアライズ(オブジェクト → JSON 文字列)
var order = new OrderDto
{
OrderId = 1001,
CustomerName = "サンプル商事",
Amount = 12500m,
CreatedAt = DateTime.UtcNow,
};
string json = JsonConvert.SerializeObject(order);
// → {"OrderId":1001,"CustomerName":"サンプル商事","Amount":12500.0,"CreatedAt":"2026-05-09T05:00:00Z"}
// デシリアライズ(JSON 文字列 → オブジェクト)
string responseJson = HttpClient.GetString(apiUrl);
var parsed = JsonConvert.DeserializeObject<OrderDto>(responseJson);
Console.WriteLine($"{parsed.OrderId}: {parsed.CustomerName} {parsed.Amount}円");
ポイント:
JsonConvert.SerializeObject(obj)でオブジェクト → 文字列JsonConvert.DeserializeObject<T>(json)で文字列 → 型付きオブジェクト- プロパティ名は 大文字小文字含めて完全一致(既定)
- 既定の挙動だと
DateTimeがローカル時刻で出力される罠あり(次の定石で対処)
ここまでが最小。業務系では次の JsonSerializerSettings でベース設定を固定するのが本命っす。
定石2: JsonSerializerSettings で業務系ベース設定を固定
業務系 API 連携で最初に設定したい挙動を JsonSerializerSettings にまとめておきます:
// ✅ 定石2: 業務系ベース設定(プロジェクト全体で使い回す)
public static class JsonConfig
{
public static readonly JsonSerializerSettings BusinessDefault = new JsonSerializerSettings
{
// DateTime は UTC 統一(ローカル化事故を予防)
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
DateFormatHandling = DateFormatHandling.IsoDateFormat,
// null プロパティは出力しない(API レスポンスを軽くする)
NullValueHandling = NullValueHandling.Ignore,
// decimal を double にしない(金額計算の精度保持)
FloatParseHandling = FloatParseHandling.Decimal,
// CamelCase に統一(外部 API で多数派)
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver(),
// 循環参照を無視(DataTable / EF エンティティでよく踏む)
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
};
}
// 使う側
string json = JsonConvert.SerializeObject(order, JsonConfig.BusinessDefault);
var parsed = JsonConvert.DeserializeObject<OrderDto>(json, JsonConfig.BusinessDefault);
業務系チームに1個置いておくだけで、JSON 周りの挙動が揃います。新規 API 連携を実装する人が JsonConvert.SerializeObject(obj) を裸で呼ぶと既定挙動でブレるので、設定セットを共有資産化するのが鉄則っす。
ん?こんな感じの設定で大丈夫なん??って思うかもだけど、業務系の API 連携ではこの組み合わせが多数派。社内 API で別の規約があるなら ContractResolver を差し替えたり、NullValueHandling を Include に変えたりで、プロジェクト単位でカスタマイズします。
定石3: DateTime の UTC 統一で時刻ズレを潰す
ここが業務SEで一番踏みやすい罠っす。Newtonsoft.Json の DateTime 既定挙動は RoundtripKind で、Kind=Local の DateTime はローカル時刻でシリアライズされて受け側でズレる:
// ❌ NG: 既定設定で Kind=Local をシリアライズすると、ローカル時刻が出力される
var bad = new { CreatedAt = DateTime.Now }; // Kind=Local
string badJson = JsonConvert.SerializeObject(bad);
// → {"CreatedAt":"2026-05-09T14:00:00+09:00"} ← JST 環境のローカル時刻
// ✅ OK: DateTimeZoneHandling.Utc で UTC 強制
var settings = new JsonSerializerSettings
{
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
DateFormatHandling = DateFormatHandling.IsoDateFormat,
};
var good = new { CreatedAt = DateTime.UtcNow }; // Kind=Utc
string goodJson = JsonConvert.SerializeObject(good, settings);
// → {"CreatedAt":"2026-05-09T05:00:00Z"} ← UTC + Z 表記
業務系の API 連携・ログ出力・設定ファイルでは、DateTimeZoneHandling.Utc + IsoDateFormat のセットを既定にしておくのがいい感じに事故予防になります。DateTime の Kind 周りの背景は別記事 C# DateTime と DateTimeOffset の違い・タイムゾーン処理の正解 で詳しく書いたので、Kind=Unspecified の罠が気になる人はそっちと併せて押さえておくと早いです。
定石4: カスタム JsonConverter — 業務固有型の専用変換
業務系で「金額型は3桁カンマ区切り表示・社内コードは特殊フォーマット」のような独自の変換ロジックが必要な場面では、JsonConverter を継承してカスタム実装します:
// ✅ 定石4: 金額用カスタム JsonConverter(書き出し時に円マーク付与など)
public class MoneyJsonConverter : JsonConverter<decimal>
{
public override void WriteJson(JsonWriter writer, decimal value, JsonSerializer serializer)
{
writer.WriteValue(value); // 数値で書き出し(業務系では文字列化しない方が後段が楽)
}
public override decimal ReadJson(JsonReader reader, Type objectType,
decimal existingValue, bool hasExistingValue, JsonSerializer serializer)
{
// 数値・文字列どちらでも受けられるように寛容実装
if (reader.TokenType == JsonToken.Integer || reader.TokenType == JsonToken.Float)
return Convert.ToDecimal(reader.Value);
if (reader.TokenType == JsonToken.String)
return decimal.Parse((string)reader.Value);
throw new JsonSerializationException($"Unexpected token type: {reader.TokenType}");
}
}
// 使い方: クラスのプロパティに [JsonConverter] 属性で適用
public class InvoiceDto
{
public int InvoiceId { get; set; }
[JsonConverter(typeof(MoneyJsonConverter))]
public decimal Amount { get; set; }
}
JsonConverter のメリット:
- 業務固有の型変換ロジックを1箇所に集約(プロジェクト全体で再利用)
- 入力寛容・出力厳格のような業務系 API 連携の典型パターンが書ける
- 既存クラスを汚さずに変換ルールだけ差し替えられる
業務系の社内コード型(CustomerCode / ProductCode 等)も同じパターンで JsonConverter を作れば、JSON ⇔ 型付きオブジェクトの境界が綺麗に分離できる。業務系チームで型ライブラリを整えていく時の本命パターンっす。
定石5: JsonTextReader でストリーミング読み込み
巨大 JSON(10万件超)を一括 DeserializeObject<List<T>> するとメモリが膨れるので、JsonTextReader で1要素ずつ読むパターン:
// ✅ 定石5: JsonTextReader で巨大 JSON をストリーミング読み込み
using (var fs = new FileStream(jsonFilePath, FileMode.Open, FileAccess.Read))
using (var sr = new StreamReader(fs))
using (var reader = new JsonTextReader(sr))
{
var serializer = new JsonSerializer
{
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
};
// 配列の開始 [ までスキップ
while (reader.Read() && reader.TokenType != JsonToken.StartArray) { }
// 配列内の各要素を1件ずつデシリアライズ
while (reader.Read())
{
if (reader.TokenType == JsonToken.StartObject)
{
var item = serializer.Deserialize<OrderDto>(reader);
// 1件ずつ処理(CSV 書き出し・DB INSERT・別 API 転送 など)
ProcessOneOrder(item);
}
else if (reader.TokenType == JsonToken.EndArray)
{
break;
}
}
}
DataReader のストリーミング読み取りと同じ発想で、メモリは1件分しか使わないのがメリット。10万件・100万件規模の JSON ファイル読み込みや、ETL バッチでの中間データ処理で本命のパターンっす。
定石6: decimal が double に化ける問題と対策
業務系の本番事故ワースト級がこれ。JObject 経由でアクセスすると、JSON の数値が double 解釈されて精度が落ちます:
// ❌ NG: JObject 経由で double 化け(金額計算が事故る)
string apiResponse = "{\"amount\": 12500.10}";
var jobj = Newtonsoft.Json.Linq.JObject.Parse(apiResponse);
double amountDouble = (double)jobj["amount"]; // 12500.099999999... の罠
decimal amountFromDouble = (decimal)amountDouble; // 既に double 化けしてる
// ✅ OK 1: JObject から直接 decimal キャスト
decimal amountDirect = (decimal)jobj["amount"]; // 正確に 12500.10
// ✅ OK 2: FloatParseHandling.Decimal 設定
var settings = new JsonSerializerSettings
{
FloatParseHandling = FloatParseHandling.Decimal,
};
string s = "[12500.10, 998.50, 0.1]";
var values = JsonConvert.DeserializeObject<decimal[]>(s, settings);
// ✅ OK 3: 型付きクラスに直接デシリアライズ(業務系の本命)
public class PaymentDto { public decimal Amount { get; set; } }
var payment = JsonConvert.DeserializeObject<PaymentDto>(apiResponse);
// → payment.Amount は 12500.10 のまま、化けない
業務系の鉄則:
- 金額・税率・数量のような
decimal計算が必要なフィールドは型付きクラスに直接デシリアライズ - どうしても
JObject経由で取る時は、(decimal)キャストを忘れない((double)経由は禁忌) - グローバル設定なら
FloatParseHandling.DecimalをJsonSerializerSettingsに追加
俺の本番事故事例(決済 API の月次集計0.1円ズレ)も、原因は完全に(double)jobj["amount"] で書いてた箇所。型付きクラスに書き換えただけで解決した。金額系の API 連携は型付きクラス寄せを業務系チーム規約に入れておくのが事故予防になります。
ハマりポイント — 実体験ベースの本番事故3点
1. decimal が double 化けて月次集計0.1円ズレ(半日デバッガで追ってハマった)
決済 API のレスポンスから金額を取る箇所で (double)jobj["amount"] と書いてたせいで、月次集計が経理から「合計が0.1円ズレてる」って指摘された事件。夕方の運用報告で気付いて半日デバッガで追ってハマった末に (decimal) キャストに変えて解決。それ以来、業務系チームで「金額系 API は型付きクラス寄せ・JObject 経由禁止」をルール化しました。
2. DateTime ローカル化で API 連携先と時刻ズレ(30分溶かした)
外部 API に DateTime.Now でログを送ったら、受け側がそれを「自分のローカル時刻」と解釈して時刻が9時間ズレる事件。30分溶かした末に、DateTimeZoneHandling.Utc + DateFormatHandling.IsoDateFormat を JsonSerializerSettings に追加して解決。DateTime.UtcNow 寄せ + UTC 強制シリアライズを業務系チーム規約に揃えた。
3. プロパティ名の大文字小文字違いで全部 null(夕方の運用報告で気づいた)
外部 API のレスポンスが customerName(CamelCase)で C# クラスが CustomerName(PascalCase)だったが、JsonSerializerSettings に ContractResolver を入れてなくてデシリアライズ結果が全プロパティ nullになる事件。夕方の運用報告で「画面に何も出ない」って報告で気づいた。CamelCasePropertyNamesContractResolver を共通設定に追加して解決。それ以来、業務系チームで JsonSerializerSettings を共通資産化する運用に変えました。
著者の現場メモ — 業務系チームでの JSON 処理ルール
流通系SIer時代に、過去コードを grep -rn "JsonConvert\|JObject" . でひっかけたら、90箇所近く 出てきたんですよね。書き方がバラバラで、JsonConvert.SerializeObject(obj) を裸で呼んでて DateTime ローカル化してる箇所、JObject 経由で (double) キャストしてる箇所、ContractResolver なしで CamelCase API と連携してる箇所、全部入り。
んで、後輩と一緒に 3行ルール にまとめた:
JsonSerializerSettingsを共通資産化(プロジェクト全体で同じインスタンスを使い回す)- 金額系・数量系は型付きクラス寄せ(
JObject経由で(double)キャストは禁忌) DateTimeはUtcNow寄せ +DateTimeZoneHandling.Utc強制(API 連携の時刻ズレ事故予防)
このルール化で、JSON 周りの本番事故が消えた。書き方を1パターンに揃えるだけで保守工数も事故率も両方下がるので、業務系チームには結構おすすめのルールっす。
C# 7.3 + .NET Framework 4.7.2 の業務系って、Newtonsoft.Json は10年以上 API が変わってないのに、プロジェクトごとに設定がバラバラなコードベースが本当に多い。書き方のアップデートだけで保守工数が下がる実例だと思ってます。
まとめ
| 状況 | 推奨パターン |
|---|---|
| .NET Framework 4.7.2 環境 | Newtonsoft.Json 一択(System.Text.Json は Core 系) |
| 共通設定 | JsonSerializerSettings を共通資産化 |
DateTime 統一 |
DateTimeZoneHandling.Utc + IsoDateFormat |
| 金額・数量計算 | 型付きクラスに直接デシリアライズ |
| 業務固有型 | カスタム JsonConverter で変換ロジック分離 |
| 巨大 JSON 読み込み | JsonTextReader ストリーミング |
| プロパティ名規約 | CamelCasePropertyNamesContractResolver or [JsonProperty] |
| 循環参照 | ReferenceLoopHandling.Ignore |
業務系の JSON 処理事故は、「設定を共通化」「金額は型付き」「DateTime は UTC」 の3点で9割消えます。JsonConvert.SerializeObject(obj) を裸で呼ぶ運用は事故の温床なので、プロジェクト全体で JsonSerializerSettings を1箇所にまとめるのが業務SEの現実解です。
よくある質問
Q1. .NET Framework 4.7.2 で System.Text.Json は使えますか?
A. 実用的には使えないと思っておくのが安全です。System.Text.Json は .NET Standard 2.0 / .NET Core 2.0+ から提供されていますが、.NET Framework 4.7.2 環境では NuGet で参照できても性能・安定性のテストが薄く、業務系本番では Newtonsoft.Json 一択が現実解。Microsoft の公式ガイドも .NET Framework 系は Newtonsoft.Json を継続推奨しています。
Q2. decimal が double に化けて金額がズレるのを防ぐには?
A. JObject 経由や dynamic 受けでアクセスすると、JSON の数値が double に解釈されて精度が落ちる事故が起きます。金額・税率・数量のような decimal 計算が必要なフィールドは、(1)型付きクラスに直接デシリアライズする、(2)JObject から取る時は (decimal)jobj["amount"] のように明示キャストする、(3)FloatParseHandling.Decimal を JsonSerializerSettings に設定する、の3つで防げます。業務系の金額計算は型付きクラス寄せが鉄則です。
Q3. DateTime のローカル / UTC を Newtonsoft で正しく扱うには?
A. JsonSerializerSettings の DateTimeZoneHandling を Utc に固定するのが安全です。既定値は RoundtripKind で、Kind=Local の DateTime はローカル時刻でシリアライズされて受け側でズレる事故が起きやすい。DateTimeZoneHandling = DateTimeZoneHandling.Utc + DateFormatHandling = DateFormatHandling.IsoDateFormat のセットで、ISO 8601 + UTC 形式(例: 2026-05-09T05:00:00Z)に統一できます。詳しい背景は別記事「C# DateTime と DateTimeOffset の違い」を参照してください。
Q4. 10万件規模の JSON を読み込んでもメモリが膨れないようにするには?
A. JsonTextReader でストリーミング読み込みします。JsonConvert.DeserializeObject<List<T>>(json) は全件をメモリにロードするのでメモリ消費が大きいですが、JsonTextReader で1要素ずつ読み進めれば メモリ消費は最小に抑えられます。DataReader のストリーミング読み取りと同じ発想で、巨大 JSON を1行ずつ処理する CSV 出力・帳票生成に使うのが業務系の本命です。
Q5. JSON のプロパティ名が CamelCase で C# クラスが PascalCase の時は?
A. [JsonProperty("customerName")] 属性で個別マッピングするか、JsonSerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver() で全体に CamelCase 規約を適用します。業務系で外部 API(多くは CamelCase)と連携する場合、ContractResolver で一括設定が楽。社内 API で大文字小文字混在の場合は属性で個別指定するのが現実的です。
ここまでで Newtonsoft.Json の業務系定石・落とし穴・現場ルール化は押さえた。DateTime / 性能 / DataTable JSON 化の隣接トピックも貼っておきます。
関連記事
- C# DateTime と DateTimeOffset の違い・タイムゾーン処理の正解(業務SE本番事故編) — JSON シリアライズ前の
DateTime.Kind制御を整える時に効く - C# 文字列結合のパフォーマンス完全比較(+ / Concat / StringBuilder / Format / 補間) — JSON 出力で
StringBuilderを使う場面に効く - C# DataTable を LINQ でフィルタ・GroupBy・分割する3パターン —
DataTableを JSON 化する前段処理に効く
以上!
同じ罠でハマってる業務SE仲間いたら、どんどんシェア待ってるぜ!!


コメント