Skip to content

PUT vs PATCH in ASP.NET Core Web API

Both PUT and PATCH are used to update an existing resource, so beginners often use them as if they were the same. They are not:

  • PUT replaces the whole resource. The client sends the complete new version, every field.
  • PATCH changes part of the resource. The client sends only the fields that change.

Think of your address in a bank's records. With PUT, you fill in a completely new address form: house number, street, city, PIN code, all of it, even if only the house number changed. Whatever you leave blank is now blank in the bank's records. With PATCH, you write a short note: "please change my house number to 42". Everything else stays as it was.

This lesson continues with the Products API from What is Web API and GET vs POST. All responses below were captured from the running API.

Before the updates, product 2 looks like this:

GET /api/products/2
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id":2,"name":"Mechanical Keyboard","category":"Accessories","price":3499,"stock":45}

PUT: replace the whole resource

PUT uses the same ProductDto as POST, because it needs the complete product:

public class ProductDto
{
    [Required, StringLength(100)]
    public string? Name { get; set; }

    [Required]
    public string? Category { get; set; }

    [Range(0.01, 1_000_000)]
    public decimal Price { get; set; }

    [Range(0, 100_000)]
    public int Stock { get; set; }
}

// PUT api/products/2  -> replace the WHOLE product
[HttpPut("{id:int}")]
public IActionResult Replace(int id, ProductDto dto)
{
    var product = _store.Find(id);
    if (product is null) return NotFound();

    // every field is overwritten with what the client sent
    product.Name = dto.Name!;
    product.Category = dto.Category!;
    product.Price = dto.Price;
    product.Stock = dto.Stock;
    return Ok(product);
}
PUT /api/products/2
Content-Type: application/json

{ "name": "Mechanical Keyboard RGB", "category": "Accessories", "price": 3999, "stock": 40 }
Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id":2,"name":"Mechanical Keyboard RGB","category":"Accessories","price":3999,"stock":40}

PUT is idempotent

Sending exactly the same PUT again gives exactly the same result. The product is simply set to the same values once more:

Same PUT /api/products/2, a second time
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id":2,"name":"Mechanical Keyboard RGB","category":"Accessories","price":3999,"stock":40}

Compare that with POST, where a second identical request created a second product. Because PUT is idempotent, a client can safely retry it after a network timeout.

What if you send only one field with PUT?

Suppose the client only wants to change the price, and sends just that with PUT:

PUT /api/products/2
Content-Type: application/json

{ "price": 2999 }
Response
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json; charset=utf-8

{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1",
 "title":"One or more validation errors occurred.",
 "status":400,
 "errors":{"Name":["The Name field is required."],
           "Category":["The Category field is required."]},
 "traceId":"00-a82d3fec..."}

PUT means "here is the complete new product", so Name and Category are required. Our validation rules protected the data.

This is the classic PUT bug. Without [Required] and [Range] attributes, this request would have succeeded and silently set Name and Category to null and Stock to 0, because missing JSON fields get their default values. With PUT, whatever the client doesn't send is lost. Always validate PUT bodies, or use PATCH for partial changes.

PATCH: change only what is sent

For PATCH we need a different DTO in which every field is optional. Nullable types (decimal?, int?) let us tell the difference between "not sent" (null) and "sent":

// What the client sends to partially update a product: every field optional
public class ProductPatchDto
{
    [StringLength(100)]
    public string? Name { get; set; }

    public string? Category { get; set; }

    [Range(0.01, 1_000_000)]
    public decimal? Price { get; set; }       // null = "don't change the price"

    [Range(0, 100_000)]
    public int? Stock { get; set; }
}

// PATCH api/products/2  -> change ONLY the fields that were sent
[HttpPatch("{id:int}")]
public IActionResult Update(int id, ProductPatchDto dto)
{
    var product = _store.Find(id);
    if (product is null) return NotFound();

    if (dto.Name is not null) product.Name = dto.Name;
    if (dto.Category is not null) product.Category = dto.Category;
    if (dto.Price is not null) product.Price = dto.Price.Value;
    if (dto.Stock is not null) product.Stock = dto.Stock.Value;
    return Ok(product);
}

Now the same "change only the price" request works with PATCH:

PATCH /api/products/2
Content-Type: application/json

{ "price": 2999 }
Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id":2,"name":"Mechanical Keyboard RGB","category":"Accessories","price":2999,"stock":40}

Only the price changed; the name, category and stock kept their values. Another PATCH, this time only the stock (for example after a sale):

PATCH /api/products/2 with { "stock": 35 }
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id":2,"name":"Mechanical Keyboard RGB","category":"Accessories","price":2999,"stock":35}

Validation still applies to the fields that are sent:

PATCH /api/products/2 with { "price": -5 }
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json; charset=utf-8

{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1",
 "title":"One or more validation errors occurred.",
 "status":400,
 "errors":{"Price":["The field Price must be between 0.01 and 1000000."]},
 "traceId":"00-14414bad..."}

And both PUT and PATCH return 404 when the product doesn't exist:

PUT /api/products/99
HTTP/1.1 404 Not Found
Content-Type: application/problem+json; charset=utf-8

