次の方法で共有


ASP.NET Core Web API での JSON パッチのサポート

この記事では、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 パッチ用のアクション メソッド:

コントローラー アクション メソッドの例:

[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応答を返します。
  • JSON パッチの適用:
    • ApplyTo(Object) メソッドは、patchDoc から取得したCustomer オブジェクトに JSON Patch 操作を適用します。
    • 無効な操作や競合など、パッチ アプリケーション中にエラーが発生した場合は、エラー処理デリゲートによってキャプチャされます。 このデリゲートは、影響を受けるオブジェクトの型名とエラー メッセージを使用して、 ModelState にエラー メッセージを追加します。
  • 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> を適用する

その具体的な例を次に示します:

  • addreplace、および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>の規則とオプションに従います。

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 オブジェクトが使用されていない場合は、操作の適用後に修正プログラムが適用されたオブジェクトを検証して、ビジネス ルールとインバリアントに違反していないことを確認します。

認証と承認

  • シナリオ: 認証されていないクライアントまたは未承認のクライアントが、悪意のある 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]
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 パッチのサポートを有効にするには、次の手順を実行します。

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

  2. プロジェクトの 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 つをコピーして貼り付けます。