読者です 読者をやめる 読者になる 読者になる

xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

ASP.NET MVCでCamel CaseなJSONを出力する

ASP.NET MVCでWebアプリを開発しているときに非同期でサーバーからJSONを取得するのはよくあることだと思います。このとき、サーバー側は標準機能のJsonResultを利用してJSONの生成を行うのが定石ですが、.NETのクラスインスタンスのプロパティから名前と値をリフレクションで取得して自動で生成するため、(命名規則に従っていれば) JSONに出力されるプロパティ名はPascal Caseになります。以下のような感じです。

public class JsonController : Controller
{
    public ActionResult Sample()
    {
        return this.Json(new
        {
            FamilyName = "Anders",
            FirstName  = "Hejlsberg",
            Age        = 53
        }, JsonRequestBehavior.AllowGet);
    }
}

//--- 出力結果
// { "FamilyName": "Anders", "FirstName": "Hejlsberg", "Age": 53 }

クライアントサイドの開発は大抵JavaScriptになりますが、こちらはCamel Caseが一般的な命名規則になっています。そのためインピーダンスミスマッチが発生して少々扱い辛い、というか気持ち悪い感じになります。JSONを何とかCamel Caseとして返すことでこれを解決します。

前準備

アクションメソッドから返すJsonResultをCamel Case対応のものにするため、JsonResultを継承した専用クラスを作成します。

public class CamelCaseJsonResult : JsonResult
{
    //--- CamelCase形式でJSONをシリアライズするための設定を保持 (何度も作るの勿体ないからキャッシュ)
    private static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
    {
        ContractResolver = new CamelCasePropertyNamesContractResolver(),
        Converters       = new List<JsonConverter>{ new StringEnumConverter() }
    };

    //--- JSONの出力方法を変更 (基本的にはMVC標準の機能をコピペ)
    public override void ExecuteResult(ControllerContext context)
    {
        if (context == null)
            throw new ArgumentNullException("context");

        if (this.JsonRequestBehavior == JsonRequestBehavior.DenyGet)
        if (string.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
            throw new InvalidOperationException("GET request not allowed");

        var response             = context.HttpContext.Response;
        response.ContentEncoding = this.ContentEncoding ?? response.ContentEncoding;
        response.ContentType     = string.IsNullOrEmpty(this.ContentType)
                                 ? "application/json"
                                 : this.ContentType;
        if (this.Data != null)
            response.Write(JsonConvert.SerializeObject(this.Data, Settings));
    }
}

MVC標準の処理をコピペして、JSONシリアライズする部分だけJson.NETのCamel Case変換機能を使うようにして置き換えます。

方法.1 : Jsonメソッドをオーバーライドする

残作業は先のCamelCaseJsonResultをアクションメソッドの戻り値にすることです。ひとつ目の方法はControllerクラスのJsonメソッドをオーバーライドする方法です。引数をそのままCamelCaseJsonResultクラスのプロパティに流し込んで返すだけで非常に簡単です。

public class JsonController : Controller
{
    protected override JsonResult Json(object data, string contentType, Encoding contentEncoding, JsonRequestBehavior behavior)
    {
        return new CamelCaseJsonResult()
        {
            Data                = data,
            ContentType         = contentType,
            ContentEncoding     = contentEncoding,
            JsonRequestBehavior = behavior,
        };
    }

    public ActionResult Sample()
    {
        //--- 省略 (何も実装を変える必要がない!)
    }
}

//--- 出力
// { "familyName": "Anders", "firstName": "Hejlsberg", "age": 53 }

これのメリットは既存のアクションメソッドを一切触ることなく変更できることです。CamelCaseJsonControllerなどを作り、コントローラーの基底クラスとしてしまっても良いかもしれません。

方法.2 : 拡張メソッドを作る

先の方法ではコントローラー毎にJsonメソッドをオーバーライドするか、もしくはコントローラーの基底クラスを変更する必要がありました。毎回オーバーライドするのはもちろんNGですし、基底クラスにするのは依存関係が強過ぎるのでイヤです。ということで拡張メソッドを作って対応しましょう。

public static class ControllerExtensions
{
    public static JsonResult CamelCaseJson(this Controller controller, object data)
    {
        return controller.CamelCaseJson(data, null, null, JsonRequestBehavior.DenyGet);
    }

    public static JsonResult CamelCaseJson(this Controller controller, object data, JsonRequestBehavior behavior)
    {
        return controller.CamelCaseJson(data, null, null, behavior);
    }

    public static JsonResult CamelCaseJson(this Controller controller, object data, string contentType)
    {
        return controller.CamelCaseJson(data, contentType, null, JsonRequestBehavior.DenyGet);
    }

    public static JsonResult CamelCaseJson(this Controller controller, object data, string contentType, Encoding contentEncoding)
    {
        return controller.CamelCaseJson(data, contentType, contentEncoding, JsonRequestBehavior.DenyGet);
    }

    public static JsonResult CamelCaseJson(this Controller controller, object data, string contentType, JsonRequestBehavior behavior)
    {
        return controller.CamelCaseJson(data, contentType, null, JsonRequestBehavior.DenyGet);
    }

    public static JsonResult CamelCaseJson(this Controller controller, object data, string contentType, Encoding contentEncoding, JsonRequestBehavior behavior)
    {
        return new CamelCaseJsonResult()
        {
            Data                = data,
            ContentType         = contentType,
            ContentEncoding     = contentEncoding,
            JsonRequestBehavior = behavior,
        };
    }
}

上記のようにJsonメソッドオーバーロードと同じだけのオーバーロードを用意してあげると親切だと思います。利用する際はメソッド名を変更するだけです。非常にお手軽にCamel Case対応ができますね。

public class JsonController : Controller
{
    public ActionResult Sample()
    {
        return this.CamelCaseJson(new
        {
            FamilyName = "Anders",
            FirstName  = "Hejlsberg",
            Age        = 53
        }, JsonRequestBehavior.AllowGet);
    }
}

//--- 出力
// { "familyName": "Anders", "firstName": "Hejlsberg", "age": 53 }