Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

InvalidOperationException is thrown while patching elements in array #393

Closed
anaruzhnii opened this issue Mar 7, 2023 · 3 comments · Fixed by #394
Closed

InvalidOperationException is thrown while patching elements in array #393

anaruzhnii opened this issue Mar 7, 2023 · 3 comments · Fixed by #394
Labels
bug Something isn't working pkg:patch

Comments

@anaruzhnii
Copy link

anaruzhnii commented Mar 7, 2023

Environment

  • Nuget Package: JsonPatch.Net
  • Nuget Version: 2.0.4
  • OS: Windows 11 Pro
  • .Net Target: .NET 5, 6, 7

Describe the bug
InvalidOperationException is thrown when I try to patch JSON object from a JSON array.
Exception message: The node already has a parent.
See call stack below.

To Reproduce

  • Parse JSON array
  • Pick one object and try to patch it
  • Get an error
Code to reproduce the problem

using System;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using Json.More;
using Json.Patch;
using Json.Pointer;

const string mask = "*****";
var maskJson = JsonValue.Create(mask);

var pathsToPatch = new[] { "/first_name", "/last_name" };

var patchOperations = pathsToPatch.Select(path => PatchOperation.Replace(JsonPointer.Parse(path), maskJson));
var patchConfig = new JsonPatch(patchOperations);

const string singleObjectJson = "{" +
    "\"_id\":\"640729d45434f90313d25c78\"," +
    "\"guid\":\"f2e2767c-03e0-4862-addc-7d46c55efb33\"," +
    "\"first_name\":\"Kathrine\"," +
    "\"last_name\":\"Pate\"" +
    "}";

var singleObject = JsonDocument.Parse(singleObjectJson).RootElement;
var patchedSingleObject = patchConfig.Apply(singleObject.AsNode()).Result;
Console.WriteLine(JsonSerializer.Serialize(patchedSingleObject));

const string arrayObjectJson = "[" +
    "{" +
    "\"_id\":\"640729d45434f90313d25c78\"," +
    "\"guid\":\"f2e2767c-03e0-4862-addc-7d46c55efb33\"," +
    "\"first_name\":\"Kathrine\"," +
    "\"last_name\":\"Pate\"" +
    "}," +
    "{\"_id\":\"640729d45b5824ffcabc30a5\"," +
    "\"guid\":\"73193eda-074b-4f31-9f09-507a008ccb75\"," +
    "\"first_name\":\"Rivers\"," +
    "\"last_name\":\"Smith\"" +
    "}" +
    "]";

var arrayObject = JsonDocument.Parse(arrayObjectJson).RootElement;

// Way 1: patch whole array
var patchedArray = patchConfig.Apply(arrayObject.AsNode()).Result;  // <- does nothing

var jsonArray = arrayObject.AsNode().AsArray();

// Way 2: just patch every element
foreach (var element in jsonArray)
{
    var patchedNode = patchConfig.Apply(element).Result;            // <-  throws an error
}

// Way 3: remove from initial array and then patch
for (int currentIndex = jsonArray.Count - 1; currentIndex >= 0; currentIndex--)
{
    var nodeToPatch = jsonArray[currentIndex];
    jsonArray.RemoveAt(currentIndex);

    var patchedNode = patchConfig.Apply(nodeToPatch).Result;        // <-  throws an error
}

Expected behavior
JSON object is patched successfully.

Additional context

  • Patching of a single object works fine
  • Patching of array as a whole does nothing (see Way 1 in code above)
  • It looks like removing the object from JSON array resets its parent (see Way 3), but it doesn't work either.
Call stack

   System.InvalidOperationException: The node already has a parent.
   at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_NodeAlreadyHasParent()
   at System.Text.Json.Nodes.JsonNode.AssignParent(JsonNode parent)
   at System.Text.Json.Nodes.JsonObject.<>c__DisplayClass9_0.<SetItem>b__0()
   at System.Text.Json.JsonPropertyDictionary`1.SetValue(String propertyName, T value, Action assignParent)
   at System.Text.Json.Nodes.JsonObject.SetItem(String propertyName, JsonNode value)
   at System.Text.Json.Nodes.JsonNode.set_Item(String propertyName, JsonNode value)
   at Json.Patch.ReplaceOperationHandler.Process(PatchContext context, PatchOperation operation)
   at Json.Patch.PatchOperation.Handle(PatchContext context)
   at Json.Patch.JsonPatch.Apply(JsonNode source)
   at Program.<Main>$(String[] args) in C:\upteam\Experiments\JsonPatchNetBug\Program.cs:line 48

@anaruzhnii anaruzhnii added the bug Something isn't working label Mar 7, 2023
@anaruzhnii anaruzhnii changed the title InvalidOperationException: The node already has a parent while patching elements in array InvalidOperationException is thrown while patching elements in array Mar 7, 2023
@gregsdennis
Copy link
Owner

Thanks for reporting this. Yeah, it seems I have several places across the libs where I just assign a pre-existing node instead of copying it.

This should be a simple fix.

@gregsdennis
Copy link
Owner

gregsdennis commented Mar 7, 2023

So the first case, where it does nothing, is correct. It should do nothing because the locations indicated by the patch don't exist.

The patch is

[
  {"op":"replace","path":"/first_name","value":"*****"},
  {"op":"replace","path":"/last_name","value":"*****"}
]

The data is

[
  {"_id":"640729d45434f90313d25c78","guid":"f2e2767c-03e0-4862-addc-7d46c55efb33","first_name":"Kathrine","last_name":"Pate"},
  {"_id":"640729d45b5824ffcabc30a5","guid":"73193eda-074b-4f31-9f09-507a008ccb75","first_name":"Rivers","last_name":"Smith"}
]

/first_name and /last_name aren't valid locations within the data, so it does nothing. You should be able to confirm this on other JSON Patch sites.

As far as I'm aware, there's not a way to apply a single patch to all elements in an array at once like this. Mainly this is due to JSON Patch using JsonPointer which can only indicate a single location within a JSON structure. You'd need to indicate multiple locations.

@gregsdennis
Copy link
Owner

Lastly, just a suggestion.

Instead of

var singleObject = JsonDocument.Parse(singleObjectJson).RootElement;
var patchedSingleObject = patchConfig.Apply(singleObject.AsNode()).Result;

Just use JsonNode.Parse():

var singleObject = JsonNode.Parse(singleObjectJson);
var patchedSingleObject = patchConfig.Apply(singleObject).Result;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working pkg:patch
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants