Skip to content

JsonPatchDocument .ApplyTo(...) raises JsonPatchException for unknown Properties instead of transparently mapping them to JsonExtensionData #57711

@hf-kklein

Description

@hf-kklein

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

If a property is annotated with the [System.Text.Json.Serialization.JsonExtensionData] attribute, I expect the JsonPatchDocument.ApplyTo(...) method to map unknown paths to the respective property.
Instead (actual behaviour), an Microsoft.AspNetCore.JsonPatch.Exceptions.JsonPatchException is raised.

Expected Behavior

Unmapped paths in a JsonPatchDocument should be mapped (transparently) to the JsonExtensionData property.
By "transparently" I mean, that the client sending the patch to the server doesn't need to be aware of the ExtensionData property.

Steps To Reproduce

#nullable enable
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.JsonPatch.Operations;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace UnitTests;

class MyClass
{
    public int Foo { get; set; }
    public string Bar { get; set; }

    [System.Text.Json.Serialization.JsonExtensionData]
    public IDictionary<string, object>? MyExtensionData { get; set; }
}

[TestClass]
public class JsonPatchDocumentTest
{
    [TestMethod]
    [DataRow(true, false, false)]
    [DataRow(false, false, false)]
    [DataRow(false, true, false)]
    // the following operations with "useStringlyTypedPath=true fail
    [DataRow(true, false, true)]
    [DataRow(false, false, true)]
    [DataRow(false, true, true)]
    public void TestPatchingExtensionData(bool initialExtensionDataAreEmpty, bool overwriteExistingExtensionData, bool useStringlyTypedPath)
    {
        if (initialExtensionDataAreEmpty && overwriteExistingExtensionData)
        {
            throw new Exception("This makes no sense");
        }

        string myEntityAsJson;
        if (initialExtensionDataAreEmpty)
        {
            myEntityAsJson = """
                             {
                                 "Foo": 17,
                                 "Bar": "asd"
                             }   
                             """;
        }
        else
        {
            myEntityAsJson = """
                             {
                                 "Foo": 17,
                                 "Bar": "asd",
                                 "abc": "def"
                             }   
                             """;
        }

        var myEntity = System.Text.Json.JsonSerializer.Deserialize<MyClass>(myEntityAsJson);
        Assert.AreEqual(expected: initialExtensionDataAreEmpty, actual: myEntity.MyExtensionData is null);

        var myPatch = new JsonPatchDocument<MyClass>();
        myPatch.Add(x => x.Foo, 42);
        myPatch.Add(x => x.Bar, "fgh");
        string modifiedKey = overwriteExistingExtensionData ? "abc" : "uvw";

        if (!useStringlyTypedPath)
        {
            // this code path works, but my client isn't aware of the extension data property nor its name
            if (initialExtensionDataAreEmpty)
            {
                myPatch.Add(x => x.MyExtensionData, new Dictionary<string, object> { { modifiedKey, "xyz" } });
            }
            else
            {
                myPatch.Add(x => x.MyExtensionData[modifiedKey], "xyz");
            }
        }
        else
        {
            // this is the behaviour of my client, which doesn't know about the extension data
            myPatch.Operations.Add(new Operation<MyClass> { path = "/" + modifiedKey, op = "add", value = "xyz" });
        }
        
        myPatch.ApplyTo(myEntity);
        // this throws a Microsoft.AspNetCore.JsonPatch.Exceptions.JsonPatchException:
        // "The target location specified by path segment 'abc' was not found."
        // "The target location specified by path segment 'uvw' was not found."

        // Instead of the exception, I'd like to automatically map the path "/xyz" (or "/abc" respectively) to MyExtensionData

        Assert.AreEqual(expected: 42, actual: myEntity.Foo);
        Assert.AreEqual(expected: "fgh", actual: myEntity.Bar);
        Assert.IsNotNull(myEntity.MyExtensionData);
        Assert.IsTrue(myEntity.MyExtensionData.ContainsKey(modifiedKey));
        Assert.AreEqual(expected: "xyz", actual: myEntity.MyExtensionData[modifiedKey]);
        if (!overwriteExistingExtensionData && !initialExtensionDataAreEmpty)
        {
            Assert.IsTrue(myEntity.MyExtensionData.ContainsKey("abc"));
            Assert.AreEqual(expected: "def", actual: myEntity.MyExtensionData["abc"].ToString());
        }
    }
}

Exceptions (if any)

No response

.NET Version

8

Anything else?

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    ✔️ Resolution: AnsweredResolved because the question asked by the original author has been answered.Status: Resolvedarea-mvcIncludes: MVC, Actions and Controllers, Localization, CORS, most templates

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions