ASP.NET Core Web API における Json パッチ

この記事では、ASP.NET Core Web API において JSON パッチ要求を処理する方法について説明します。

パッケージ インストール

ASP.NET Core Web API での JSON パッチのサポートは、Newtonsoft.Json に基づいており、Microsoft.AspNetCore.Mvc.NewtonsoftJson NuGet パッケージが必要です。 JSON パッチのサポートを有効 にするには:

  • Microsoft.AspNetCore.Mvc.NewtonsoftJson NuGet パッケージをインストールします。

  • AddNewtonsoftJson を呼び出します。 次に例を示します。

    var builder = WebApplication.CreateBuilder(args);
    
    builder.Services.AddControllers()
        .AddNewtonsoftJson();
    
    var app = builder.Build();
    
    app.UseHttpsRedirection();
    
    app.UseAuthorization();
    
    app.MapControllers();
    
    app.Run();
    

AddNewtonsoftJson では、すべてのJSON コンテンツの書式設定に使用される既定の System.Text.Json ベースの入力フォーマッタと出力フォーマッタが置換されます。 この拡張メソッドは、次の MVC サービス登録メソッドと互換性があります。

JsonPatch では、Content-Type ヘッダーを application/json-patch+json に設定する必要があります。

System.Text.Json を使用する場合の JSON パッチのサポートを追加する

System.Text.Json ベースの入力フォーマッタは JSON パッチをサポート していません。 他の入力フォーマッタと出力フォーマッタを変更せずに、Newtonsoft.Json を使用して JSON パッチのサポートを追加するには:

  • Microsoft.AspNetCore.Mvc.NewtonsoftJson NuGet パッケージをインストールします。

  • 以下を更新します。Program.cs:

    using JsonPatchSample;
    using Microsoft.AspNetCore.Mvc.Formatters;
    
    var builder = WebApplication.CreateBuilder(args);
    
    builder.Services.AddControllers(options =>
    {
        options.InputFormatters.Insert(0, MyJPIF.GetJsonPatchInputFormatter());
    });
    
    var app = builder.Build();
    
    app.UseHttpsRedirection();
    
    app.UseAuthorization();
    
    app.MapControllers();
    
    app.Run();
    
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Mvc.Formatters;
    using Microsoft.Extensions.Options;
    
    namespace JsonPatchSample;
    
    public static class MyJPIF
    {
        public static NewtonsoftJsonPatchInputFormatter GetJsonPatchInputFormatter()
        {
            var builder = new ServiceCollection()
                .AddLogging()
                .AddMvc()
                .AddNewtonsoftJson()
                .Services.BuildServiceProvider();
    
            return builder
                .GetRequiredService<IOptions<MvcOptions>>()
                .Value
                .InputFormatters
                .OfType<NewtonsoftJsonPatchInputFormatter>()
                .First();
        }
    }
    

上記のコードでは、NewtonsoftJsonPatchInputFormatter のインスタンスを作成し、MvcOptions.InputFormatters コレクションの最初のエントリとして挿入します。 この登録順序により、次のことが保証されます。

  • NewtonsoftJsonPatchInputFormatter は JSON パッチ要求を処理 します。
  • 既存の System.Text.Json ベースの入力フォーマッタと出力フォーマッタは、他のすべての JSON 要求と応答を処理します。

Newtonsoft.Json.JsonConvert.SerializeObject メソッドを使用して、JsonPatchDocument をシリアル化します。

PATCH HTTP 要求メソッド

PUT および PATCH メソッドは、既存のリソースを更新するために使用されます。 両者の違いは、PUT ではリソース全体を置き換えるのに対して、PATCH では変更箇所だけを指定することです。

JSON パッチ

JSON パッチは、リソースに適用される更新を指定するための形式です。 JSON パッチ ドキュメントには、操作の配列が含まれます。 各操作により、特定の種類の変更が識別されます。 このような変更の例としては、配列要素の追加やプロパティ値の置き換えがあります。

たとえば、次の JSON ドキュメントは、1 つのリソースとそのリソースに対応する JSON パッチ ドキュメント、およびパッチ操作を適用した結果を表しています。

