ASP.NET CoreでjQuery AjaxのJSON POSTがnullになる原因と解決策(FromBodyとモデルバインディング)

jQueryの$.ajaxでJSONをPOSTしているのに、ASP.NET CoreのController側では引数がnull(または0などの既定値)になる――。旧ASP.NETでは動いていたのに移行後に詰まりやすいこの現象を、モデルバインディングの仕組みから整理し、確実に直る実装例とチェックポイントをまとめます。

目次

起きている現象:JSONをPOSTしているのにControllerで受け取れない

たとえばクライアント側で次のようにJSONを送っているのに、ASP.NET Core(MVC)のアクションに入ると値が入らない、というケースです。

$.ajax({
  url: "/Person/personInfoPost",
  type: "POST",
  contentType: "application/json; charset=utf-8",
  data: JSON.stringify({ name: "Taro", age: 20, SSN: "123-45-6789" }),
  success: function (res) { console.log(res); }
});

そしてサーバー側が次のような受け取り方になっていると、引数が期待どおりにバインドされず、null/既定値になります。

[HttpPost]
public string personInfoPost([FromBody] string name, int age, string SSN)
{
    return $"{name}, {age}, {SSN} are Received ok...";
}

「.NET Frameworkだと動いていたのに、ASP.NET Coreだと動かない」という相談で多いのですが、原因の本質はフレームワークの世代差というより、ASP.NET Coreのモデルバインディング(受け取りルール)を前提にした設計に合わせていないことです。

ASP.NET Coreのモデルバインディングをざっくり理解する

ASP.NET Coreでは、リクエストに含まれる値(URL、クエリ文字列、フォーム、ボディなど)をアクション引数へ割り当てる仕組みをモデルバインディングと呼びます。どこから値を取るかは「型」と「属性」で大きく決まります。

値の場所例よく使う属性向いている用途
Route(URLパス)/person/123[FromRoute]IDなどの必須パラメータ
Query(クエリ文字列)?page=2&sort=desc[FromQuery]検索条件、ページング
Form(フォーム)application/x-www-form-urlencoded[FromForm]一般的なフォーム送信、ファイルアップロード
Body(リクエストボディ)JSON本文[FromBody]APIのJSON入力(DTO/モデル)
Header(ヘッダー)Authorization: …[FromHeader]認証情報、言語設定

ここで重要なのが、JSONを送る場合の「Body」の扱いです。ASP.NET CoreはボディをInput Formatter(JSONならJSONパーサー)で読み取り、アクション引数へ割り当てます。ところがボディは基本的に1回しか読めないストリームです。だから設計上、[FromBody]でボディを読む引数は原則1つというルールになります。

なぜ「[FromBody] string name, int age, string SSN」で失敗するのか

先ほどの受け取り方がうまくいかない理由は、主に次の2点です。

  • [FromBody]は原則として1つの引数だけ
    ボディ(JSON本文)からバインドする引数は基本的に1つだけです。1つ目の引数でボディを消費してしまうため、2つ目以降がボディから読まれることはありません。その結果、age/SSNはボディからは入らず、クエリやルート等に値が無ければ既定値(0やnull)になります。
  • [FromBody] string name はJSONオブジェクトの形と合っていない
    クライアントが送っているのは { name: "...", age: ..., SSN: "..." } という「オブジェクト」です。一方、サーバー側は「文字列1本」を期待しています。文字列にはname/age/SSNというプロパティが無いため、JSONの各項目を割り当てられません。

つまり「JSONをPOSTしているのにnullになる」というより、正確にはJSONの形に合う受け取りモデルを用意していないことが原因です。

解決策:DTO(受信用モデル)を1つ作って[FromBody]で受ける

ASP.NET Coreで最も素直で保守しやすいのは、受信用のDTO(モデル)クラスを作り、それを[FromBody]で受ける方法です。JSONオブジェクトを「1つのC#オブジェクト」にマッピングするイメージです。

サーバー側(Model / Controller)

public class PersonInfoModel
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string SSN { get; set; }
}

[HttpPost]
[Consumes("application/json")]
public IActionResult personInfoPost([FromBody] PersonInfoModel model)
{
// 実運用なら SSN をそのまま返すのは避けましょう(ログやレスポンス漏洩に注意)
var message = $"{model.Name}, {model.Age} are Received ok...";
return Ok(new { message });
}

ポイントは次のとおりです。

  • [FromBody]を付ける引数は1つにする(DTOにまとめる)
  • 必要に応じて [Consumes("application/json")] を付け、意図した入力形式を明確にする
  • クライアントがJSONを期待するなら、返却もJSONとして返す(後述)

なお、API用途であればControllerに [ApiController] を付けると、モデルバインディングやバリデーションの失敗が自動的に400として返り、原因が追いやすくなります。画面用のMVC(Razor)とAPIを混在させる場合は、Controllerを分けると安全です。

クライアント側(jQuery $.ajax)

送信側は次のように、JSON文字列をボディとして送ります。

const payload = {
  name: "Taro",
  age: 20,
  SSN: "123-45-6789"
};

