この記事では、ASP.NET Core Web API において JSON パッチ要求を処理する方法について説明します。
ASP.NET Core Web API での JSON パッチのサポートは、 System.Text.Json シリアル化に基づいており、 Microsoft.AspNetCore.JsonPatch.SystemTextJson
NuGet パッケージが必要です。
JSON パッチ標準とは
JSON パッチ標準:
JSON ドキュメントに適用する変更を記述するための標準形式です。
RFC 6902 で定義されており、JSON リソースの部分的な更新を実行するために RESTful API で広く使用されています。
次のような JSON ドキュメントを変更する一連の操作について説明します。
add
remove
replace
move
copy
test
Web アプリでは、JSON Patch は、一般的に PATCH 操作でリソースの部分的な更新を実行するために使用されます。 クライアントは、更新プログラムのリソース全体を送信するのではなく、変更のみを含む JSON パッチ ドキュメントを送信できます。 修正プログラムを適用すると、ペイロードのサイズが減少し、効率が向上します。
JSON パッチ標準の概要については、 jsonpatch.com を参照してください。
ASP.NET Core Web API での JSON パッチのサポート
ASP.NET Core Web API での JSON Patch のサポートは、.NET 10 以降のSystem.Text.Jsonシリアル化に基づき、Microsoft.AspNetCore.JsonPatchシリアル化に基づくSystem.Text.Jsonを実装します。 This feature:
-
Microsoft.AspNetCore.JsonPatch.SystemTextJson
NuGet パッケージが必要です。 - .NET 用に最適化された System.Text.Json ライブラリを利用して、最新の .NET プラクティスに合わせて調整します。
- 従来の
Newtonsoft.Json
ベースの実装と比較して、パフォーマンスが向上し、メモリ使用量が削減されます。 従来のNewtonsoft.Json
ベースの実装の詳細については、 この記事の .NET 9 バージョンを参照してください。
Note
Microsoft.AspNetCore.JsonPatchシリアル化に基づくSystem.Text.Jsonの実装は、従来のNewtonsoft.Json
ベースの実装に代わるものではありません。
ExpandoObjectなど、動的な型はサポートされていません。
Important
JSON Patch 標準には固有の セキュリティ リスクがあります。 これらのリスクは JSON Patch 標準に固有であるため、ASP.NET Core の実装 では固有のセキュリティ リスクを軽減しようとはしません。 JSON パッチ ドキュメントがターゲット オブジェクトに安全に適用されるようにするのは開発者の責任です。 詳細については、「 セキュリティ リスクの軽減」セクションを 参照してください。
で JSON パッチのサポートを有効にする System.Text.Json
System.Text.Jsonで JSON Patch のサポートを有効にするには、Microsoft.AspNetCore.JsonPatch.SystemTextJson
NuGet パッケージをインストールします。
dotnet add package Microsoft.AspNetCore.JsonPatch.SystemTextJson --prerelease
このパッケージには、JsonPatchDocument<TModel>型のオブジェクトの JSON Patch ドキュメントを表すT
クラスと、System.Text.Jsonを使用して JSON Patch ドキュメントをシリアル化および逆シリアル化するためのカスタム ロジックが用意されています。
JsonPatchDocument<TModel> クラスのキー メソッドはApplyTo(Object)であり、T
型のターゲット オブジェクトにパッチ操作を適用します。
JSON パッチを適用するアクション メソッド コード
API コントローラーにおける JSON パッチ用のアクション メソッド:
- HttpPatchAttribute 属性によって注釈されます。
- 通常は JsonPatchDocument<TModel> を利用して、FromBodyAttribute を受け入れます。
- パッチ ドキュメント上の ApplyTo(Object) を呼び出して、変更を適用します。
コントローラー アクション メソッドの例:
[HttpPatch("{id}", Name = "UpdateCustomer")]
public IActionResult Update(AppDb db, string id, [FromBody] JsonPatchDocument<Customer> patchDoc)
{
// Retrieve the customer by ID
var customer = db.Customers.FirstOrDefault(c => c.Id == id);
// Return 404 Not Found if customer doesn't exist
if (customer == null)
{
return NotFound();
}
patchDoc.ApplyTo(customer, jsonPatchError =>
{
var key = jsonPatchError.AffectedObject.GetType().Name;
ModelState.AddModelError(key, jsonPatchError.ErrorMessage);
}
);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
return new ObjectResult(customer);
}
サンプル アプリのこのコードは、次の Customer
モデルと Order
モデルで動作します。
namespace App.Models;
public class Customer
{
public string Id { get; set; }
public string? Name { get; set; }
public string? Email { get; set; }
public string? PhoneNumber { get; set; }
public string? Address { get; set; }
public List<Order>? Orders { get; set; }
public Customer()
{
Id = Guid.NewGuid().ToString();
}
}
namespace App.Models;
public class Order
{
public string Id { get; set; }
public DateTime? OrderDate { get; set; }
public DateTime? ShipDate { get; set; }
public decimal TotalAmount { get; set; }
public Order()
{
Id = Guid.NewGuid().ToString();
}
}
サンプル アクション メソッドの主要な手順は次のとおりです。
-
顧客を取得します。
- このメソッドは、指定された ID を使用して、データベース
Customer
からAppDb
オブジェクトを取得します。 -
Customer
オブジェクトが見つからない場合は、404 Not Found
応答を返します。
- このメソッドは、指定された ID を使用して、データベース
-
JSON パッチの適用:
-
ApplyTo(Object) メソッドは、patchDoc から取得した
Customer
オブジェクトに JSON Patch 操作を適用します。 - 無効な操作や競合など、パッチ アプリケーション中にエラーが発生した場合は、エラー処理デリゲートによってキャプチャされます。 このデリゲートは、影響を受けるオブジェクトの型名とエラー メッセージを使用して、
ModelState
にエラー メッセージを追加します。
-
ApplyTo(Object) メソッドは、patchDoc から取得した
-
Validate ModelState:
- 修正プログラムを適用した後、メソッドはエラーの
ModelState
をチェックします。 - 修正プログラムのエラーなど、
ModelState
が無効な場合は、検証エラーを含む400 Bad Request
応答が返されます。
- 修正プログラムを適用した後、メソッドはエラーの
-
更新された顧客を返します。
- 修正プログラムが正常に適用され、
ModelState
が有効な場合、メソッドは応答で更新されたCustomer
オブジェクトを返します。
- 修正プログラムが正常に適用され、
エラー応答の例:
次の例は、指定したパスが無効な場合の JSON パッチ操作の 400 Bad Request
応答の本文を示しています。
{
"Customer": [
"The target location specified by path segment 'foobar' was not found."
]
}
JSON パッチ ドキュメントをオブジェクトに適用する
次の例では、 ApplyTo(Object) メソッドを使用して JSON Patch ドキュメントをオブジェクトに適用する方法を示します。
例: オブジェクトに JsonPatchDocument<TModel> を適用する
その具体的な例を次に示します:
-
add
、replace
、およびremove
操作。 - 入れ子になったプロパティに対する操作。
- 配列への新しい項目の追加。
- JSON パッチ ドキュメントでの JSON 文字列列挙型コンバーターの使用。
// Original object
var person = new Person {
FirstName = "John",
LastName = "Doe",
Email = "johndoe@gmail.com",
PhoneNumbers = [new() {Number = "123-456-7890", Type = PhoneNumberType.Mobile}],
Address = new Address
{
Street = "123 Main St",
City = "Anytown",
State = "TX"
}
};
// Raw JSON patch document
string jsonPatch = """
[
{ "op": "replace", "path": "/FirstName", "value": "Jane" },
{ "op": "remove", "path": "/Email"},
{ "op": "add", "path": "/Address/ZipCode", "value": "90210" },
{ "op": "add", "path": "/PhoneNumbers/-", "value": { "Number": "987-654-3210",
"Type": "Work" } }
]
""";
// Deserialize the JSON patch document
var patchDoc = JsonSerializer.Deserialize<JsonPatchDocument<Person>>(jsonPatch);
// Apply the JSON patch document
patchDoc!.ApplyTo(person);
// Output updated object
Console.WriteLine(JsonSerializer.Serialize(person, serializerOptions));
前の例では、更新されたオブジェクトの出力が次のようになります。
{
"firstName": "Jane",
"lastName": "Doe",
"address": {
"street": "123 Main St",
"city": "Anytown",
"state": "TX",
"zipCode": "90210"
},
"phoneNumbers": [
{
"number": "123-456-7890",
"type": "Mobile"
},
{
"number": "987-654-3210",
"type": "Work"
}
]
}
ApplyTo(Object)メソッドは、通常、次のオプションによって制御される動作を含め、System.Text.Jsonを処理するためのJsonPatchDocument<TModel>の規則とオプションに従います。
- JsonNumberHandling: 数値プロパティを文字列から読み取るかどうか。
- PropertyNameCaseInsensitive: プロパティ名で大文字と小文字が区別されるかどうか。
System.Text.Jsonと新しいJsonPatchDocument<TModel>実装の主な違い:
- 宣言された型ではなく、ターゲット オブジェクトのランタイム型によって、パッチ ApplyTo(Object) プロパティが決まります。
- System.Text.Json 逆シリアル化は、対象となるプロパティを識別するために宣言された型に依存します。
例: エラー処理を使用して JsonPatchDocument を適用する
JSON パッチ ドキュメントを適用するときに発生する可能性があるさまざまなエラーがあります。 たとえば、ターゲット オブジェクトに指定されたプロパティがない場合や、指定した値がプロパティ型と互換性がない可能性があります。
JSON Patch
では、指定した値がターゲット プロパティと等しいかどうかを確認する test
操作がサポートされています。 そうでない場合は、エラーが返されます。
次の例では、これらのエラーを適切に処理する方法を示します。
Important
ApplyTo(Object) メソッドに渡されたオブジェクトは、インプレースで変更されます。 操作が失敗した場合、呼び出し元は変更を破棄する必要があります。
// Original object
var person = new Person {
FirstName = "John",
LastName = "Doe",
Email = "johndoe@gmail.com"
};
// Raw JSON patch document
string jsonPatch = """
[
{ "op": "replace", "path": "/Email", "value": "janedoe@gmail.com"},
{ "op": "test", "path": "/FirstName", "value": "Jane" },
{ "op": "replace", "path": "/LastName", "value": "Smith" }
]
""";
// Deserialize the JSON patch document
var patchDoc = JsonSerializer.Deserialize<JsonPatchDocument<Person>>(jsonPatch);
// Apply the JSON patch document, catching any errors
Dictionary<string, string[]>? errors = null;
patchDoc!.ApplyTo(person, jsonPatchError =>
{
errors ??= new ();
var key = jsonPatchError.AffectedObject.GetType().Name;
if (!errors.ContainsKey(key))
{
errors.Add(key, new string[] { });
}
errors[key] = errors[key].Append(jsonPatchError.ErrorMessage).ToArray();
});
if (errors != null)
{
// Print the errors
foreach (var error in errors)
{
Console.WriteLine($"Error in {error.Key}: {string.Join(", ", error.Value)}");
}
}
// Output updated object
Console.WriteLine(JsonSerializer.Serialize(person, serializerOptions));
前の例では、次の出力が得られます。
Error in Person: The current value 'John' at path 'FirstName' is not equal
to the test value 'Jane'.
{
"firstName": "John",
"lastName": "Smith", <<< Modified!
"email": "janedoe@gmail.com", <<< Modified!
"phoneNumbers": []
}
セキュリティ リスクの軽減
Microsoft.AspNetCore.JsonPatch.SystemTextJson
パッケージを使用する場合は、潜在的なセキュリティ リスクを理解して軽減することが重要です。 以下のセクションでは、JSON Patch に関連付けられている特定されたセキュリティ リスクについて説明し、パッケージの安全な使用を確保するための推奨される軽減策を提供します。
Important
これは、脅威の完全な一覧ではありません。 アプリ開発者は、独自の脅威モデル レビューを実施して、アプリ固有の包括的な一覧を決定し、必要に応じて適切な軽減策を考え出す必要があります。 たとえば、コレクションをパッチ操作に公開するアプリでは、それらの操作がコレクションの先頭に要素を挿入または削除する場合に、アルゴリズムの複雑さの攻撃の可能性を考慮する必要があります。
JSON パッチ機能をアプリに統合する際のセキュリティ リスクを最小限に抑えるには、開発者は次の作業を行う必要があります。
- 独自のアプリに対して包括的な脅威モデルを実行します。
- 特定された脅威に対処します。
- 次のセクションで推奨される軽減策に従います。
メモリ増幅によるサービス拒否 (DoS)
-
シナリオ: 悪意のあるクライアントが、大きなオブジェクト グラフを複数回複製する
copy
操作を送信すると、メモリが過剰に消費されます。 - 影響: 潜在的なOut-Of-Memory (OOM)状態が発生し、サービスが中断する可能性があります。
-
Mitigation:
- ApplyTo(Object)を呼び出す前に、受信 JSON パッチ ドキュメントのサイズと構造を検証します。
- 検証はアプリ固有である必要がありますが、検証の例は次のようになります。
public void Validate(JsonPatchDocument<T> patch)
{
// This is just an example. It's up to the developer to make sure that
// this case is handled properly, based on the app needs.
if (patch.Operations.Where(op=>op.OperationType == OperationType.Copy).Count()
> MaxCopyOperationsCount)
{
throw new InvalidOperationException();
}
}
ビジネスロジックの覆し
- シナリオ: パッチ操作では、暗黙的なインバリアント (内部フラグ、ID、計算フィールドなど) を持つフィールドを操作でき、ビジネス上の制約に違反します。
- 影響:データ整合性の問題と意図しないアプリの動作。
-
Mitigation:
- 明示的に定義されたプロパティを持ち、変更に安全な POCO (Plain Old CLR Objects) を使用します。
- ターゲット オブジェクトで機密性の高いプロパティまたはセキュリティ クリティカルなプロパティを公開しないようにします。
- POCO オブジェクトが使用されていない場合は、操作の適用後に修正プログラムが適用されたオブジェクトを検証して、ビジネス ルールとインバリアントに違反していないことを確認します。
- 明示的に定義されたプロパティを持ち、変更に安全な POCO (Plain Old CLR Objects) を使用します。
認証と承認
- シナリオ: 認証されていないクライアントまたは未承認のクライアントが、悪意のある JSON パッチ要求を送信します。
- 影響:機密データを変更したり、アプリの動作を中断したりするための未承認のアクセス。
-
Mitigation:
- 適切な認証と承認メカニズムを使用して、JSON パッチ要求を受け入れるエンドポイントを保護します。
- 適切なアクセス許可を持つ信頼されたクライアントまたはユーザーへのアクセスを制限します。
コードを取得する
サンプル コードを表示またはダウンロードします。 (ダウンロード方法)。
サンプルをテストするには、アプリを実行して、次の設定を使って HTTP 要求を送信します。
- URL:
http://localhost:{port}/jsonpatch/jsonpatchwithmodelstate
- HTTP メソッド:
PATCH
- ヘッダー:
Content-Type: application/json-patch+json
- 本文: JSON プロジェクト フォルダーから JSON パッチ ドキュメント サンプルの 1 つをコピーして貼り付けます。
Additional resources
この記事では、ASP.NET Core Web API において JSON パッチ要求を処理する方法について説明します。
Important
JSON Patch 標準には固有の セキュリティ リスクがあります。 この実装 では、これらの固有のセキュリティ リスクを軽減しようとはしません。 JSON パッチ ドキュメントがターゲット オブジェクトに安全に適用されるようにするのは開発者の責任です。 詳細については、「 セキュリティ リスクの軽減」セクションを 参照してください。
Package installation
ASP.NET Core Web API での JSON パッチのサポートは、Newtonsoft.Json
に基づいており、Microsoft.AspNetCore.Mvc.NewtonsoftJson
NuGet パッケージが必要です。
JSON パッチのサポートを有効にするには:
Microsoft.AspNetCore.Mvc.NewtonsoftJson
NuGet パッケージをインストールします。AddNewtonsoftJson を呼び出します。 For example:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers() .AddNewtonsoftJson(); var app = builder.Build(); app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run();
AddNewtonsoftJson
では、System.Text.Json
の 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 Patch
JSON パッチは、リソースに適用される更新を指定するための形式です。 JSON パッチ ドキュメントには、"操作" の配列が含まれます。 各操作により、特定の種類の変更が識別されます。 このような変更の例としては、配列要素の追加やプロパティ値の置き換えがあります。
たとえば、次の JSON ドキュメントは、1 つのリソースとそのリソースに対応する JSON パッチ ドキュメントと、パッチ操作を適用した結果を表しています。
Resource example
{
"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 syntax
操作オブジェクトの path プロパティでは、レベル間にスラッシュを保持します。 たとえば、「 "/address/zipCode"
」のように入力します。
0 から始まるインデックスは、配列の要素を指定するために使用されます。
addresses
配列の最初の要素は、/addresses/0
にあります。 配列の末尾への add
では、インデックス番号ではなく、-
のようにハイフン (/addresses/-
) を使用します。
Operations
次の表は、JSON パッチの仕様に定義されている、サポートされる操作を示しています。
Operation | Notes |
---|---|
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
属性によって注釈されます。 - 通常は JsonPatchDocument<TModel> を利用して、
[FromBody]
を受け入れます。 - パッチ ドキュメント上の ApplyTo(Object) を呼び出して、変更を適用します。
次に例を示します:
[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
を構築します。 - パッチを適用します
- 応答の本文内で結果を返します。
実際のアプリでは、コードはデータベースなどの保存場所からデータを取得し、パッチを適用した後にデータベースを更新します。
Model state
前のアクション メソッドの例では、パラメーターの 1 つとしてモデルの状態を取得する ApplyTo
のオーバーロードを呼び出しています。 このオプションを利用すると、応答内にエラー メッセージを取得できます。 次の例では、test
操作に対する 400 Bad Request 応答の本文を示しています。
{
"Customer": [
"The current value 'John' at path 'customerName' != test value 'Nancy'."
]
}
Dynamic objects
次のアクション メソッドの例では、動的オブジェクトにパッチを適用する方法を示しています。
[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"
}
]
置換の操作
この操作は、remove
が後に続く add
と機能的に同じです。
次のサンプル パッチ ドキュメントでは、CustomerName
の値を設定して、Orders[0]
を新しい Order
オブジェクトに置き換えます。
[
{
"op": "replace",
"path": "/customerName",
"value": "Barry"
},
{
"op": "replace",
"path": "/orders/0",
"value": {
"orderName": "Order2",
"orderType": null
}
}
]
移動の操作
-
path
が配列要素を参照する場合:from
要素をpath
要素の場所にコピーしてから、remove
要素に対してfrom
操作を実行します。 -
path
がプロパティを参照する場合:from
プロパティの値をpath
プロパティにコピーしてから、remove
プロパティに対してfrom
操作を実行します。 -
path
が存在しないプロパティを参照する場合:- パッチへのリソースが静的オブジェクトの場合: 要求は失敗します。
- パッチへのリソースが動的オブジェクトの場合:
from
プロパティをpath
によって指示された場所にコピーしてから、remove
プロパティに対してfrom
操作を実行します。
次のサンプル パッチ ドキュメントでは、以下の操作を行います。
-
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"
}
]
コピー操作
この操作は、最後の move
手順がない remove
操作と機能的に同じです。
次のサンプル パッチ ドキュメントでは、以下の操作を行います。
-
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 つをコピーして貼り付けます。
セキュリティ リスクの軽減
Microsoft.AspNetCore.JsonPatch
ベースの実装で Newtonsoft.Json
パッケージを使用する場合は、潜在的なセキュリティ リスクを理解して軽減することが重要です。 以下のセクションでは、JSON Patch に関連付けられている特定されたセキュリティ リスクについて説明し、パッケージの安全な使用を確保するための推奨される軽減策を提供します。
Important
これは、脅威の完全な一覧ではありません。 アプリ開発者は、独自の脅威モデル レビューを実施して、アプリ固有の包括的な一覧を決定し、必要に応じて適切な軽減策を考え出す必要があります。 たとえば、コレクションをパッチ操作に公開するアプリでは、それらの操作がコレクションの先頭に要素を挿入または削除する場合に、アルゴリズムの複雑さの攻撃の可能性を考慮する必要があります。
独自のアプリに対して包括的な脅威モデルを実行し、以下の推奨される軽減策に従って特定された脅威に対処することで、これらのパッケージのコンシューマーは、セキュリティ リスクを最小限に抑えながら、JSON パッチ機能をアプリに統合できます。
メモリ増幅によるサービス拒否 (DoS)
-
シナリオ: 悪意のあるクライアントが、大きなオブジェクト グラフを複数回複製する
copy
操作を送信すると、メモリが過剰に消費されます。 - 影響: 潜在的なOut-Of-Memory (OOM)状態が発生し、サービスが中断する可能性があります。
-
Mitigation:
-
ApplyTo
を呼び出す前に、受信 JSON パッチ ドキュメントのサイズと構造を検証します。 - 検証はアプリ固有である必要がありますが、検証の例は次のようになります。
-
public void Validate(JsonPatchDocument patch)
{
// This is just an example. It's up to the developer to make sure that
// this case is handled properly, based on the app needs.
if (patch.Operations.Where(op => op.OperationType == OperationType.Copy).Count()
> MaxCopyOperationsCount)
{
throw new InvalidOperationException();
}
}
ビジネスロジックの覆し
- シナリオ: パッチ操作では、暗黙的なインバリアント (内部フラグ、ID、計算フィールドなど) を持つフィールドを操作でき、ビジネス上の制約に違反します。
- 影響:データ整合性の問題と意図しないアプリの動作。
-
Mitigation:
- 変更しても安全な明示的に定義されたプロパティを持つ POCO オブジェクトを使用します。
- ターゲット オブジェクトで機密性の高いプロパティまたはセキュリティ クリティカルなプロパティを公開しないようにします。
- POCO オブジェクトが使用されていない場合は、操作の適用後に修正プログラムが適用されたオブジェクトを検証して、ビジネス ルールとインバリアントが違反していないことを確認します。
認証と承認
- シナリオ: 認証されていないクライアントまたは未承認のクライアントが、悪意のある JSON パッチ要求を送信します。
- 影響:機密データを変更したり、アプリの動作を中断したりするための未承認のアクセス。
-
Mitigation:
- 適切な認証と承認メカニズムを使用して、JSON パッチ要求を受け入れるエンドポイントを保護します。
- 適切なアクセス許可を持つ信頼されたクライアントまたはユーザーへのアクセスを制限します。
Additional resources
この記事では、ASP.NET Core Web API において JSON パッチ要求を処理する方法について説明します。
Important
JSON Patch 標準には固有の セキュリティ リスクがあります。 これらのリスクは JSON Patch 標準に固有であるため、この実装 では固有のセキュリティ リスクを軽減しようとはしません。 JSON パッチ ドキュメントがターゲット オブジェクトに安全に適用されるようにするのは開発者の責任です。 詳細については、「 セキュリティ リスクの軽減」セクションを 参照してください。
Package installation
ご使用のアプリで JSON パッチのサポートを有効にするには、次の手順を実行します。
Microsoft.AspNetCore.Mvc.NewtonsoftJson
NuGet パッケージをインストールします。プロジェクトの
Startup.ConfigureServices
メソッドを更新して、AddNewtonsoftJson を呼び出します。 For example:services .AddControllersWithViews() .AddNewtonsoftJson();
AddNewtonsoftJson
は MVC サービス登録メソッドと互換性があります。
JSON パッチ、AddNewtonsoftJson、System.Text.Json
AddNewtonsoftJson
では、System.Text.Json
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 Patch
JSON パッチは、リソースに適用される更新を指定するための形式です。 JSON パッチ ドキュメントには、"操作" の配列が含まれます。 各操作により、特定の種類の変更が識別されます。 このような変更の例としては、配列要素の追加やプロパティ値の置き換えがあります。
たとえば、次の JSON ドキュメントは、1 つのリソースとそのリソースに対応する JSON パッチ ドキュメントと、パッチ操作を適用した結果を表しています。
Resource example
{
"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 syntax
操作オブジェクトの path プロパティでは、レベル間にスラッシュを保持します。 たとえば、「 "/address/zipCode"
」のように入力します。
0 から始まるインデックスは、配列の要素を指定するために使用されます。
addresses
配列の最初の要素は、/addresses/0
にあります。 配列の末尾への add
では、インデックス番号ではなく、-
のようにハイフン (/addresses/-
) を使用します。
Operations
次の表は、JSON パッチの仕様に定義されている、サポートされる操作を示しています。
Operation | Notes |
---|---|
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
属性によって注釈されます。 - 通常は
JsonPatchDocument<T>
を利用して、[FromBody]
を受け入れます。 - パッチ ドキュメント上の
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
を構築します。 - パッチを適用します
- 応答の本文内で結果を返します。
実際のアプリでは、コードはデータベースなどの保存場所からデータを取得し、パッチを適用した後にデータベースを更新します。
Model state
前のアクション メソッドの例では、パラメーターの 1 つとしてモデルの状態を取得する ApplyTo
のオーバーロードを呼び出しています。 このオプションを利用すると、応答内にエラー メッセージを取得できます。 次の例では、test
操作に対する 400 Bad Request 応答の本文を示しています。
{
"Customer": [
"The current value 'John' at path 'customerName' is not equal to the test value 'Nancy'."
]
}
Dynamic objects
次のアクション メソッドの例では、動的オブジェクトにパッチを適用する方法を示しています。
[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"
}
]
置換の操作
この操作は、remove
が後に続く add
と機能的に同じです。
次のサンプル パッチ ドキュメントでは、CustomerName
の値を設定して、Orders[0]
を新しい Order
オブジェクトに置き換えます。
[
{
"op": "replace",
"path": "/customerName",
"value": "Barry"
},
{
"op": "replace",
"path": "/orders/0",
"value": {
"orderName": "Order2",
"orderType": null
}
}
]
移動の操作
-
path
が配列要素を参照する場合:from
要素をpath
要素の場所にコピーしてから、remove
要素に対してfrom
操作を実行します。 -
path
がプロパティを参照する場合:from
プロパティの値をpath
プロパティにコピーしてから、remove
プロパティに対してfrom
操作を実行します。 -
path
が存在しないプロパティを参照する場合:- パッチへのリソースが静的オブジェクトの場合: 要求は失敗します。
- パッチへのリソースが動的オブジェクトの場合:
from
プロパティをpath
によって指示された場所にコピーしてから、remove
プロパティに対してfrom
操作を実行します。
次のサンプル パッチ ドキュメントでは、以下の操作を行います。
-
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"
}
]
コピー操作
この操作は、最後の move
手順がない remove
操作と機能的に同じです。
次のサンプル パッチ ドキュメントでは、以下の操作を行います。
-
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