リソースの例

{
  "customerName": "John",
  "orders": [
    {
      "orderName": "Order0",
      "orderType": null
    },
    {
      "orderName": "Order1",
      "orderType": null
    }
  ]
}

JSON パッチの例

[
  {
    "op": "add",
    "path": "/customerName",
    "value": "Barry"
  },
  {
    "op": "add",
    "path": "/orders/-",
    "value": {
      "orderName": "Order2",
      "orderType": null
    }
  }
]

上記の JSON の場合:

  • op プロパティでは、操作の種類を指示します。
  • path プロパティでは、更新する要素を指示します。
  • value プロパティでは、新しい値を指定します。

パッチ後のリソース

上記の JSON パッチ ドキュメントを適用した後のリソースを、以下に示します。

{
  "customerName": "Barry",
  "orders": [
    {
      "orderName": "Order0",
      "orderType": null
    },
    {
      "orderName": "Order1",
      "orderType": null
    },
    {
      "orderName": "Order2",
      "orderType": null
    }
  ]
}

JSON パッチ ドキュメントをリソースに適用することによって行われた変更はアトミックです。 リスト内のいずれかの操作が失敗した場合、リスト内のどの操作も適用されません。

パス構文

操作オブジェクトの path プロパティでは、レベル間にスラッシュを保持します。 たとえば、「 "/address/zipCode" 」のように入力します。

0 から始まるインデックスは、配列の要素を指定するために使用されます。 addresses 配列の最初の要素は、/addresses/0 にあります。 配列の末尾への add では、インデックス番号ではなく、/addresses/- のようにハイフン (-) を使用します。

操作

次の表は、JSON パッチの仕様に定義されている、サポートされる操作を示しています。

操作 メモ
add プロパティまたは配列要素を追加します。 既存のプロパティの場合: 値を設定します。
remove プロパティまたは配列要素を削除します。
replace remove の後に、同じ場所で add が続く場合と同じです。
move ソースからの remove の後に、ソースからの値を使用した宛先への add が続く場合と同じです。
copy ソースからの値を使用した宛先への add と同じです。
test path の値が指定された valueと一致する場合に、成功の状態コードを返します。

ASP.NET Core 内の JSON パッチ

JSON パッチの ASP.NET Core 実装は、Microsoft.AspNetCore.JsonPatch NuGet パッケージ内に提供されています。

アクション メソッド コード

API コントローラーにおける JSON パッチ用のアクション メソッド:

次に例を示します。

[HttpPatch]
public IActionResult JsonPatchWithModelState(
    [FromBody] JsonPatchDocument<Customer> patchDoc)
{
    if (patchDoc != null)
    {
        var customer = CreateCustomer();

        patchDoc.ApplyTo(customer, ModelState);

        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        return new ObjectResult(customer);
    }
    else
    {
        return BadRequest(ModelState);
    }
}

サンプル アプリからのこのコードでは、以下の Customer モデルを利用します。

namespace JsonPatchSample.Models;

public class Customer
{
    public string? CustomerName { get; set; }
    public List<Order>? Orders { get; set; }
}
namespace JsonPatchSample.Models;

public class Order
{
    public string OrderName { get; set; }
    public string OrderType { get; set; }
}

同じアクション メソッド:

  • Customer を構築します。
  • パッチを適用します
  • 応答の本文内で結果を返します。

実際のアプリでは、コードはデータベースなどの保存場所からデータを取得し、パッチを適用した後にデータベースを更新します。

モデルの状態

前のアクション メソッドの例では、パラメーターの 1 つとしてモデルの状態を取得する ApplyTo のオーバーロードを呼び出しています。 このオプションを利用すると、応答内にエラー メッセージを取得できます。 次の例では、test 操作に対する 400 Bad Request 応答の本文を示しています。

{
  "Customer": [
    "The current value 'John' at path 'customerName' != test value 'Nancy'."
  ]
}

動的オブジェクト

次のアクション メソッドの例では、動的オブジェクトにパッチを適用する方法を示しています。

