diff --git a/AspNetCore.slnx b/AspNetCore.slnx
index 42988a869787..b4b5b19b9662 100644
--- a/AspNetCore.slnx
+++ b/AspNetCore.slnx
@@ -211,6 +211,10 @@
+
+
+
+
diff --git a/src/Features/JsonPatch.SystemTextJson/.vsconfig b/src/Features/JsonPatch.SystemTextJson/.vsconfig
new file mode 100644
index 000000000000..77009588dff2
--- /dev/null
+++ b/src/Features/JsonPatch.SystemTextJson/.vsconfig
@@ -0,0 +1,12 @@
+{
+ "version": "1.0",
+ "components": [
+ "Microsoft.Net.Component.4.6.2.TargetingPack",
+ "Microsoft.Net.Component.4.7.2.SDK",
+ "Microsoft.Net.Component.4.7.2.TargetingPack",
+ "Microsoft.VisualStudio.Workload.ManagedDesktop",
+ "Microsoft.VisualStudio.Workload.NetCoreTools",
+ "Microsoft.VisualStudio.Workload.NetWeb",
+ "Microsoft.VisualStudio.Workload.VisualStudioExtension"
+ ]
+}
diff --git a/src/Features/JsonPatch.SystemTextJson/JsonPatch.SystemTextJson.slnf b/src/Features/JsonPatch.SystemTextJson/JsonPatch.SystemTextJson.slnf
new file mode 100644
index 000000000000..19edc5866421
--- /dev/null
+++ b/src/Features/JsonPatch.SystemTextJson/JsonPatch.SystemTextJson.slnf
@@ -0,0 +1,9 @@
+{
+ "solution": {
+ "path": "..\\..\\..\\AspNetCore.slnx",
+ "projects" : [
+ "src\\Features\\JsonPatch.SystemTextJson\\src\\Microsoft.AspNetCore.JsonPatch.SystemTextJson.csproj",
+ "src\\Features\\JsonPatch.SystemTextJson\\test\\Microsoft.AspNetCore.JsonPatch.SystemTextJson.Tests.csproj"
+ ]
+ }
+}
diff --git a/src/Features/JsonPatch.SystemTextJson/build.cmd b/src/Features/JsonPatch.SystemTextJson/build.cmd
new file mode 100644
index 000000000000..956c031417d3
--- /dev/null
+++ b/src/Features/JsonPatch.SystemTextJson/build.cmd
@@ -0,0 +1,4 @@
+
+@ECHO OFF
+SET RepoRoot=%~dp0..\..\..
+%RepoRoot%\eng\build.cmd -projects %~dp0**\*.*proj %*
diff --git a/src/Features/JsonPatch.SystemTextJson/build.sh b/src/Features/JsonPatch.SystemTextJson/build.sh
new file mode 100644
index 000000000000..4eb40c27e392
--- /dev/null
+++ b/src/Features/JsonPatch.SystemTextJson/build.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+repo_root="$DIR/../.."
+"$repo_root/eng/build.sh" --projects "$DIR/**/*.*proj" "$@"
diff --git a/src/Features/JsonPatch.SystemTextJson/src/Adapters/AdapterFactory.cs b/src/Features/JsonPatch.SystemTextJson/src/Adapters/AdapterFactory.cs
new file mode 100644
index 000000000000..aff6f3a6bf5a
--- /dev/null
+++ b/src/Features/JsonPatch.SystemTextJson/src/Adapters/AdapterFactory.cs
@@ -0,0 +1,40 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Text.Json.Nodes;
+using Microsoft.AspNetCore.JsonPatch.SystemTextJson.Internal;
+using Microsoft.AspNetCore.Shared;
+
+namespace Microsoft.AspNetCore.JsonPatch.SystemTextJson.Adapters;
+
+///
+/// The default AdapterFactory to be used for resolving .
+///
+internal class AdapterFactory : IAdapterFactory
+{
+ internal static AdapterFactory Default { get; } = new();
+
+ ///
+ public virtual IAdapter Create(object target)
+ {
+ ArgumentNullThrowHelper.ThrowIfNull(target);
+
+ var typeToConvert = target.GetType();
+ if (typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(Dictionary<,>))
+ {
+ return (IAdapter)Activator.CreateInstance(typeof(DictionaryAdapter<,>).MakeGenericType(typeToConvert.GenericTypeArguments[0], typeToConvert.GenericTypeArguments[1]));
+ }
+
+ return target switch
+ {
+ JsonObject => new JsonObjectAdapter(),
+ JsonArray => new ListAdapter(),
+ IList => new ListAdapter(),
+ _ => new PocoAdapter()
+ };
+ }
+}
+
diff --git a/src/Features/JsonPatch.SystemTextJson/src/Adapters/IAdapterFactory.cs b/src/Features/JsonPatch.SystemTextJson/src/Adapters/IAdapterFactory.cs
new file mode 100644
index 000000000000..2afae2ee4e6c
--- /dev/null
+++ b/src/Features/JsonPatch.SystemTextJson/src/Adapters/IAdapterFactory.cs
@@ -0,0 +1,19 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.JsonPatch.SystemTextJson.Internal;
+
+namespace Microsoft.AspNetCore.JsonPatch.SystemTextJson.Adapters;
+
+///
+/// Defines the operations used for loading an based on the current object and ContractResolver.
+///
+internal interface IAdapterFactory
+{
+ ///
+ /// Creates an for the current object
+ ///
+ /// The target object
+ /// The needed
+ IAdapter Create(object target);
+}
diff --git a/src/Features/JsonPatch.SystemTextJson/src/Adapters/IObjectAdapter.cs b/src/Features/JsonPatch.SystemTextJson/src/Adapters/IObjectAdapter.cs
new file mode 100644
index 000000000000..4a20c424484d
--- /dev/null
+++ b/src/Features/JsonPatch.SystemTextJson/src/Adapters/IObjectAdapter.cs
@@ -0,0 +1,111 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.JsonPatch.SystemTextJson.Operations;
+
+namespace Microsoft.AspNetCore.JsonPatch.SystemTextJson.Adapters;
+
+///
+/// Defines the operations that can be performed on a JSON patch document.
+///
+public interface IObjectAdapter
+{
+ ///
+ /// Using the "add" operation a new value is inserted into the root of the target
+ /// document, into the target array at the specified valid index, or to a target object at
+ /// the specified location.
+ ///
+ /// When adding to arrays, the specified index MUST NOT be greater than the number of elements in the array.
+ /// To append the value to the array, the index of "-" character is used (see [RFC6901]).
+ ///
+ /// When adding to an object, if an object member does not already exist, a new member is added to the object at the
+ /// specified location or if an object member does exist, that member's value is replaced.
+ ///
+ /// The operation object MUST contain a "value" member whose content
+ /// specifies the value to be added.
+ ///
+ /// For example:
+ ///
+ /// { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] }
+ ///
+ /// See RFC 6902
+ ///
+ /// The add operation.
+ /// Object to apply the operation to.
+ void Add(Operation operation, object objectToApplyTo);
+
+ ///
+ /// Using the "copy" operation, a value is copied from a specified location to the
+ /// target location.
+ ///
+ /// The operation object MUST contain a "from" member, which references the location in the
+ /// target document to copy the value from.
+ ///
+ /// The "from" location MUST exist for the operation to be successful.
+ ///
+ /// For example:
+ ///
+ /// { "op": "copy", "from": "/a/b/c", "path": "/a/b/e" }
+ ///
+ /// See RFC 6902
+ ///
+ /// The copy operation.
+ /// Object to apply the operation to.
+ void Copy(Operation operation, object objectToApplyTo);
+
+ ///
+ /// Using the "move" operation the value at a specified location is removed and
+ /// added to the target location.
+ ///
+ /// The operation object MUST contain a "from" member, which references the location in the
+ /// target document to move the value from.
+ ///
+ /// The "from" location MUST exist for the operation to be successful.
+ ///
+ /// For example:
+ ///
+ /// { "op": "move", "from": "/a/b/c", "path": "/a/b/d" }
+ ///
+ /// A location cannot be moved into one of its children.
+ ///
+ /// See RFC 6902
+ ///
+ /// The move operation.
+ /// Object to apply the operation to.
+ void Move(Operation operation, object objectToApplyTo);
+
+ ///
+ /// Using the "remove" operation the value at the target location is removed.
+ ///
+ /// The target location MUST exist for the operation to be successful.
+ ///
+ /// For example:
+ ///
+ /// { "op": "remove", "path": "/a/b/c" }
+ ///
+ /// If removing an element from an array, any elements above the
+ /// specified index are shifted one position to the left.
+ ///
+ /// See RFC 6902
+ ///
+ /// The remove operation.
+ /// Object to apply the operation to.
+ void Remove(Operation operation, object objectToApplyTo);
+
+ ///
+ /// Using the "replace" operation the value at the target location is replaced
+ /// with a new value. The operation object MUST contain a "value" member
+ /// which specifies the replacement value.
+ ///
+ /// The target location MUST exist for the operation to be successful.
+ ///
+ /// For example:
+ ///
+ /// { "op": "replace", "path": "/a/b/c", "value": 42 }
+ ///
+ /// See RFC 6902
+ ///
+ /// The replace operation.
+ /// Object to apply the operation to.
+ void Replace(Operation operation, object objectToApplyTo);
+}
diff --git a/src/Features/JsonPatch.SystemTextJson/src/Adapters/IObjectAdapterWithTest.cs b/src/Features/JsonPatch.SystemTextJson/src/Adapters/IObjectAdapterWithTest.cs
new file mode 100644
index 000000000000..9dd4612a727f
--- /dev/null
+++ b/src/Features/JsonPatch.SystemTextJson/src/Adapters/IObjectAdapterWithTest.cs
@@ -0,0 +1,31 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.JsonPatch.SystemTextJson.Operations;
+
+namespace Microsoft.AspNetCore.JsonPatch.SystemTextJson.Adapters;
+
+///
+/// Defines the operations that can be performed on a JSON patch document, including "test".
+///
+public interface IObjectAdapterWithTest : IObjectAdapter
+{
+ ///
+ /// Using the "test" operation a value at the target location is compared for
+ /// equality to a specified value.
+ ///
+ /// The operation object MUST contain a "value" member that specifies
+ /// value to be compared to the target location's value.
+ ///
+ /// The target location MUST be equal to the "value" value for the
+ /// operation to be considered successful.
+ ///
+ /// For example:
+ /// { "op": "test", "path": "/a/b/c", "value": "foo" }
+ ///
+ /// See RFC 6902
+ ///
+ /// The test operation.
+ /// Object to apply the operation to.
+ void Test(Operation operation, object objectToApplyTo);
+}
diff --git a/src/Features/JsonPatch.SystemTextJson/src/Adapters/ObjectAdapter.cs b/src/Features/JsonPatch.SystemTextJson/src/Adapters/ObjectAdapter.cs
new file mode 100644
index 000000000000..708dd61dffeb
--- /dev/null
+++ b/src/Features/JsonPatch.SystemTextJson/src/Adapters/ObjectAdapter.cs
@@ -0,0 +1,280 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization.Metadata;
+using Microsoft.AspNetCore.JsonPatch.SystemTextJson.Internal;
+using Microsoft.AspNetCore.JsonPatch.SystemTextJson.Operations;
+using Microsoft.AspNetCore.Shared;
+
+namespace Microsoft.AspNetCore.JsonPatch.SystemTextJson.Adapters;
+
+///
+internal class ObjectAdapter : IObjectAdapterWithTest
+{
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The .
+ /// The for logging .
+ public ObjectAdapter(
+ JsonSerializerOptions serializerOptions,
+ Action logErrorAction) :
+ this(serializerOptions, logErrorAction, Adapters.AdapterFactory.Default)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The .
+ /// The for logging .
+ /// The to use when creating adaptors.
+ public ObjectAdapter(
+ JsonSerializerOptions serializerOptions,
+ Action logErrorAction,
+ IAdapterFactory adapterFactory)
+ {
+ SerializerOptions = serializerOptions ?? throw new ArgumentNullException(nameof(serializerOptions));
+ LogErrorAction = logErrorAction;
+ AdapterFactory = adapterFactory ?? throw new ArgumentNullException(nameof(adapterFactory));
+ }
+
+ ///
+ /// Gets or sets the .
+ ///
+ public JsonSerializerOptions SerializerOptions { get; }
+
+ ///
+ /// Gets or sets the
+ ///
+ public IAdapterFactory AdapterFactory { get; }
+
+ ///
+ /// Action for logging .
+ ///
+ public Action LogErrorAction { get; }
+
+ public void Add(Operation operation, object objectToApplyTo)
+ {
+ ArgumentNullThrowHelper.ThrowIfNull(operation);
+ ArgumentNullThrowHelper.ThrowIfNull(objectToApplyTo);
+
+ Add(operation.path, operation.value, objectToApplyTo, operation);
+ }
+
+ ///
+ /// Add is used by various operations (eg: add, copy, ...), yet through different operations;
+ /// This method allows code reuse yet reporting the correct operation on error
+ ///
+ private void Add(
+ string path,
+ object value,
+ object objectToApplyTo,
+ Operation operation)
+ {
+ ArgumentNullThrowHelper.ThrowIfNull(path);
+ ArgumentNullThrowHelper.ThrowIfNull(objectToApplyTo);
+ ArgumentNullThrowHelper.ThrowIfNull(operation);
+
+ var parsedPath = new ParsedPath(path);
+ var visitor = new ObjectVisitor(parsedPath, SerializerOptions, AdapterFactory);
+
+ var target = objectToApplyTo;
+ // Find the target object and the appropriate adapter
+ if (!visitor.TryVisit(ref target, out var adapter, out var errorMessage))
+ {
+ var error = CreatePathNotFoundError(objectToApplyTo, path, operation, errorMessage);
+ ErrorReporter(error);
+ return;
+ }
+
+ if (!adapter.TryAdd(target, parsedPath.LastSegment, SerializerOptions, value, out errorMessage))
+ {
+ var error = CreateOperationFailedError(objectToApplyTo, path, operation, errorMessage);
+ ErrorReporter(error);
+ return;
+ }
+ }
+
+ public void Move(Operation operation, object objectToApplyTo)
+ {
+ ArgumentNullThrowHelper.ThrowIfNull(operation);
+ ArgumentNullThrowHelper.ThrowIfNull(objectToApplyTo);
+
+ // Get value at 'from' location and add that value to the 'path' location
+ if (TryGetValue(operation.from, objectToApplyTo, operation, out var propertyValue))
+ {
+ // remove that value
+ Remove(operation.from, objectToApplyTo, operation);
+
+ // add that value to the path location
+ Add(operation.path, propertyValue, objectToApplyTo, operation);
+ }
+ }
+
+ public void Remove(Operation operation, object objectToApplyTo)
+ {
+ ArgumentNullThrowHelper.ThrowIfNull(operation);
+ ArgumentNullThrowHelper.ThrowIfNull(objectToApplyTo);
+
+ Remove(operation.path, objectToApplyTo, operation);
+ }
+
+ ///
+ /// Remove is used by various operations (eg: remove, move, ...), yet through different operations;
+ /// This method allows code reuse yet reporting the correct operation on error. The return value
+ /// contains the type of the item that has been removed (and a bool possibly signifying an error)
+ /// This can be used by other methods, like replace, to ensure that we can pass in the correctly
+ /// typed value to whatever method follows.
+ ///
+ private void Remove(string path, object objectToApplyTo, Operation operationToReport)
+ {
+ var parsedPath = new ParsedPath(path);
+ var visitor = new ObjectVisitor(parsedPath, SerializerOptions, AdapterFactory);
+
+ var target = objectToApplyTo;
+ if (!visitor.TryVisit(ref target, out var adapter, out var errorMessage))
+ {
+ var error = CreatePathNotFoundError(objectToApplyTo, path, operationToReport, errorMessage);
+ ErrorReporter(error);
+ return;
+ }
+
+ if (!adapter.TryRemove(target, parsedPath.LastSegment, SerializerOptions, out errorMessage))
+ {
+ var error = CreateOperationFailedError(objectToApplyTo, path, operationToReport, errorMessage);
+ ErrorReporter(error);
+ return;
+ }
+ }
+
+ public void Replace(Operation operation, object objectToApplyTo)
+ {
+ ArgumentNullThrowHelper.ThrowIfNull(operation);
+ ArgumentNullThrowHelper.ThrowIfNull(objectToApplyTo);
+
+ var parsedPath = new ParsedPath(operation.path);
+ var visitor = new ObjectVisitor(parsedPath, SerializerOptions, AdapterFactory);
+
+ var target = objectToApplyTo;
+ if (!visitor.TryVisit(ref target, out var adapter, out var errorMessage))
+ {
+ var error = CreatePathNotFoundError(objectToApplyTo, operation.path, operation, errorMessage);
+ ErrorReporter(error);
+ return;
+ }
+
+ if (!adapter.TryReplace(target, parsedPath.LastSegment, SerializerOptions, operation.value, out errorMessage))
+ {
+ var error = CreateOperationFailedError(objectToApplyTo, operation.path, operation, errorMessage);
+ ErrorReporter(error);
+ return;
+ }
+ }
+
+ public void Copy(Operation operation, object objectToApplyTo)
+ {
+ ArgumentNullThrowHelper.ThrowIfNull(operation);
+ ArgumentNullThrowHelper.ThrowIfNull(objectToApplyTo);
+
+ // Get value at 'from' location and add that value to the 'path' location
+ if (TryGetValue(operation.from, objectToApplyTo, operation, out var propertyValue))
+ {
+ // Create deep copy
+ var copyResult = ConversionResultProvider.CopyTo(propertyValue, propertyValue?.GetType(), SerializerOptions);
+ if (copyResult.CanBeConverted)
+ {
+ Add(operation.path, copyResult.ConvertedInstance, objectToApplyTo, operation);
+ }
+ else
+ {
+ var error = CreateOperationFailedError(objectToApplyTo, operation.path, operation, Resources.FormatCannotCopyProperty(operation.from));
+ ErrorReporter(error);
+ return;
+ }
+ }
+ }
+
+ public void Test(Operation operation, object objectToApplyTo)
+ {
+ ArgumentNullThrowHelper.ThrowIfNull(operation);
+ ArgumentNullThrowHelper.ThrowIfNull(objectToApplyTo);
+
+ var parsedPath = new ParsedPath(operation.path);
+ var visitor = new ObjectVisitor(parsedPath, SerializerOptions, AdapterFactory);
+
+ var target = objectToApplyTo;
+ if (!visitor.TryVisit(ref target, out var adapter, out var errorMessage))
+ {
+ var error = CreatePathNotFoundError(objectToApplyTo, operation.path, operation, errorMessage);
+ ErrorReporter(error);
+ return;
+ }
+
+ if (!adapter.TryTest(target, parsedPath.LastSegment, SerializerOptions, operation.value, out errorMessage))
+ {
+ var error = CreateOperationFailedError(objectToApplyTo, operation.path, operation, errorMessage);
+ ErrorReporter(error);
+ return;
+ }
+ }
+
+ private bool TryGetValue(
+ string fromLocation,
+ object objectToGetValueFrom,
+ Operation operation,
+ out object propertyValue)
+ {
+ ArgumentNullThrowHelper.ThrowIfNull(fromLocation);
+ ArgumentNullThrowHelper.ThrowIfNull(objectToGetValueFrom);
+ ArgumentNullThrowHelper.ThrowIfNull(operation);
+
+ propertyValue = null;
+
+ var parsedPath = new ParsedPath(fromLocation);
+ var visitor = new ObjectVisitor(parsedPath, SerializerOptions, AdapterFactory);
+
+ var target = objectToGetValueFrom;
+ if (!visitor.TryVisit(ref target, out var adapter, out var errorMessage))
+ {
+ var error = CreatePathNotFoundError(objectToGetValueFrom, fromLocation, operation, errorMessage);
+ ErrorReporter(error);
+ return false;
+ }
+
+ if (!adapter.TryGet(target, parsedPath.LastSegment, SerializerOptions, out propertyValue, out errorMessage))
+ {
+ var error = CreateOperationFailedError(objectToGetValueFrom, fromLocation, operation, errorMessage);
+ ErrorReporter(error);
+ return false;
+ }
+
+ return true;
+ }
+
+ private Action ErrorReporter
+ {
+ get
+ {
+ return LogErrorAction ?? Internal.ErrorReporter.Default;
+ }
+ }
+
+ private static JsonPatchError CreateOperationFailedError(object target, string path, Operation operation, string errorMessage)
+ {
+ return new JsonPatchError(
+ target,
+ operation,
+ errorMessage ?? Resources.FormatCannotPerformOperation(operation.op, path));
+ }
+
+ private static JsonPatchError CreatePathNotFoundError(object target, string path, Operation operation, string errorMessage)
+ {
+ return new JsonPatchError(
+ target,
+ operation,
+ errorMessage ?? Resources.FormatTargetLocationNotFound(operation.op, path));
+ }
+}
diff --git a/src/Features/JsonPatch.SystemTextJson/src/Converters/JsonConverterForJsonPatchDocumentOfT.cs b/src/Features/JsonPatch.SystemTextJson/src/Converters/JsonConverterForJsonPatchDocumentOfT.cs
new file mode 100644
index 000000000000..d6871a8b455e
--- /dev/null
+++ b/src/Features/JsonPatch.SystemTextJson/src/Converters/JsonConverterForJsonPatchDocumentOfT.cs
@@ -0,0 +1,69 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.AspNetCore.JsonPatch.SystemTextJson.Operations;
+
+namespace Microsoft.AspNetCore.JsonPatch.SystemTextJson.Converters;
+
+internal sealed class JsonConverterForJsonPatchDocumentOfT : JsonConverter>
+ where T : class
+{
+ private static JsonConverter> GetConverter(JsonSerializerOptions options) =>
+ (JsonConverter>)options.GetConverter(typeof(Operation));
+
+ public override JsonPatchDocument Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (typeToConvert != typeof(JsonPatchDocument))
+ {
+ throw new ArgumentException(Resources.FormatParameterMustMatchType(nameof(typeToConvert), nameof(JsonPatchDocument)), nameof(typeToConvert));
+ }
+
+ if (reader.TokenType != JsonTokenType.StartArray)
+ {
+ throw new JsonException(Resources.InvalidJsonPatchDocument);
+ }
+
+ if (reader.TokenType == JsonTokenType.Null)
+ {
+ return null;
+ }
+
+ List> ops = [];
+ try
+ {
+ JsonConverter> operationConverter = GetConverter(options);
+ while (reader.Read() && reader.TokenType is not JsonTokenType.EndArray)
+ {
+ var op = operationConverter.Read(ref reader, typeof(Operation), options);
+ ops.Add(op);
+ }
+
+ return new JsonPatchDocument(ops, options);
+ }
+ catch (Exception ex)
+ {
+ throw new JsonException(Resources.InvalidJsonPatchDocument, ex);
+ }
+ }
+
+ public override void Write(Utf8JsonWriter writer, JsonPatchDocument value, JsonSerializerOptions options)
+ {
+ if (value == null)
+ {
+ writer.WriteNullValue();
+ return;
+ }
+
+ JsonConverter> operationConverter = GetConverter(options);
+ writer.WriteStartArray();
+ foreach (var operation in value.Operations)
+ {
+ operationConverter.Write(writer, operation, options);
+ }
+ writer.WriteEndArray();
+ }
+}
diff --git a/src/Features/JsonPatch.SystemTextJson/src/Converters/JsonPatchDocumentConverter.cs b/src/Features/JsonPatch.SystemTextJson/src/Converters/JsonPatchDocumentConverter.cs
new file mode 100644
index 000000000000..554fe8d12eb8
--- /dev/null
+++ b/src/Features/JsonPatch.SystemTextJson/src/Converters/JsonPatchDocumentConverter.cs
@@ -0,0 +1,71 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.AspNetCore.JsonPatch.SystemTextJson.Operations;
+
+namespace Microsoft.AspNetCore.JsonPatch.SystemTextJson.Converters;
+
+internal class JsonPatchDocumentConverter : JsonConverter
+{
+ private static JsonConverter GetConverter(JsonSerializerOptions options) =>
+ (JsonConverter)options.GetConverter(typeof(Operation));
+
+ public override JsonPatchDocument Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (typeToConvert != typeof(JsonPatchDocument))
+ {
+ throw new ArgumentException(Resources.FormatParameterMustMatchType(nameof(typeToConvert), nameof(JsonPatchDocument)), nameof(typeToConvert));
+ }
+
+ if (reader.TokenType != JsonTokenType.StartArray)
+ {
+ throw new JsonException(Resources.InvalidJsonPatchDocument);
+ }
+
+ if (reader.TokenType == JsonTokenType.Null)
+ {
+ return null;
+ }
+
+ List ops = [];
+
+ try
+ {
+ JsonConverter operationConverter = GetConverter(options);
+ while (reader.Read() && reader.TokenType is not JsonTokenType.EndArray)
+ {
+ var op = operationConverter.Read(ref reader, typeof(Operation), options);
+ ops.Add(op);
+ }
+
+ return new JsonPatchDocument(ops, options);
+ }
+ catch (Exception ex)
+ {
+ throw new JsonException(Resources.InvalidJsonPatchDocument, ex);
+ }
+ }
+
+ public override void Write(Utf8JsonWriter writer, JsonPatchDocument value, JsonSerializerOptions options)
+ {
+ if (value == null)
+ {
+ writer.WriteNullValue();
+ return;
+ }
+
+ JsonConverter operationConverter = GetConverter(options);
+
+ writer.WriteStartArray();
+ foreach (var operation in value.Operations)
+ {
+ operationConverter.Write(writer, operation, options);
+ }
+
+ writer.WriteEndArray();
+ }
+}
diff --git a/src/Features/JsonPatch.SystemTextJson/src/Converters/JsonPatchDocumentConverterFactory.cs b/src/Features/JsonPatch.SystemTextJson/src/Converters/JsonPatchDocumentConverterFactory.cs
new file mode 100644
index 000000000000..d862cb97df80
--- /dev/null
+++ b/src/Features/JsonPatch.SystemTextJson/src/Converters/JsonPatchDocumentConverterFactory.cs
@@ -0,0 +1,27 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.AspNetCore.JsonPatch.SystemTextJson.Converters;
+
+internal class JsonPatchDocumentConverterFactory : JsonConverterFactory
+{
+ public override bool CanConvert(Type typeToConvert)
+ {
+ return typeToConvert == typeof(JsonPatchDocument) ||
+ (typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(JsonPatchDocument<>));
+ }
+
+ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (typeToConvert == typeof(JsonPatchDocument))
+ {
+ return new JsonPatchDocumentConverter();
+ }
+
+ return (JsonConverter)Activator.CreateInstance(typeof(JsonConverterForJsonPatchDocumentOfT<>).MakeGenericType(typeToConvert.GenericTypeArguments[0]));
+ }
+}
diff --git a/src/Features/JsonPatch.SystemTextJson/src/Exceptions/JsonPatchException.cs b/src/Features/JsonPatch.SystemTextJson/src/Exceptions/JsonPatchException.cs
new file mode 100644
index 000000000000..a772e4393773
--- /dev/null
+++ b/src/Features/JsonPatch.SystemTextJson/src/Exceptions/JsonPatchException.cs
@@ -0,0 +1,34 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using Microsoft.AspNetCore.JsonPatch.SystemTextJson.Operations;
+
+namespace Microsoft.AspNetCore.JsonPatch.SystemTextJson.Exceptions;
+
+public class JsonPatchException : Exception
+{
+ public Operation FailedOperation { get; private set; }
+ public object AffectedObject { get; private set; }
+
+ public JsonPatchException()
+ {
+ }
+
+ public JsonPatchException(JsonPatchError jsonPatchError, Exception innerException)
+ : base(jsonPatchError.ErrorMessage, innerException)
+ {
+ FailedOperation = jsonPatchError.Operation;
+ AffectedObject = jsonPatchError.AffectedObject;
+ }
+
+ public JsonPatchException(JsonPatchError jsonPatchError)
+ : this(jsonPatchError, null)
+ {
+ }
+
+ public JsonPatchException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+}
diff --git a/src/Features/JsonPatch.SystemTextJson/src/Helpers/GenericListOrJsonArrayUtilities.cs b/src/Features/JsonPatch.SystemTextJson/src/Helpers/GenericListOrJsonArrayUtilities.cs
new file mode 100644
index 000000000000..a82ee38ab951
--- /dev/null
+++ b/src/Features/JsonPatch.SystemTextJson/src/Helpers/GenericListOrJsonArrayUtilities.cs
@@ -0,0 +1,105 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections;
+using System.Text.Json.Nodes;
+
+namespace Microsoft.AspNetCore.JsonPatch.SystemTextJson.Helpers;
+
+internal static class GenericListOrJsonArrayUtilities
+{
+ internal static object GetElementAt(object list, int index)
+ {
+ if (list is IList nonGenericList)
+ {
+ return nonGenericList[index];
+ }
+
+ if (list is JsonArray array)
+ {
+ return array[index];
+ }
+
+ throw new InvalidOperationException($"Unsupported list type: {list.GetType()}");
+ }
+
+ internal static void SetValueAt(object list, int index, object value)
+ {
+ if (list is IList nonGenericList)
+ {
+ nonGenericList[index] = value;
+ }
+ else if (list is JsonArray array)
+ {
+ array[index] = (JsonNode)value;
+ }
+ else
+ {
+ throw new InvalidOperationException($"Unsupported list type: {list.GetType()}");
+ }
+ }
+
+ internal static int GetCount(object list)
+ {
+ if (list is ICollection nonGenericList)
+ {
+ return nonGenericList.Count;
+ }
+
+ if (list is JsonArray jsonArray)
+ {
+ return jsonArray.Count;
+ }
+
+ throw new InvalidOperationException($"Unsupported list type: {list.GetType()}");
+ }
+
+ internal static void RemoveElementAt(object list, int index)
+ {
+ if (list is IList nonGenericList)
+ {
+ nonGenericList.RemoveAt(index);
+ }
+ else if (list is JsonArray array)
+ {
+ array.RemoveAt(index);
+ }
+ else
+ {
+ throw new InvalidOperationException($"Unsupported list type: {list.GetType()}");
+ }
+ }
+
+ internal static void InsertElementAt(object list, int index, object value)
+ {
+ if (list is IList nonGenericList)
+ {
+ nonGenericList.Insert(index, value);
+ }
+ else if (list is JsonArray array)
+ {
+ array.Insert(index, (JsonNode)value);
+ }
+ else
+ {
+ throw new InvalidOperationException($"Unsupported list type: {list.GetType()}");
+ }
+ }
+
+ internal static void AddElement(object list, object value)
+ {
+ if (list is IList nonGenericList)
+ {
+ nonGenericList.Add(value);
+ }
+ else if (list is JsonArray array)
+ {
+ array.Add(value);
+ }
+ else
+ {
+ throw new InvalidOperationException($"Unsupported list type: {list.GetType()}");
+ }
+ }
+}
diff --git a/src/Features/JsonPatch.SystemTextJson/src/Helpers/JsonUtilities.cs b/src/Features/JsonPatch.SystemTextJson/src/Helpers/JsonUtilities.cs
new file mode 100644
index 000000000000..7c223fff2093
--- /dev/null
+++ b/src/Features/JsonPatch.SystemTextJson/src/Helpers/JsonUtilities.cs
@@ -0,0 +1,51 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+
+namespace Microsoft.AspNetCore.JsonPatch.SystemTextJson.Helpers;
+
+internal static class JsonUtilities
+{
+ public static bool DeepEquals(object a, object b, JsonSerializerOptions serializerOptions)
+ {
+ if (a == null && b == null)
+ {
+ return true;
+ }
+
+ if (a == null || b == null)
+ {
+ return false;
+ }
+
+ if (a is JsonNode nodeA && b is JsonNode nodeB)
+ {
+ return JsonNode.DeepEquals(nodeA, nodeB);
+ }
+
+ using var docA = TryGetJsonElement(a, serializerOptions, out var elementA);
+ using var docB = TryGetJsonElement(b, serializerOptions, out var elementB);
+
+ return JsonElement.DeepEquals(elementA, elementB);
+ }
+
+ private static IDisposable TryGetJsonElement(object item, JsonSerializerOptions serializerOptions, out JsonElement element)
+ {
+ IDisposable result = null;
+ if (item is JsonElement jsonElement)
+ {
+ element = jsonElement;
+ }
+ else
+ {
+ var docA = JsonSerializer.SerializeToDocument(item, serializerOptions);
+ element = docA.RootElement;
+ result = docA;
+ }
+
+ return result;
+ }
+}
diff --git a/src/Features/JsonPatch.SystemTextJson/src/IJsonPatchDocument.cs b/src/Features/JsonPatch.SystemTextJson/src/IJsonPatchDocument.cs
new file mode 100644
index 000000000000..01aafc5431a7
--- /dev/null
+++ b/src/Features/JsonPatch.SystemTextJson/src/IJsonPatchDocument.cs
@@ -0,0 +1,15 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Text.Json;
+using Microsoft.AspNetCore.JsonPatch.SystemTextJson.Operations;
+
+namespace Microsoft.AspNetCore.JsonPatch.SystemTextJson;
+
+public interface IJsonPatchDocument
+{
+ JsonSerializerOptions SerializerOptions { get; set; }
+
+ IList GetOperations();
+}
diff --git a/src/Features/JsonPatch.SystemTextJson/src/Internal/ConversionResult.cs b/src/Features/JsonPatch.SystemTextJson/src/Internal/ConversionResult.cs
new file mode 100644
index 000000000000..57d88953acea
--- /dev/null
+++ b/src/Features/JsonPatch.SystemTextJson/src/Internal/ConversionResult.cs
@@ -0,0 +1,16 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.JsonPatch.SystemTextJson.Internal;
+
+internal class ConversionResult
+{
+ public ConversionResult(bool canBeConverted, object convertedInstance)
+ {
+ CanBeConverted = canBeConverted;
+ ConvertedInstance = convertedInstance;
+ }
+
+ public bool CanBeConverted { get; }
+ public object ConvertedInstance { get; }
+}
diff --git a/src/Features/JsonPatch.SystemTextJson/src/Internal/ConversionResultProvider.cs b/src/Features/JsonPatch.SystemTextJson/src/Internal/ConversionResultProvider.cs
new file mode 100644
index 000000000000..1ef5c87a92e7
--- /dev/null
+++ b/src/Features/JsonPatch.SystemTextJson/src/Internal/ConversionResultProvider.cs
@@ -0,0 +1,85 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Text.Json;
+
+namespace Microsoft.AspNetCore.JsonPatch.SystemTextJson.Internal;
+
+internal static class ConversionResultProvider
+{
+ internal static ConversionResult ConvertTo(object value, Type typeToConvertTo, JsonSerializerOptions serializerOptions)
+ {
+ if (value == null)
+ {
+ return new ConversionResult(IsNullableType(typeToConvertTo), null);
+ }
+
+ if (typeToConvertTo.IsAssignableFrom(value.GetType()))
+ {
+ // No need to convert
+ return new ConversionResult(true, value);
+ }
+
+ return GetConvertedValue(value, serializerOptions, ref typeToConvertTo);
+ }
+
+ internal static ConversionResult CopyTo(object value, Type typeToConvertTo, JsonSerializerOptions serializerOptions)
+ {
+ var targetType = typeToConvertTo;
+ if (value == null)
+ {
+ return new ConversionResult(canBeConverted: true, convertedInstance: null);
+ }
+
+ if (typeToConvertTo != value.GetType() && typeToConvertTo.IsAssignableFrom(value.GetType()))
+ {
+ // Keep original type
+ targetType = value.GetType();
+ }
+
+ return GetConvertedValue(value, serializerOptions, ref targetType);
+ }
+
+ private static ConversionResult GetConvertedValue(object value, JsonSerializerOptions serializerOptions, ref Type targetType)
+ {
+ // Workaround for the https://github.com/dotnet/runtime/issues/113926
+ if (targetType.Name == "JsonValuePrimitive`1")
+ {
+ targetType = typeof(JsonElement);
+ }
+
+ try
+ {
+ var deserialized = ConvertToTargetType(value, targetType, serializerOptions);
+ return new ConversionResult(true, deserialized);
+ }
+ catch
+ {
+ return new ConversionResult(canBeConverted: false, convertedInstance: null);
+ }
+ }
+
+ private static bool IsNullableType(Type type)
+ {
+ if (type.IsValueType)
+ {
+ // value types are only nullable if they are Nullable
+ return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
+ }
+
+ // reference types are always nullable
+ return true;
+ }
+
+ private static object ConvertToTargetType(object value, Type targetType, JsonSerializerOptions serializerOptions)
+ {
+ if (value is JsonElement jsonElement)
+ {
+ return JsonSerializer.Deserialize(jsonElement, targetType, serializerOptions);
+ }
+
+ using JsonDocument doc = JsonSerializer.SerializeToDocument(value, serializerOptions);
+ return JsonSerializer.Deserialize(doc.RootElement, targetType, serializerOptions);
+ }
+}
diff --git a/src/Features/JsonPatch.SystemTextJson/src/Internal/DictionaryAdapterOfTU.cs b/src/Features/JsonPatch.SystemTextJson/src/Internal/DictionaryAdapterOfTU.cs
new file mode 100644
index 000000000000..c0540d4c4cd0
--- /dev/null
+++ b/src/Features/JsonPatch.SystemTextJson/src/Internal/DictionaryAdapterOfTU.cs
@@ -0,0 +1,235 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using Microsoft.AspNetCore.JsonPatch.SystemTextJson.Helpers;
+
+namespace Microsoft.AspNetCore.JsonPatch.SystemTextJson.Internal;
+
+internal class DictionaryAdapter : IAdapter
+{
+ public virtual bool TryAdd(
+ object target,
+ string segment,
+ JsonSerializerOptions serializerOptions,
+ object value,
+ out string errorMessage)
+ {
+ var key = segment;
+ var dictionary = (IDictionary)target;
+
+ // As per JsonPatch spec, if a key already exists, adding should replace the existing value
+ if (!TryConvertKey(key, out var convertedKey, out errorMessage))
+ {
+ return false;
+ }
+
+ if (!TryConvertValue(value, serializerOptions, out var convertedValue, out errorMessage))
+ {
+ return false;
+ }
+
+ dictionary[convertedKey] = convertedValue;
+ errorMessage = null;
+ return true;
+ }
+
+ public virtual bool TryGet(
+ object target,
+ string segment,
+ JsonSerializerOptions serializerOptions,
+ out object value,
+ out string errorMessage)
+ {
+ var key = segment;
+ var dictionary = (IDictionary)target;
+
+ if (!TryConvertKey(key, out var convertedKey, out errorMessage))
+ {
+ value = null;
+ return false;
+ }
+
+ if (!dictionary.TryGetValue(convertedKey, out var valueAsT))
+ {
+ value = null;
+ errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
+ return false;
+ }
+
+ value = valueAsT;
+ errorMessage = null;
+ return true;
+ }
+
+ public virtual bool TryRemove(
+ object target,
+ string segment,
+ JsonSerializerOptions serializerOptions,
+ out string errorMessage)
+ {
+ var key = segment;
+ var dictionary = (IDictionary)target;
+
+ if (!TryConvertKey(key, out var convertedKey, out errorMessage))
+ {
+ return false;
+ }
+
+ // As per JsonPatch spec, the target location must exist for remove to be successful
+ if (!dictionary.Remove(convertedKey))
+ {
+ errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
+ return false;
+ }
+
+ errorMessage = null;
+ return true;
+ }
+
+ public virtual bool TryReplace(
+ object target,
+ string segment,
+ JsonSerializerOptions serializerOptions,
+ object value,
+ out string errorMessage)
+ {
+ var key = segment;
+ var dictionary = (IDictionary)target;
+
+ if (!TryConvertKey(key, out var convertedKey, out errorMessage))
+ {
+ return false;
+ }
+
+ // As per JsonPatch spec, the target location must exist for remove to be successful
+ if (!dictionary.ContainsKey(convertedKey))
+ {
+ errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
+ return false;
+ }
+
+ if (!TryConvertValue(value, serializerOptions, out var convertedValue, out errorMessage))
+ {
+ return false;
+ }
+
+ dictionary[convertedKey] = convertedValue;
+
+ errorMessage = null;
+ return true;
+ }
+
+ public virtual bool TryTest(
+ object target,
+ string segment,
+ JsonSerializerOptions serializerOptions,
+ object value,
+ out string errorMessage)
+ {
+ var key = segment;
+ var dictionary = (IDictionary)target;
+
+ if (!TryConvertKey(key, out var convertedKey, out errorMessage))
+ {
+ return false;
+ }
+
+ // As per JsonPatch spec, the target location must exist for test to be successful
+ if (!dictionary.TryGetValue(convertedKey, out var currentValue))
+ {
+ errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
+ return false;
+ }
+
+ if (!TryConvertValue(value, serializerOptions, out var convertedValue, out errorMessage))
+ {
+ return false;
+ }
+
+ // The target segment does not have an assigned value to compare the test value with
+ if (currentValue == null)
+ {
+ errorMessage = Resources.FormatValueForTargetSegmentCannotBeNullOrEmpty(segment);
+ return false;
+ }
+
+ if (!JsonUtilities.DeepEquals(currentValue, convertedValue, serializerOptions))
+ {
+ errorMessage = Resources.FormatValueNotEqualToTestValue(currentValue, value, segment);
+ return false;
+ }
+ else
+ {
+ errorMessage = null;
+ return true;
+ }
+ }
+
+ public virtual bool TryTraverse(
+ object target,
+ string segment,
+ JsonSerializerOptions serializerOptions,
+ out object nextTarget,
+ out string errorMessage)
+ {
+ var key = segment;
+ var dictionary = (IDictionary)target;
+
+ if (!TryConvertKey(key, out var convertedKey, out errorMessage))
+ {
+ nextTarget = null;
+ return false;
+ }
+
+ if (dictionary.TryGetValue(convertedKey, out var valueAsT))
+ {
+ nextTarget = valueAsT;
+ errorMessage = null;
+ return true;
+ }
+ else
+ {
+ nextTarget = null;
+ errorMessage = null;
+ return false;
+ }
+ }
+
+ private static bool TryConvertKey(string key, out TKey convertedKey, out string errorMessage)
+ {
+ var options = new JsonSerializerOptions() { NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString };
+ var conversionResult = ConversionResultProvider.ConvertTo(key, typeof(TKey), options);
+ if (conversionResult.CanBeConverted)
+ {
+ errorMessage = null;
+ convertedKey = (TKey)conversionResult.ConvertedInstance;
+ return true;
+ }
+ else
+ {
+ errorMessage = Resources.FormatInvalidPathSegment(key);
+ convertedKey = default(TKey);
+ return false;
+ }
+ }
+
+ private static bool TryConvertValue(object value, JsonSerializerOptions serializerOptions, out TValue convertedValue, out string errorMessage)
+ {
+ var conversionResult = ConversionResultProvider.ConvertTo(value, typeof(TValue), serializerOptions);
+ if (conversionResult.CanBeConverted)
+ {
+ errorMessage = null;
+ convertedValue = (TValue)conversionResult.ConvertedInstance;
+ return true;
+ }
+ else
+ {
+ errorMessage = Resources.FormatInvalidValueForProperty(value);
+ convertedValue = default(TValue);
+ return false;
+ }
+ }
+}
diff --git a/src/Features/JsonPatch.SystemTextJson/src/Internal/ErrorReporter.cs b/src/Features/JsonPatch.SystemTextJson/src/Internal/ErrorReporter.cs
new file mode 100644
index 000000000000..2b32156b2d90
--- /dev/null
+++ b/src/Features/JsonPatch.SystemTextJson/src/Internal/ErrorReporter.cs
@@ -0,0 +1,15 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using Microsoft.AspNetCore.JsonPatch.SystemTextJson.Exceptions;
+
+namespace Microsoft.AspNetCore.JsonPatch.SystemTextJson.Internal;
+
+internal static class ErrorReporter
+{
+ public static readonly Action Default = (error) =>
+ {
+ throw new JsonPatchException(error);
+ };
+}
diff --git a/src/Features/JsonPatch.SystemTextJson/src/Internal/IAdapter.cs b/src/Features/JsonPatch.SystemTextJson/src/Internal/IAdapter.cs
new file mode 100644
index 000000000000..5e1dbceb75a7
--- /dev/null
+++ b/src/Features/JsonPatch.SystemTextJson/src/Internal/IAdapter.cs
@@ -0,0 +1,50 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text.Json;
+
+namespace Microsoft.AspNetCore.JsonPatch.SystemTextJson.Internal;
+
+internal interface IAdapter
+{
+ bool TryTraverse(
+ object target,
+ string segment,
+ JsonSerializerOptions serializerOptions,
+ out object nextTarget,
+ out string errorMessage);
+
+ bool TryAdd(
+ object target,
+ string segment,
+ JsonSerializerOptions serializerOptions,
+ object value,
+ out string errorMessage);
+
+ bool TryRemove(
+ object target,
+ string segment,
+ JsonSerializerOptions serializerOptions,
+ out string errorMessage);
+
+ bool TryGet(
+ object target,
+ string segment,
+ JsonSerializerOptions serializerOptions,
+ out object value,
+ out string errorMessage);
+
+ bool TryReplace(
+ object target,
+ string segment,
+ JsonSerializerOptions serializerOptions,
+ object value,
+ out string errorMessage);
+
+ bool TryTest(
+ object target,
+ string segment,
+ JsonSerializerOptions serializerOptions,
+ object value,
+ out string errorMessage);
+}
diff --git a/src/Features/JsonPatch.SystemTextJson/src/Internal/JsonObjectAdapter.cs b/src/Features/JsonPatch.SystemTextJson/src/Internal/JsonObjectAdapter.cs
new file mode 100644
index 000000000000..7db344c78545
--- /dev/null
+++ b/src/Features/JsonPatch.SystemTextJson/src/Internal/JsonObjectAdapter.cs
@@ -0,0 +1,142 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using Microsoft.AspNetCore.JsonPatch.SystemTextJson.Helpers;
+
+namespace Microsoft.AspNetCore.JsonPatch.SystemTextJson.Internal;
+
+internal class JsonObjectAdapter : IAdapter
+{
+ public virtual bool TryAdd(
+ object target,
+ string segment,
+ JsonSerializerOptions serializerOptions,
+ object value,
+ out string errorMessage)
+ {
+ // Set the property specified by the `segment` argument to the given `value` of the `target` object.
+ var obj = (JsonObject)target;
+
+ obj[segment] = value != null ? JsonSerializer.SerializeToNode(value, serializerOptions) : GetJsonNull();
+
+ errorMessage = null;
+ return true;
+ }
+
+ public virtual bool TryGet(
+ object target,
+ string segment,
+ JsonSerializerOptions serializerOptions,
+ out object value,
+ out string errorMessage)
+ {
+ var obj = (JsonObject)target;
+
+ if (!obj.TryGetPropertyValue(segment, out var valueAsToken))
+ {
+ value = null;
+ errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
+ return false;
+ }
+
+ value = valueAsToken;
+ errorMessage = null;
+ return true;
+ }
+
+ public virtual bool TryRemove(
+ object target,
+ string segment,
+ JsonSerializerOptions serializerOptions,
+ out string errorMessage)
+ {
+ var obj = (JsonObject)target;
+
+ if (!obj.Remove(segment))
+ {
+ errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
+ return false;
+ }
+
+ errorMessage = null;
+ return true;
+ }
+
+ public virtual bool TryReplace(
+ object target,
+ string segment,
+ JsonSerializerOptions serializerOptions,
+ object value,
+ out string errorMessage)
+ {
+ var obj = (JsonObject)target;
+
+ int index = obj.IndexOf(segment);
+ if (index == -1)
+ {
+ errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
+ return false;
+ }
+
+ obj[index] = value != null ? JsonSerializer.SerializeToNode(value, serializerOptions) : GetJsonNull();
+
+ errorMessage = null;
+ return true;
+ }
+
+ public virtual bool TryTest(
+ object target,
+ string segment,
+ JsonSerializerOptions serializerOptions,
+ object value,
+ out string errorMessage)
+ {
+ var obj = (JsonObject)target;
+
+ if (!obj.TryGetPropertyValue(segment, out var currentValue))
+ {
+ errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
+ return false;
+ }
+
+ if (currentValue == null || string.IsNullOrEmpty(currentValue.ToString()))
+ {
+ errorMessage = Resources.FormatValueForTargetSegmentCannotBeNullOrEmpty(segment);
+ return false;
+ }
+
+ if (!JsonUtilities.DeepEquals(currentValue, value, serializerOptions))
+ {
+ errorMessage = Resources.FormatValueNotEqualToTestValue(currentValue, value, segment);
+ return false;
+ }
+
+ errorMessage = null;
+ return true;
+ }
+
+ public virtual bool TryTraverse(
+ object target,
+ string segment,
+ JsonSerializerOptions serializerOptions,
+ out object nextTarget,
+ out string errorMessage)
+ {
+ var obj = (JsonObject)target;
+
+ if (!obj.TryGetPropertyValue(segment, out var nextTargetToken))
+ {
+ nextTarget = null;
+ errorMessage = null;
+ return false;
+ }
+
+ nextTarget = nextTargetToken;
+ errorMessage = null;
+ return true;
+ }
+
+ private static JsonValue GetJsonNull() => JsonValue.Create