この記事では、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 ドキュメントを変更する一連の操作について説明します。
addremovereplacemovecopytest
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を実装します。 この機能を使用すると、次のことが可能になります。
-
Microsoft.AspNetCore.JsonPatch.SystemTextJsonNuGet パッケージが必要です。 - .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 から取得した
-
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 つをコピーして貼り付けます。
その他のリソース
この記事では、ASP.NET Core Web API において JSON パッチ要求を処理する方法について説明します。
Important
JSON Patch 標準には固有の セキュリティ リスクがあります。 この実装 では、これらの固有のセキュリティ リスクを軽減しようとはしません。 JSON パッチ ドキュメントがターゲット オブジェクトに安全に適用されるようにするのは開発者の責任です。 詳細については、「 セキュリティ リスクの軽減」セクションを 参照してください。
パッケージのインストール
ASP.NET Core Web API での JSON パッチのサポートは、Newtonsoft.Json に基づいており、Microsoft.AspNetCore.Mvc.NewtonsoftJson NuGet パッケージが必要です。
JSON パッチのサポートを有効にするには:
Microsoft.AspNetCore.Mvc.NewtonsoftJsonNuGet パッケージをインストールします。AddNewtonsoftJson を呼び出します。 例えば次が挙げられます。
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.NewtonsoftJsonNuGet パッケージをインストールします。以下を更新します。
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/-) を使用します。
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を構築します。 - パッチを適用します
- 応答の本文内で結果を返します。
実際のアプリでは、コードはデータベースなどの保存場所からデータを取得し、パッチを適用した後にデータベースを更新します。
モデルの状態
前のアクション メソッドの例では、パラメーターの 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"
}
]
置換の操作
この操作は、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 パッチ要求を受け入れるエンドポイントを保護します。
- 適切なアクセス許可を持つ信頼されたクライアントまたはユーザーへのアクセスを制限します。
その他のリソース
この記事では、ASP.NET Core Web API において JSON パッチ要求を処理する方法について説明します。
Important
JSON Patch 標準には固有の セキュリティ リスクがあります。 これらのリスクは JSON Patch 標準に固有であるため、この実装 では固有のセキュリティ リスクを軽減しようとはしません。 JSON パッチ ドキュメントがターゲット オブジェクトに安全に適用されるようにするのは開発者の責任です。 詳細については、「 セキュリティ リスクの軽減」セクションを 参照してください。
パッケージのインストール
ご使用のアプリで JSON パッチのサポートを有効にするには、次の手順を実行します。
Microsoft.AspNetCore.Mvc.NewtonsoftJsonNuGet パッケージをインストールします。プロジェクトの
Startup.ConfigureServicesメソッドを更新して、AddNewtonsoftJson を呼び出します。 例えば次が挙げられます。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 パッチ
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/-) を使用します。
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を構築します。 - パッチを適用します
- 応答の本文内で結果を返します。
実際のアプリでは、コードはデータベースなどの保存場所からデータを取得し、パッチを適用した後にデータベースを更新します。
モデルの状態
前のアクション メソッドの例では、パラメーターの 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"
}
]
置換の操作
この操作は、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