[HttpPatch]
public IActionResult JsonPatchForDynamic([FromBody]JsonPatchDocument patch)
{
    dynamic obj = new ExpandoObject();
    patch.ApplyTo(obj);

    return Ok(obj);
}

追加の操作

  • path が配列要素を参照する場合: path によって指定された要素の前に新しい要素を挿入します。
  • path がプロパティを参照する場合: プロパティ値を設定します。
  • path が存在しない場所を参照する場合:
    • パッチへのリソースが動的オブジェクトの場合: プロパティを追加します。
    • パッチへのリソースが静的オブジェクトの場合: 要求は失敗します。

次のサンプル パッチ ドキュメントでは、CustomerName の値を設定して、Order オブジェクトを Orders 配列の末尾に追加します。

[
  {
    "op": "add",
    "path": "/customerName",
    "value": "Barry"
  },
  {
    "op": "add",
    "path": "/orders/-",
    "value": {
      "orderName": "Order2",
      "orderType": null
    }
  }
]

削除の操作

  • path が配列要素を参照する場合: 配列を削除します。
  • path がプロパティを参照する場合:
    • パッチへのリソースが動的オブジェクトの場合: プロパティを削除します。
    • パッチへのリソースが静的オブジェクトの場合:
      • プロパティが null 値を許容する場合: null を設定します。
      • プロパティが null 値を許容しない場合: default<T> を設定します。

次のサンプル パッチ ドキュメントでは、CustomerName に null を設定し、Orders[0] を削除します。

[
  {
    "op": "remove",
    "path": "/customerName"
  },
  {
    "op": "remove",
    "path": "/orders/0"
  }
]

置換の操作

この操作は、add が後に続く remove と機能的に同じです。

次のサンプル パッチ ドキュメントでは、CustomerName の値を設定して、Orders[0] を新しい Order オブジェクトに置き換えます。

[
  {
    "op": "replace",
    "path": "/customerName",
    "value": "Barry"
  },
  {
    "op": "replace",
    "path": "/orders/0",
    "value": {
      "orderName": "Order2",
      "orderType": null
    }
  }
]

移動の操作

  • path が配列要素を参照する場合: from 要素を path 要素の場所にコピーしてから、from 要素に対して remove 操作を実行します。
  • path がプロパティを参照する場合: from プロパティの値を path プロパティにコピーしてから、from プロパティに対して remove 操作を実行します。
  • path が存在しないプロパティを参照する場合:
    • パッチへのリソースが静的オブジェクトの場合: 要求は失敗します。
    • パッチへのリソースが動的オブジェクトの場合: from プロパティを pathによって指示された場所にコピーしてから、from プロパティに対して remove 操作を実行します。

次のサンプル パッチ ドキュメントでは、以下の操作を行います。

  • Orders[0].OrderName の値を CustomerName にコピーします。
  • Orders[0].OrderName に null を設定します。
  • Orders[1]Orders[0] の前に移動します。
[
  {
    "op": "move",
    "from": "/orders/0/orderName",
    "path": "/customerName"
  },
  {
    "op": "move",
    "from": "/orders/1",
    "path": "/orders/0"
  }
]

コピー操作

この操作は、最後の remove 手順がない move 操作と機能的に同じです。

次のサンプル パッチ ドキュメントでは、以下の操作を行います。

  • Orders[0].OrderName の値を CustomerName にコピーします。
  • Orders[1] のコピーを Orders[0] の前に挿入します。
[
  {
    "op": "copy",
    "from": "/orders/0/orderName",
    "path": "/customerName"
  },
  {
    "op": "copy",
    "from": "/orders/1",
    "path": "/orders/0"
  }
]

テストの操作

path によって指示された場所にある値が value に指定されている値と異なる場合、要求は失敗します。 その場合は、パッチ ドキュメントにあるその他すべての操作が成功したとしても、PATCH 要求全体が失敗します。

test 操作は、同時実行の競合がある場合に、一般的に更新を防ぐために使用されます。

CustomerName の初期値が "John" の場合、テストは失敗するため、次のサンプル パッチ ドキュメントは無効です。

[
  {
    "op": "test",
    "path": "/customerName",
    "value": "Nancy"
  },
  {
    "op": "add",
    "path": "/customerName",
    "value": "Barry"
  }
]

