みなさんこんにちは!ヒロポンです!!
今回は業務系の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仲間いたら、どんどんシェア待ってるぜ!!
執筆者
バイブス父さん — 業務 SE 7 年 (正社員 2 / フリーランス 5)。 現職は SEO 直轄部の AI アドバイザー兼 PL、 副業で中小 SIer の CTO。 SES 複数社・フリーランスエージェント複数経由の経験ベースで「業務 SE 視点」 の技術 + キャリア記事を書いています。
🐦 X: @hiro_progra0524 (日々の現場メモ更新中)
📝 About Me で経歴詳細を見る


コメント