クライアントから ASP.NET Core MVC や Web API のアクションメソッドに送信されてくる JSON 文字列の構造が不定の場合、どのように受け取って処理できるかという話を書きます。(.NET Core 3.x 以降の話です。.NET Framework および .NET Core 2.x 以前は未検証・未確認です)
(注: 以下は MVC のアクションメソッドを例に取って書いていますが、Web API のアクションメソッドでもモデルバインディングの関係は全く同じです)
クライアントから送信されてくる JSON 文字列の構造が常に同じなら、先の記事「JSON 文字列から C# のクラス定義生成」に書いたような手段で C# のクラス定義を生成し、それをアクションメソッドの引数に設定してやれば、フレームワーク組み込みのモデルバインダが JSON 文字列を C# のオブジェクトにデシリアライズしてバインドしてくれます。
しかし、JSON 文字列の構造が不定の場合は C# のクラス定義ができません。それでも ASP.NET のフレームワークがモデルバインディングしてくれるようにするにはどのようにしたらいいでしょう?
構造が不定とは言っても必要な情報のある JSON 文字列 {"name" : "value"} の name は事前に分かっているのであれば、それに該当する value はJsonElement オブジェクトとして取得できます(詳しくは先の記事「JSON 文字列から指定した name の value を取得」を見てください)。それで目的が果たせるのであればその方向で進めるのが良さそうです。
クライアントから送信されてきた構造不定の JSON 文字列を、アクションメソッドでどのように受け取って JsonElement オブジェクトにデシリアライズするかですが、それはフレームワーク組み込みのモデルバインダが自動的に行ってくれます。
具体例は下のサンプルコードの Full アクションメソッドを見てください。JsonElement 型の引数を設定するだけで、自動的にクライアントから送信されてきた JSON 文字列をデシリアライズしてバインドしてくれます。
using Microsoft.AspNetCore.Mvc;
using MvcCore5App4.Models;
using System.Text.Json;
namespace MvcCore5App4.Controllers
{
public class JsonController : Controller
{
[HttpPost]
public IActionResult Partial([FromBody] Rootobject postedObject)
{
JsonElement element = postedObject.Response.Result.ObjectArray;
return Content(element.ToString());
}
[HttpPost]
public IActionResult Full([FromBody] JsonElement postedObject)
{
JsonElement? element = FindElementByName(postedObject, "ObjectArray");
string returnValue = (element != null) ?
element.Value.ToString() : "ObjectArray は取得できません";
return Content(returnValue);
}
private JsonElement? FindElementByName(JsonElement jelem, string name)
{
if (jelem.ValueKind == JsonValueKind.Object)
{
foreach (JsonProperty jprop in jelem.EnumerateObject())
{
if (jprop.Name == name)
{
return jprop.Value;
}
else
{
JsonElement? retVal = FindElementByName(jprop.Value, name);
if (retVal != null)
{
return retVal;
}
}
}
}
else if (jelem.ValueKind == JsonValueKind.Array)
{
foreach (JsonElement jelemInArray in jelem.EnumerateArray())
{
JsonElement? retVal = FindElementByName(jelemInArray, name);
if (retVal != null)
{
return retVal;
}
}
}
else
{
return null;
}
return null;
}
}
}
送信した JSON 文字列のサンプルは以下の通りです。先の記事「JSON 文字列から指定した name の value を取得」に書いたものと同じです。
{
"Title": "This is my title",
"Response": {
"Version": 1,
"StatusCode": "OK",
"Result": {
"Profile": {
"UserName": "SampleUser",
"IsMobileNumberVerified": false,
"MobilePhoneNumber": null
},
"ObjectArray" : [
{"Code": 2000,"Description": "Fail"},
{"Code": 3000,"Description": "Success"}
],
"lstEnrollment": "2021-2-5"
},
"Message": {
"Code": 1000,
"Description": "OK"
}
},
"StringArray" : ["abc", "def", "ghi"]
}
JSON 文字列まるまる全部の構造が不定なわけではなく、特定の項目だけが不定の場合は別の方法があります。例えば、上の JSON 文字列の中で "ObjectArray" の value のみが不定だとします。
その場合は、上の JSON 文字列から Visual Studio の機能を利用して生成したクラス定義の中の ObjectArray プロパティの型を JsonElement に書き換えてやります。具体的には以下の通りです。
using System.Text.Json;
namespace MvcCore5App4.Models
{
// Visual Studio を利用して JSON 文字列から生成したクラス定義
public class Rootobject
{
public string Title { get; set; }
public Response Response { get; set; }
public string[] StringArray { get; set; }
}
public class Response
{
public int Version { get; set; }
public string StatusCode { get; set; }
public Result Result { get; set; }
public Message Message { get; set; }
}
public class Result
{
public Profile Profile { get; set; }
// ObjectArray の value の構造が不定ということで書き換え
//public Objectarray[] ObjectArray { get; set; }
public JsonElement ObjectArray { get; set; }
public string lstEnrollment { get; set; }
}
public class Profile
{
public string UserName { get; set; }
public bool IsMobileNumberVerified { get; set; }
public object MobilePhoneNumber { get; set; }
}
// ObjectArray クラスの定義は不要なのでコメントアウト
//public class Objectarray
//{
// public int Code { get; set; }
// public string Description { get; set; }
//}
public class Message
{
public int Code { get; set; }
public string Description { get; set; }
}
}
上の Rootobject クラスをアクションメソッドの引数に設定してやれば、ObjectArray プロパティには JsonElement 型、それ以外は上の定義に指定した通りの型にデシリアライズしてくれます。
そのアクションメソッドの具体例は上のコントローラのサンプルコードの Partial アクションメソッドを見てください。アクションメソッドの実行結果が上の画像です。