コードを取得する

サンプル コードを表示またはダウンロードします。 (ダウンロード方法)。

サンプルをテストするには、アプリを実行して、次の設定を使って HTTP 要求を送信します。

  • URL: http://localhost:{port}/jsonpatch/jsonpatchwithmodelstate
  • HTTP メソッド: PATCH
  • ヘッダー: Content-Type: application/json-patch+json
  • 本文: JSON プロジェクト フォルダーから JSON パッチ ドキュメント サンプルの 1 つをコピーして貼り付けます。

その他のリソース

この記事では、ASP.NET Core Web API において JSON パッチ要求を処理する方法について説明します。

パッケージ インストール

ご使用のアプリで JSON パッチのサポートを有効にするには、次の手順を実行します。

  1. Microsoft.AspNetCore.Mvc.NewtonsoftJson NuGet パッケージをインストールします。

  2. プロジェクトの Startup.ConfigureServices メソッドを更新して、AddNewtonsoftJson を呼び出します。 次に例を示します。

    services
        .AddControllersWithViews()
        .AddNewtonsoftJson();
    

AddNewtonsoftJson は MVC サービス登録メソッドと互換性があります。

JSON パッチ、AddNewtonsoftJson、System.Text.Json

AddNewtonsoftJson では、すべてのJSON コンテンツの書式設定に使用される System.Text.Json ベースの入力フォーマッタと出力フォーマッタが置換されます。 他のフォーマッタを変更しないまま、Newtonsoft.Json を使用して JSON パッチのサポートを追加するには、プロジェクトの Startup.ConfigureServices メソッドを次のように更新します。

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews(options =>
    {
        options.InputFormatters.Insert(0, GetJsonPatchInputFormatter());
    });
}

private static NewtonsoftJsonPatchInputFormatter GetJsonPatchInputFormatter()
{
    var builder = new ServiceCollection()
        .AddLogging()
        .AddMvc()
        .AddNewtonsoftJson()
        .Services.BuildServiceProvider();

    return builder
        .GetRequiredService<IOptions<MvcOptions>>()
        .Value
        .InputFormatters
        .OfType<NewtonsoftJsonPatchInputFormatter>()
        .First();
}

上のコードでは、Microsoft.AspNetCore.Mvc.NewtonsoftJson パッケージと次の using ステートメントが必要です。

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using System.Linq;

Newtonsoft.Json.JsonConvert.SerializeObject メソッドを使用して、JsonPatchDocument をシリアル化します。

PATCH HTTP 要求メソッド

PUT および PATCH メソッドは、既存のリソースを更新するために使用されます。 両者の違いは、PUT ではリソース全体を置き換えるのに対して、PATCH では変更箇所だけを指定することです。

JSON パッチ

JSON パッチは、リソースに適用される更新を指定するための形式です。 JSON パッチ ドキュメントには、操作の配列が含まれます。 各操作により、特定の種類の変更が識別されます。 このような変更の例としては、配列要素の追加やプロパティ値の置き換えがあります。

たとえば、次の JSON ドキュメントは、1 つのリソースとそのリソースに対応する JSON パッチ ドキュメント、およびパッチ操作を適用した結果を表しています。

リソースの例

{
  "customerName": "John",
  "orders": [
    {
      "orderName": "Order0",
      "orderType": null
    },
    {
      "orderName": "Order1",
      "orderType": null
    }
  ]
}

JSON パッチの例

[
  {
    "op": "add",
    "path": "/customerName",
    "value": "Barry"
  },
  {
    "op": "add",
    "path": "/orders/-",
    "value": {
      "orderName": "Order2",
      "orderType": null
    }
  }
]

上記の JSON の場合:

  • op プロパティでは、操作の種類を指示します。
  • path プロパティでは、更新する要素を指示します。
  • value プロパティでは、新しい値を指定します。

パッチ後のリソース

上記の JSON パッチ ドキュメントを適用した後のリソースを、以下に示します。