{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.5","title":"Not Found","status":404,"traceId":"00-46cf1c09..."}

The standard way: JSON Patch

The nullable-DTO approach above is simple and very common, and its body format is close to the JSON Merge Patch standard (RFC 7396). It has one limitation: null means "not sent", so a client cannot deliberately set a field to null.

The other standard is JSON Patch (RFC 6902). Instead of the new values, the client sends a list of operations:

PATCH /api/products/2
Content-Type: application/json-patch+json

[
  { "op": "replace", "path": "/price", "value": 2999 },
  { "op": "replace", "path": "/stock", "value": 35 }
]

Supported operations are add, remove, replace, move, copy and test. ASP.NET Core supports JSON Patch through JsonPatchDocument<T>. On .NET 8 it requires the Microsoft.AspNetCore.Mvc.NewtonsoftJson package and builder.Services.AddControllers().AddNewtonsoftJson();. (.NET 10 adds a System.Text.Json-based JSON Patch package as well.)

using Microsoft.AspNetCore.JsonPatch;

[HttpPatch("{id:int}")]
public IActionResult Patch(int id, [FromBody] JsonPatchDocument<Product> patch)
{
    var product = _store.Find(id);
    if (product is null) return NotFound();

    patch.ApplyTo(product, ModelState);        // apply the operations; problems go into ModelState
    if (!ModelState.IsValid)
        return ValidationProblem(ModelState);

    return Ok(product);
}

Be careful with JSON Patch on database entities: a client could patch fields it shouldn't touch, such as /id or /isApproved. Apply the patch to a DTO containing only the editable fields, validate it, and then copy the values to the entity.

Is PATCH idempotent?

Not necessarily. PUT is always idempotent. PATCH can be idempotent, and our "set price to 2999" PATCH is, because repeating it leaves the same result. But PATCH may also describe operations that are not, for example a JSON Patch add to an array (each request appends another item), or an API that accepts { "stockChange": -1 } (each request reduces the stock again). So the HTTP standard does not guarantee that PATCH is idempotent.

PUT vs PATCH: comparison table

PointPUTPATCH
MeaningReplace the resource with this complete versionApply these changes to the resource
Request bodyThe full resource (all fields)Only the fields to change, or a list of operations
Fields not sentSet to default/empty (or rejected by validation)Left unchanged
Idempotent?Yes, alwaysNot guaranteed (depends on the operations)
Payload sizeLarger (whole object)Smaller (only changes): good for mobile apps
Typical DTOSame as create, with required fieldsAll fields optional (nullable), or JsonPatchDocument<T>
Can create a resource?Allowed by the standard, when the client chooses the URL (an "upsert")Normally no; it modifies an existing resource
ASP.NET Core attribute[HttpPut("{id}")][HttpPatch("{id}")]
Success codes200 OK (with body) or 204 No Content200 OK (with body) or 204 No Content
Content-Typeapplication/jsonapplication/json, application/merge-patch+json or application/json-patch+json

When to use which

ScenarioUse
An "Edit product" form that shows and submits all fieldsPUT
Syncing a full record from another systemPUT
Changing only the stock after a salePATCH
Changing an order's status to "Shipped"PATCH
Marking a notification as readPATCH
Mobile app on a slow network updating one field of a large objectPATCH
Two users editing different fields of the same record at the same timePATCH (each only overwrites their own field)

That last row is important. With PUT, if user A changes the price and user B changes the stock at the same time, whoever saves last sends the whole product, including the old value of the other field, and silently undoes the other user's change. PATCH sends only the changed field, so both updates survive. For stronger protection, APIs use ETags with the If-Match header, so the server rejects an update based on an old version (412 Precondition Failed).

Interview questions

1. What is the difference between PUT and PATCH?

PUT replaces the entire resource with the complete representation sent by the client; fields not sent are lost. PATCH applies a partial update, changing only the fields (or operations) sent and leaving everything else unchanged.

2. Which one is idempotent?

PUT is always idempotent: sending the same request many times leaves the same result. PATCH is not guaranteed to be idempotent, because a patch can describe operations like "append to a list" or "decrease stock by one".

3. What happens if a client sends only one field with PUT?

By the meaning of PUT, the other fields are replaced with empty or default values. That's why PUT DTOs should have [Required] validation, so incomplete requests are rejected with 400 instead of wiping data.

4. How do you implement PATCH in ASP.NET Core?

Either with a DTO whose properties are all nullable (only non-null values are applied), or with JsonPatchDocument<T> (RFC 6902 JSON Patch), applied with patch.ApplyTo(target, ModelState) and validated before saving.

5. What is the difference between PATCH and POST?

PATCH modifies an existing resource at a known URL (PATCH /api/products/2). POST usually creates a new resource in a collection (POST /api/products) or triggers an action, and the server decides the new resource's URL.

6. Can PUT be used to create a resource?

Yes, the HTTP standard allows it when the client knows the resource's URL (for example PUT /api/settings/theme): create it if it doesn't exist, replace it if it does. Many APIs choose to return 404 for unknown ids instead, as our example does, and use POST for creation.

7. What status codes should PUT and PATCH return?

200 OK with the updated resource, or 204 No Content with no body. 404 if the resource doesn't exist, 400 for invalid data, 409 for a conflict, and 201 Created if a PUT created a new resource.

Summary

  • PUT replaces the whole resource: send every field. Missing fields are lost, so validate.
  • PATCH updates only what is sent; other fields stay unchanged.
  • PUT is always idempotent; PATCH is not guaranteed to be.
  • Implement PATCH with an all-optional DTO, or with JsonPatchDocument<T> for RFC 6902 JSON Patch.
  • Use PUT for full edits and syncs, PATCH for small changes like status or stock, and to avoid overwriting other users' changes.

Related: What is Web API, GET vs POST and Web API CRUD with Stored Procedures.