$.ajax({
  url: "/Person/personInfoPost",
  type: "POST",
  contentType: "application/json; charset=utf-8", // ← 送信する中身がJSONであることを宣言
  dataType: "json",                                // ← レスポンスをJSONとして扱う
  data: JSON.stringify(payload),
  success: function (res) {
    console.log(res.message);
  },
  error: function (xhr) {
    console.error(xhr.status, xhr.responseText);
  }
});

ここで地味に重要なのが、jQueryのdataType と contentTypeの違いです。混同すると「送れているのにエラー」「受け取れているのにパースできない」など別の詰まりを呼びやすいので、次の表で整理しておきます。

項目役割今回の推奨値よくある勘違い
contentTypeリクエストのContent-Type(何を送るか)application/json; charset=utf-8レスポンス形式だと思ってしまう
dataTypeレスポンスをどう解釈するか(何が返る想定か)json(サーバーがJSONを返すなら)リクエスト形式だと思ってしまう
data送信する本文(今回はJSON文字列)JSON.stringify(payload)オブジェクトのまま渡してしまい、意図せずクエリ化される
processDatadataをクエリ文字列へ変換するかfalse(dataにオブジェクト等を渡す場合)常に不要だと思い込む/逆に必要な場面で付け忘れる

「dataType: ‘json’」なのに文字列を返すと起きる別トラブル

質問文にもあるとおり、クライアントで dataType: "json" を指定しているのに、サーバーが文字列を返していると、成功時にパースエラーが起きることがあります(jQueryはJSONとして解釈しようとするため)。受信null問題とは別ですが、移行時に一緒に踏み抜きやすいので押さえておきましょう。

  • サーバーが文字列を返すなら、クライアントの dataType は "text" にする
  • クライアントがJSONを期待するなら、サーバーは Ok(new { ... }) などでJSONを返す

現場では後者(JSONで返す)に寄せると、フロント側の型扱い・エラーハンドリングが統一できて運用が楽です。

プロパティ名の命名(camelCase vs PascalCase)で迷ったら

