MergePatchDto: A Smaller PATCH Shape for .NET


JSON Patch is powerful. Most APIs need something smaller.

For many ASP.NET Core endpoints, PATCH does not mean “let the client send edit operations against an object graph.” It means “let the client update this specific set of fields, and leave everything else alone.”

That shape wants a typed DTO. But ordinary DTO binding does not preserve whether a property was missing or explicitly sent as null.

MergePatchDto fills that gap: DTO-shaped PATCH requests, typed allowlists, explicit null vs missing tracking, and optional generated mapping.

dotnet add package MergePatchDto

Why JSON Patch feels too wide

ASP.NET Core already supports JSON Patch through JsonPatchDocument<T>, which uses the RFC 6902 operation format:

[
  { "op": "replace", "path": "/displayName", "value": "Mira" },
  { "op": "remove", "path": "/phoneNumber" }
]

That format is useful when clients need document-editing operations like add, remove, replace, move, copy, and test.

JsonPatchDocument<T> gives the server a target type, but the wire format is still not a typed DTO. Clients send op/path/value, and patchable members are referenced by JSON Pointer strings such as /displayName.

But for a lot of application APIs, the public contract is not an operation array. It is a fixed set of fields this endpoint allows the client to change.

That means the endpoint still has to decide which operations and paths are safe. The ASP.NET Core docs make that boundary explicit in the JSON Patch security note: developers are responsible for ensuring the patch document is safe to apply.

That is the right tradeoff when you want JSON Patch. It is unnecessary surface area when the API only wants a typed partial update.

A DTO is the allowlist

A profile endpoint might allow displayName, bio, and phoneNumber.

It should not accidentally allow roles, emailVerified, billingStatus, or passwordHash just because those properties exist somewhere on the target model.

That is the appeal of a DTO-shaped PATCH request: the patch type says what the endpoint accepts, and everything else is outside the contract.

That boundary is useful outside backend code too. OpenAPI and Swagger can show a normal object schema:

{
  "displayName": "Mira",
  "phoneNumber": null
}

Frontend code can model that as a normal request type. A form can send only the fields the user changed. Generated clients can expose named properties instead of asking callers to build op/path/value arrays by hand.

With JSON Patch, the API documentation can describe the operation-array shape, but the actual allowlist usually lives in prose, validation code, or endpoint policy. With a patch DTO, the allowlist is part of the request schema.

MergePatchDto keeps that shape. The package can generate presence tracking and mapping from the DTO, but the DTO remains the boundary.

Presence is not nullability

This request means “change displayName and leave phoneNumber alone”:

{ "displayName": "Mira" }

This request means “change displayName and clear phoneNumber”:

{ "displayName": "Mira", "phoneNumber": null }

After normal deserialization, both can leave PhoneNumber == null.

For PATCH, that loses intent. A property needs three states:

missing -> leave the existing value alone
present with value -> set the value
present with null -> clear the value

In C#, nullability should describe whether a field may be set to null. Presence should describe whether the client sent the field at all.

Those are different questions.

A non-nullable patch property can still be optional in the request body. If the client omits it, the generated presence flag is false and nothing is applied.

A nullable patch property means explicit null is a valid requested value.

That distinction makes validation cleaner:

missing -> skip field validation
present with invalid value -> reject
present with null -> allow only when clearing is valid

MergePatchDto keeps the request as a normal JSON object while preserving what the client actually sent.

This is backwards compatibility

PATCH endpoints evolve by adding fields.

Using PUT for every update does not avoid that problem. PUT is a good fit when the client is intentionally replacing the whole resource representation. It is a bad default for partial edits because older clients often do not know about newer fields.

At first, an endpoint might accept:

{ "displayName": "Mira" }

Later, it might add:

{
  "displayName": "Mira",
  "marketingOptIn": false
}

Old clients do not know marketingOptIn exists. They should not reset it just because they sent an older request shape. But if the server treats the request as a full replacement, the missing field can easily become a default value or null unless the endpoint adds special-case merge behavior.

The safe rule for partial updates is:

if the property was not present, it was not part of the request

That is what makes DTO-shaped PATCH endpoints safe to extend.

When to use which

Use JSON Patch when clients need operation documents, JSON Pointer paths, array-position edits, move, copy, or test.

Use MergePatchDto when your API wants this contract:

the request body is a normal JSON object
the patchable fields are a typed DTO allowlist
explicit null means clear
missing means unchanged
presence tracking and mapping are generated

That is the smaller PATCH shape many .NET APIs need.

MergePatchDto on NuGet

dotnet add package MergePatchDto