{
  "customerName": "Barry",
  "orders": [
    {
      "orderName": "Order0",
      "orderType": null
    },
    {
      "orderName": "Order1",
      "orderType": null
    },
    {
      "orderName": "Order2",
      "orderType": null
    }
  ]
}

JSON パッチ ドキュメントをリソースに適用することによって行われた変更はアトミックです。 リスト内のいずれかの操作が失敗した場合、リスト内のどの操作も適用されません。

パス構文

操作オブジェクトの path プロパティでは、レベル間にスラッシュを保持します。 たとえば、「 "/address/zipCode" 」のように入力します。

0 から始まるインデックスは、配列の要素を指定するために使用されます。 addresses 配列の最初の要素は、/addresses/0 にあります。 配列の末尾への add では、インデックス番号ではなく、/addresses/- のようにハイフン (-) を使用します。

操作

次の表は、JSON パッチの仕様に定義されている、サポートされる操作を示しています。

操作 メモ
add プロパティまたは配列要素を追加します。 既存のプロパティの場合: 値を設定します。
remove プロパティまたは配列要素を削除します。
replace remove の後に、同じ場所で add が続く場合と同じです。
move ソースからの remove の後に、ソースからの値を使用した宛先への add が続く場合と同じです。
copy ソースからの値を使用した宛先への add と同じです。
test path の値が指定された valueと一致する場合に、成功の状態コードを返します。

ASP.NET Core 内の JSON パッチ

JSON パッチの ASP.NET Core 実装は、Microsoft.AspNetCore.JsonPatch NuGet パッケージ内に提供されています。

アクション メソッド コード

API コントローラーにおける JSON パッチ用のアクション メソッド:

  • HttpPatch 属性によって注釈されます。
  • 通常は [FromBody] を利用して、JsonPatchDocument<T> を受け入れます。
  • パッチ ドキュメント上の ApplyTo を呼び出して、変更を適用します。

次に例を示します。

[HttpPatch]
public IActionResult JsonPatchWithModelState(
    [FromBody] JsonPatchDocument<Customer> patchDoc)
{
    if (patchDoc != null)
    {
        var customer = CreateCustomer();

        patchDoc.ApplyTo(customer, ModelState);

        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        return new ObjectResult(customer);
    }
    else
    {
        return BadRequest(ModelState);
    }
}

サンプル アプリからのこのコードでは、以下の Customer モデルを利用します。

using System.Collections.Generic;

namespace JsonPatchSample.Models
{
    public class Customer
    {
        public string CustomerName { get; set; }
        public List<Order> Orders { get; set; }
    }
}
namespace JsonPatchSample.Models
{
    public class Order
    {
        public string OrderName { get; set; }
        public string OrderType { get; set; }
    }
}

同じアクション メソッド:

  • Customer を構築します。
  • パッチを適用します
  • 応答の本文内で結果を返します。

実際のアプリでは、コードはデータベースなどの保存場所からデータを取得し、パッチを適用した後にデータベースを更新します。

モデルの状態

前のアクション メソッドの例では、パラメーターの 1 つとしてモデルの状態を取得する ApplyTo のオーバーロードを呼び出しています。 このオプションを利用すると、応答内にエラー メッセージを取得できます。 次の例では、test 操作に対する 400 Bad Request 応答の本文を示しています。

{
    "Customer": [
        "The current value 'John' at path 'customerName' is not equal to the test value 'Nancy'."
    ]
}

動的オブジェクト

次のアクション メソッドの例では、動的オブジェクトにパッチを適用する方法を示しています。

[HttpPatch]
public IActionResult JsonPatchForDynamic([FromBody]JsonPatchDocument patch)
{
    dynamic obj = new ExpandoObject();
    patch.ApplyTo(obj);

    return Ok(obj);
}

追加の操作

  • path が配列要素を参照する場合: path によって指定された要素の前に新しい要素を挿入します。
  • path がプロパティを参照する場合: プロパティ値を設定します。
  • path が存在しない場所を参照する場合:
    • パッチへのリソースが動的オブジェクトの場合: プロパティを追加します。
    • パッチへのリソースが静的オブジェクトの場合: 要求は失敗します。

次のサンプル パッチ ドキュメントでは、CustomerName の値を設定して、Order オブジェクトを Orders 配列の末尾に追加します。

