dotnet / AspNetCore.Docs

Documentation for ASP.NET Core
https://docs.microsoft.com/aspnet/core
Creative Commons Attribution 4.0 International
12.61k stars 25.3k forks source link

The JsonPatch Example Code Does Not Work #28500

Closed Vincent-Lz-Zhang closed 1 year ago

Vincent-Lz-Zhang commented 1 year ago

I checked out the source code in this folder: https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/web-api/jsonpatch/samples/6.x/api

Before started my testing, I went through this document: https://learn.microsoft.com/en-us/aspnet/core/web-api/jsonpatch?view=aspnetcore-6.0, so the sample HTTP request body I used was taken from it.

I built and ran the Web API project, and with Postman, I sent a PATCH request to: http://localhost:63971/jsonpatch/JsonPatchWithoutModelState

I got error:

{ "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "traceId": "00-125d7abfba57ccb4a038cd738bbc7544-ec252f112ed656d1-00", "errors": { "$": [ "The JSON value could not be converted to Microsoft.AspNetCore.JsonPatch.JsonPatchDocument`1[JsonPatchSample.Models.Customer]. Path: $ | LineNumber: 0 | BytePositionInLine: 1." ], "patchDoc": [ "The patchDoc field is required." ] } }

The error suggests that the JsonConverter for JsonPatchDocument does not work at all. I'm on Windows 10 Pro, OS build: 19045.2604 And Visual Studio 2022 Community Version 17.3.0


Document Details

Do not edit this section. It is required for learn.microsoft.com ➟ GitHub issue linking.


Associated WorkItem - 69047

fiyazbinhasan commented 1 year ago

Hi @Vincent-Lz-Zhang, which example request produced the error?

Vincent-Lz-Zhang commented 1 year ago

@fiyazbinhasan

[ { "op": "add", "path": "/customerName", "value": "Barry" }, { "op": "add", "path": "/orders/-", "value": { "orderName": "Order2", "orderType": null } } ]

Vincent-Lz-Zhang commented 1 year ago

For your information, the JsonConverter for JSON Patch Document does not work either in my own ASP.NET Core Web API project (also based on NET 6.0) after following the tutorial. And that motivated me to test this example.

fiyazbinhasan commented 1 year ago

Ok...I'll take a look at it ASAP. Will prepare a sample and modify the docs if necessary. Thanks for reporting 😊

fiyazbinhasan commented 1 year ago

@Vincent-Lz-Zhang, I've found the issue you are facing. If I'm correct, you are not correctly setting the Content-Type. For a patch, the Content-Type should be application/json-patch+json instead of the regular application/json for other HTTP methods.

image

It is documented in the Get the code

@Rick-Anderson the placement of this mandatory information needs to be better. Would you like me to point this out somewhere else in the doc?

Rick-Anderson commented 1 year ago

@fiyazbinhasan yes, after line 310, add:

JsonPatch requires setting the Content-Type Header to application/json-patch+json

Vincent-Lz-Zhang commented 1 year ago

Yes, it works. Thank you very much. @fiyazbinhasan

Vincent-Lz-Zhang commented 1 year ago

@fiyazbinhasan

Actually, while waiting for the reply, I have already implemented a simplified version of JsonConverter for JsonPatchDocument. But since you guys provided some feedback, I would like to make it work for me.

However, the challenge for me is not over yet. I modified the example code as below:

New class EditCustomerDto added, it wraps JsonPatchDocument and another property.

using Microsoft.AspNetCore.JsonPatch;

namespace JsonPatchSample.Models;

public class EditCustomerDto
{
    public JsonPatchDocument<Customer> Changes { get; set;}
    public string ChangeComments { get; set; } 
}

And I changed the controller's 3rd method to:

[HttpPatch]
public IActionResult JsonPatchWithoutModelState([FromBody] EditCustomerDto editDto)
{
    var customer = CreateCustomer();

    editDto.Changes.ApplyTo(customer);

    return new ObjectResult(customer);
}

In Postman, I specified the following body:

{
    "changes": [
        {
            "op": "add",
            "path": "/customerName",
            "value": "Barry"
        },
        {
            "op": "add",
            "path": "/orders/-",
            "value": {
                "orderName": "Order2",
                "orderType": null
            }
        }
    ],
    "ChangeComments": "Editor's comments"
}

It does not work. I got error:

{ "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "traceId": "00-5e328a3c8afdfbf69632424ccb6025d3-5ca0caaabb40e9f4-00", "errors": { "patchDoc": [ "The patchDoc field is required." ], "$.changes": [ "The JSON value could not be converted to Microsoft.AspNetCore.JsonPatch.JsonPatchDocument`1[JsonPatchSample.Models.Customer]. Path: $.changes | LineNumber: 1 | BytePositionInLine: 16." ] } }

So, is there a way to make it work when having JsonPatchDocument as a property of the root JSON document? I am pretty sure it can be done this way because I saw another .NET 6 project coded this way. Thanks.

Vincent-Lz-Zhang commented 1 year ago

I just have some new findings.

First, if I call AddNewtonsoftJson(), changing the Content-Type would not be necessary. The example in the doc still works with Content-Type being application/json.

Second, if I call AddNewtonsoftJson(), with Content-Type being application/json, it also works with my wrapper class EditCustomerDto.

image

Then here comes my question: I understand application/json-patch+json is specified by the RFC6902 standard, though, is it really the essential cause to the problem I raised?

fiyazbinhasan commented 1 year ago

@Vincent-Lz-Zhang, the reason why you are not using AddNewtonsoftJson() is that it replaces the defauft System.Text.Json input /output formatter. The idea behind MyJPIF.GetJsonPatchInputFormatter is to use the System.Text.Json while adding support for only the JSON patch using Microsoft.AspNetCore.Mvc.NewtonsoftJson. We yet don't have support for Json patch in System.Text.Json. See this issue

To address your comment earlier,

{
    "changes": [
        {
            "op": "add",
            "path": "/customerName",
            "value": "Barry"
        },
        {
            "op": "add",
            "path": "/orders/-",
            "value": {
                "orderName": "Order2",
                "orderType": null
            }
        }
    ],
    "ChangeComments": "Editor's comments"
}

As you can see, this is not a valid JSON patch document. I would instead suggest you go with the following approach,

public class EditCustomerDto
{
    public string? CustomerName { get; set; }
    public List<Order>? Orders { get; set; }
    public string ChangeComments { get; set; } 
}
[HttpPatch]
    public IActionResult JsonPatchWithDto([FromBody] JsonPatchDocument<EditCustomerDto> editDto)
    {
        var customerDto = CreateCustomerDto();

        editDto.ApplyTo(customerDto);

        return new ObjectResult(customerDto);
    }

    private EditCustomerDto CreateCustomerDto()
    {
        return new EditCustomerDto
        {
            CustomerName = "John",
            Orders = new List<Order>()
            {
                new Order
                {
                    OrderName = "Order0"
                },
                new Order
                {
                    OrderName = "Order1"
                }
            },
            ChangeComments = "Comments"
        };
    }

You can specify multiple operations in a patch. The following body replaces the value of ChangeComments to Editor's comments.

[
  {
    "op": "replace",
    "path": "/changeComments",
    "value": "Editor's comments"
  },
  {
    "op": "replace",
    "path": "/customerName",
    "value": "Barry"
  },
  {
    "op": "replace",
    "path": "/orders/0",
    "value": {
      "orderName": "Order2",
      "orderType": null
    }
  }
]

You can use some mapper library to map to and from between EditCustomerDto and Customer