クライアントのJSONは name/age のようにcamelCase、C#のプロパティは Name/Age のようにPascalCase、という組み合わせが多いです。ASP.NET CoreのJSONデシリアライズは設定(利用するシリアライザーやオプション)で挙動が変わるため、以下のどれかで「ズレ」を吸収すると安全です。

  • 命名規則を統一する(JSONはcamelCase、C#はPascalCase、などルールを決める)
  • DTO側で属性を使い、受信名を固定する(ケースや略語の揺れを吸収できる)

たとえば SSN をJSONでは ssn にしたい場合、DTO側で明示的にマッピングできます。

using System.Text.Json.Serialization;

public class PersonInfoModel
{
    public string Name { get; set; }
    public int Age { get; set; }

    [JsonPropertyName("ssn")]
    public string SSN { get; set; }
}

「動いたり動かなかったりする」状態を避けるなら、属性で固定してしまうのが最も事故りにくいです。特に略語(SSN、URL、IDなど)は揺れが出やすいのでおすすめです。

よくあるつまずきとチェックポイント(null問題の周辺も含む)

「nullになる」以外にも、jQuery Ajax × ASP.NET CoreのJSON POSTは似た症状が多いので、現場でよく遭遇するパターンをまとめます。

症状原因の典型対策
アクション引数がnull/0のまま[FromBody]を複数付けている/DTOにまとめていないボディは1モデル(DTO)で受ける
model自体はnullではないが一部プロパティが入らないプロパティ名不一致、大小文字、SSN/ssnの違い命名規則を統一、必要なら属性で固定
400 Bad RequestになるJSONが壊れている、Content-Type不一致、型変換失敗Networkタブで送信ボディを確認、ModelStateを確認
クライアントで「Unexpected token」等のパースエラーdataType:”json”なのにサーバーが文字列/HTMLを返しているサーバーをJSON返却にする or dataTypeを”text”にする
POSTが403/400で弾かれるCSRF(アンチフォージェリ)トークン不足(画面系MVCで多い)トークンをヘッダーに付ける/APIは別設計にする

画面系MVCでの注意:アンチフォージェリ(CSRF)とAjax

Razorビューなど「画面」を返すMVCで [ValidateAntiForgeryToken] を使っている場合、AjaxでJSON POSTするときにもトークンが必要です。トークン不足だと、モデルがnull以前にリクエストが弾かれてしまい「うまく届いていない」と誤認しやすくなります。

一般的には、画面に埋め込まれているトークンを取り出してヘッダーに載せます。

$.ajax({
  url: "/Person/personInfoPost",
  type: "POST",
  contentType: "application/json; charset=utf-8",
  data: JSON.stringify(payload),
  headers: {
    "RequestVerificationToken": $('input[name="__RequestVerificationToken"]').val()
  }
});

APIとして分離できるなら、画面系のCSRF対策と混ぜず、認証(Bearerトークン等)を前提にしたAPI設計に切り替えるのが王道です。移行プロジェクトでは「画面とAPIを分ける」だけでトラブルが一気に減ることも珍しくありません。

どうしても「複数の単純型引数」で受けたい場合の考え方

「DTOを作るほどでもない」「単純型で受けたい」という要望もあります。ただし、JSONボディを前提にするなら、ASP.NET Coreの設計思想的にも運用的にもDTOが無難です。それでも別案が必要なときは、次の方向性で考えます。

案1:送信形式をフォーム(x-www-form-urlencoded)にする

もしJSONにこだわらないなら、従来のフォーム送信に寄せると単純型引数が自然にバインドされます。

$.ajax({
  url: "/Person/personInfoPost",
  type: "POST",
  data: { name: "Taro", age: 20, SSN: "123-45-6789" } // ← JSON.stringifyしない
});
[HttpPost]
public IActionResult personInfoPost(string name, int age, string SSN)
{
    return Ok(new { name, age, SSN });
}

この場合、送信はJSONではなくフォーム相当になるため、APIとしての一貫性は下がります。フロントが増える・外部公開するなど将来性があるならDTO+JSONを推奨します。

案2:JsonElementなどで受けて自前で読む

どうしても固定DTOが作れない(可変なJSONを受ける、検証前に生データを見たいなど)場合は、まずボディを1つの塊として受け、そこから取り出します。

using System.Text.Json;

[HttpPost]
public IActionResult personInfoPost([FromBody] JsonElement body)
{
    var name = body.GetProperty("name").GetString();
    var age = body.GetProperty("age").GetInt32();
    var ssn = body.GetProperty("SSN").GetString();
    return Ok(new { name, age, ssn });
}

この方法は、バリデーションや型安全性を自分で担う必要があり、保守コストが上がりがちです。業務アプリでは「例外対応が多い入力」以外はDTOに寄せるのが結局早いことが多いです。

デバッグ手順:何がどこで消えているかを最短で特定する

「nullになる」問題は、原因がモデルバインディングにあるのか、そもそもリクエストが想定どおり送れていないのかで対処が変わります。次の順序で確認すると迷いにくいです。

  • ブラウザ開発者ツールのNetworkタブで、Request HeadersのContent-Typeと、Request Payload(送信JSON)が期待どおりか確認する
  • サーバー側で、アクションに入っているか(ルーティングが合っているか)を確認する(404ならそもそも別問題)
  • modelがnullか、modelの中身だけ空かを切り分ける(nullならJSONパース以前の可能性が高い)
  • ModelStateにエラーが入っていないか確認する(型変換失敗などが出ます)

DTO受信時にModelStateを確認するだけでも、原因がかなり見えやすくなります。

[HttpPost]
[Consumes("application/json")]
public IActionResult personInfoPost([FromBody] PersonInfoModel model)
{
    if (!ModelState.IsValid)
    {
        // どのフィールドがどう失敗したかを見る
        return BadRequest(ModelState);
    }

    return Ok(new { message = "ok" });
}

「ブラウザだけだと切り分けが難しい」ときは、curlやPostmanで同じJSONを送ってサーバー側が受けられるか確認すると、フロント側の問題と分離できます。

curl -X POST "https://example.com/Person/personInfoPost" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Taro", "age": 20, "SSN": "123-45-6789" }'

移行時に混乱しやすい「旧ASP.NETとの違い」の整理

「.NET Frameworkでは動いた」という話の背景には、旧ASP.NET(MVCとWeb APIが別系統)とASP.NET Core(MVCとAPIが統合)の違いがあります。ASP.NET Coreでは入力の扱いがより明確に整理され、JSONボディは1つのモデルとして受ける設計が基本になりました。移行の際は、動いていたコードをそのまま持ってくるのではなく、受信モデルを中心に再設計するとトラブルが減ります。

チーム開発のコツ:最小再現(ミニマムサンプル)を残す

この手の「なぜかnullになる」問題は、環境差・設定差・ミドルウェア差で再現性がぶれやすいのが厄介です。そこでおすすめなのが、原因を掴んだタイミングで最小再現のサンプルを作って残すことです。

  • DTOとControllerだけの最小APIを作る
  • jQuery(またはcurl)の送信例をセットで置く
  • 「何を変えると壊れるか(例:[FromBody]を複数にしたらNG)」をコメントで残す

社内のGitHubやリポジトリに置いておけば、新メンバーが同じ罠にハマったときも即座に答え合わせができます。移行期の“学び”を資産化できるので、結果的に工数が下がります。

まとめ:ASP.NET CoreのJSON POSTは「ボディ=DTO1つ」が基本

jQueryのAjaxでJSON POSTをしているのにController側でnullになる問題は、ASP.NET Coreのモデルバインディングのルールに起因することがほとんどです。ポイントは次の3つに集約できます。

  • JSONのボディは原則として[FromBody] 1引数で受ける
  • 受け取る形はDTO(モデル)にまとめる(単純型を並べない)
  • jQuery側はcontentType(送信)とdataType(受信)を揃える

この型とルールを押さえるだけで、「ASP.NET CoreでAjax JSON POSTがnullになる」という移行時の定番トラブルはほぼ解消できます。あとは命名規則やCSRFなど周辺要因をチェックし、再発しない形に整えていきましょう。

この記事を書いた人

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

コメント

コメントする

目次