[
  {
    "op": "add",
    "path": "/customerName",
    "value": "Barry"
  },
  {
    "op": "add",
    "path": "/orders/-",
    "value": {
      "orderName": "Order2",
      "orderType": null
    }
  }
]

削除の操作

  • path が配列要素を参照する場合: 配列を削除します。
  • path がプロパティを参照する場合:
    • パッチへのリソースが動的オブジェクトの場合: プロパティを削除します。
    • パッチへのリソースが静的オブジェクトの場合:
      • プロパティが null 値を許容する場合: null を設定します。
      • プロパティが null 値を許容しない場合: default<T> を設定します。

次のサンプル パッチ ドキュメントでは、CustomerName に null を設定し、Orders[0] を削除します。

[
  {
    "op": "remove",
    "path": "/customerName"
  },
  {
    "op": "remove",
    "path": "/orders/0"
  }
]

置換の操作

この操作は、add が後に続く remove と機能的に同じです。

次のサンプル パッチ ドキュメントでは、CustomerName の値を設定して、Orders[0] を新しい Order オブジェクトに置き換えます。

[
  {
    "op": "replace",
    "path": "/customerName",
    "value": "Barry"
  },
  {
    "op": "replace",
    "path": "/orders/0",
    "value": {
      "orderName": "Order2",
      "orderType": null
    }
  }
]

移動の操作

  • path が配列要素を参照する場合: from 要素を path 要素の場所にコピーしてから、from 要素に対して remove 操作を実行します。
  • path がプロパティを参照する場合: from プロパティの値を path プロパティにコピーしてから、from プロパティに対して remove 操作を実行します。
  • path が存在しないプロパティを参照する場合:
    • パッチへのリソースが静的オブジェクトの場合: 要求は失敗します。
    • パッチへのリソースが動的オブジェクトの場合: from プロパティを pathによって指示された場所にコピーしてから、from プロパティに対して remove 操作を実行します。

次のサンプル パッチ ドキュメントでは、以下の操作を行います。

  • Orders[0].OrderName の値を CustomerName にコピーします。
  • Orders[0].OrderName に null を設定します。
  • Orders[1]Orders[0] の前に移動します。
[
  {
    "op": "move",
    "from": "/orders/0/orderName",
    "path": "/customerName"
  },
  {
    "op": "move",
    "from": "/orders/1",
    "path": "/orders/0"
  }
]

コピー操作

この操作は、最後の remove 手順がない move 操作と機能的に同じです。

次のサンプル パッチ ドキュメントでは、以下の操作を行います。

  • Orders[0].OrderName の値を CustomerName にコピーします。
  • Orders[1] のコピーを Orders[0] の前に挿入します。
[
  {
    "op": "copy",
    "from": "/orders/0/orderName",
    "path": "/customerName"
  },
  {
    "op": "copy",
    "from": "/orders/1",
    "path": "/orders/0"
  }
]

テストの操作

path によって指示された場所にある値が value に指定されている値と異なる場合、要求は失敗します。 その場合は、パッチ ドキュメントにあるその他すべての操作が成功したとしても、PATCH 要求全体が失敗します。

test 操作は、同時実行の競合がある場合に、一般的に更新を防ぐために使用されます。

CustomerName の初期値が "John" の場合、テストは失敗するため、次のサンプル パッチ ドキュメントは無効です。

[
  {
    "op": "test",
    "path": "/customerName",
    "value": "Nancy"
  },
  {
    "op": "add",
    "path": "/customerName",
    "value": "Barry"
  }
]

コードを取得する

サンプル コードを表示またはダウンロードします。 (ダウンロード方法)。

サンプルをテストするには、アプリを実行して、次の設定を使って HTTP 要求を送信します。

  • URL: http://localhost:{port}/jsonpatch/jsonpatchwithmodelstate
  • HTTP メソッド: PATCH
  • ヘッダー: Content-Type: application/json-patch+json
  • 本文: JSON プロジェクト フォルダーから JSON パッチ ドキュメント サンプルの 1 つをコピーして貼り付けます。

その他のリソース