Add SystemTextJsonMatchers (#1447)

* SystemTextJsonMatcher

* ,

* .

* new projectx

* Update test/WireMock.Net.Tests/Pact/PactTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update test/WireMock.Net.Tests/WebSockets/WebSocketIntegrationTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/WireMock.Net/WireMock.Net.csproj

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/WireMock.Net.Minimal/Properties/AssemblyInfo.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* .

* more tests

* .

* .

* x

* ...

* .

* fix tests

* 0.11.0

* .

* delete jsonutils.cs

* s

* fix findings

* Apply suggestions from code review

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* Potential fix for pull request finding 'Missing Dispose call on local IDisposable'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* JsonConverter 0.12.0

* -- tools

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
This commit is contained in:
Stef Heyenrath
2026-05-29 20:19:51 +02:00
committed by GitHub
co-authored by Copilot Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
parent fed1c87663
commit 36b0a93a6c
68 changed files with 4202 additions and 976 deletions
@@ -0,0 +1,28 @@
// Copyright © WireMock.Net
using JetBrains.Annotations;
using WireMock.Types;
using WireMock.Util;
namespace WireMock.Transformers;
/// <summary>
/// Defines the contract for transforming JSON-like body data using a transformer context.
/// </summary>
[PublicAPI]
public interface IJsonBodyTransformer
{
/// <summary>
/// Transforms the JSON body using the provided transformer context and model.
/// </summary>
/// <param name="transformerContext">The transformer context used to render and evaluate template values.</param>
/// <param name="options">The JSON node replacement behavior to apply during transformation.</param>
/// <param name="model">The model used when rendering or evaluating template values.</param>
/// <param name="original">The original body data to transform.</param>
/// <returns>The transformed JSON body data.</returns>
BodyData TransformBodyAsJson(
ITransformerContext transformerContext,
ReplaceNodeOptions options,
object model,
IBodyData original);
}
@@ -0,0 +1,34 @@
// Copyright © WireMock.Net
using JetBrains.Annotations;
using WireMock.Handlers;
namespace WireMock.Transformers;
/// <summary>
/// Defines the transformer context used to render and evaluate templates during response transformation.
/// </summary>
[PublicAPI]
public interface ITransformerContext
{
/// <summary>
/// Gets the file system handler used by the current transformer context.
/// </summary>
IFileSystemHandler FileSystemHandler { get; }
/// <summary>
/// Renders the specified template text using the supplied model.
/// </summary>
/// <param name="text">The template text to render.</param>
/// <param name="model">The model used during rendering.</param>
/// <returns>The rendered text.</returns>
string ParseAndRender(string text, object model);
/// <summary>
/// Evaluates the specified template text using the supplied model.
/// </summary>
/// <param name="text">The template text to evaluate.</param>
/// <param name="model">The model used during evaluation.</param>
/// <returns>The evaluated value.</returns>
object? ParseAndEvaluate(string text, object model);
}
@@ -0,0 +1,271 @@
// Copyright © WireMock.Net
using System.Collections;
using HandlebarsDotNet.Helpers.Models;
using JetBrains.Annotations;
using JsonConverter.Newtonsoft.Json;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using WireMock.Settings;
using WireMock.Types;
using WireMock.Util;
namespace WireMock.Transformers;
/// <summary>
/// Default JSON body transformer implementation based on Newtonsoft.Json.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="NewtonsoftJsonBodyTransformer"/> class.
/// </remarks>
/// <param name="settings">The server settings used to configure JSON transformation behavior.</param>
[PublicAPI]
public class NewtonsoftJsonBodyTransformer(WireMockServerSettings settings) : IJsonBodyTransformer
{
private static readonly NewtonsoftJsonConverter _jsonConverter = new();
/// <inheritdoc />
public BodyData TransformBodyAsJson(
ITransformerContext transformerContext,
ReplaceNodeOptions options,
object model,
IBodyData original)
{
var jsonSerializer = new JsonSerializer
{
Culture = settings.Culture
};
JToken? jToken = null;
switch (original.BodyAsJson)
{
case JObject bodyAsJObject:
jToken = bodyAsJObject.DeepClone();
WalkNode(transformerContext, jsonSerializer, options, jToken, model);
break;
case JArray bodyAsJArray:
jToken = bodyAsJArray.DeepClone();
WalkNode(transformerContext, jsonSerializer, options, jToken, model);
break;
case var bodyAsEnumerable when bodyAsEnumerable is IEnumerable and not string:
jToken = JArray.FromObject(bodyAsEnumerable, jsonSerializer);
WalkNode(transformerContext, jsonSerializer, options, jToken, model);
break;
case string bodyAsString:
jToken = ReplaceSingleNode(transformerContext, jsonSerializer, options, bodyAsString, model);
break;
case not null:
jToken = JObject.FromObject(original.BodyAsJson, jsonSerializer);
WalkNode(transformerContext, jsonSerializer, options, jToken, model);
break;
}
return new BodyData
{
Encoding = original.Encoding,
DetectedBodyType = original.DetectedBodyType,
DetectedBodyTypeFromContentType = original.DetectedBodyTypeFromContentType,
ProtoDefinition = original.ProtoDefinition,
ProtoBufMessageType = original.ProtoBufMessageType,
BodyAsJson = jToken
};
}
private JToken ParseAsJObject(string stringValue)
{
if (_jsonConverter.IsValidJson(stringValue))
{
try
{
// Try to convert this string into a JObject
return JObject.Parse(stringValue!);
}
catch
{
settings.Logger.Warn("Failed to parse string ''{0}'' as JSON. Returning the original string value.", stringValue);
}
}
return stringValue;
}
private JToken ReplaceSingleNode(ITransformerContext transformerContext, JsonSerializer jsonSerializer, ReplaceNodeOptions options, string stringValue, object model)
{
var transformedString = transformerContext.ParseAndRender(stringValue, model);
if (!string.Equals(stringValue, transformedString))
{
const string property = "_";
JObject dummy = JObject.Parse($"{{ \"{property}\": null }}");
if (dummy[property] == null)
{
return string.Empty;
}
JToken node = dummy[property]!;
ReplaceNodeValue(jsonSerializer, options, node, transformedString);
return dummy[property]!;
}
return stringValue;
}
private void WalkNode(ITransformerContext transformerContext, JsonSerializer jsonSerializer, ReplaceNodeOptions options, JToken node, object model)
{
switch (node.Type)
{
case JTokenType.Object:
foreach (var child in node.Children<JProperty>().ToArray())
{
WalkNode(transformerContext, jsonSerializer, options, child.Value, model);
}
break;
case JTokenType.Array:
foreach (var child in node.Children().ToArray())
{
WalkNode(transformerContext, jsonSerializer, options, child, model);
}
break;
case JTokenType.String:
var stringValue = node.Value<string>();
if (string.IsNullOrEmpty(stringValue))
{
return;
}
var transformed = transformerContext.ParseAndEvaluate(stringValue!, model);
if (!Equals(stringValue, transformed))
{
ReplaceNodeValue(jsonSerializer, options, node, transformed);
}
break;
}
}
private void ReplaceNodeValue(JsonSerializer jsonSerializer, ReplaceNodeOptions options, JToken node, object? transformedValue)
{
switch (transformedValue)
{
case JValue jValue:
node.Replace(jValue);
return;
case string transformedString:
var (isConvertedFromString, convertedValueFromString) = TryConvert(options, transformedString);
if (isConvertedFromString)
{
node.Replace(JToken.FromObject(convertedValueFromString, jsonSerializer));
}
else
{
node.Replace(ParseAsJObject(transformedString));
}
break;
case WireMockList<string> strings:
switch (strings.Count)
{
case 1:
node.Replace(ParseAsJObject(strings[0]));
return;
case > 1:
node.Replace(JToken.FromObject(strings.ToArray(), jsonSerializer));
return;
}
break;
case { }:
var (isConverted, convertedValue) = TryConvert(options, transformedValue);
if (isConverted)
{
node.Replace(JToken.FromObject(convertedValue, jsonSerializer));
}
return;
default:
return;
}
}
private static (bool IsConverted, object ConvertedValue) TryConvert(ReplaceNodeOptions options, object value)
{
var valueAsString = value as string;
if (options == ReplaceNodeOptions.Evaluate)
{
if (valueAsString != null && WrappedString.TryDecode(valueAsString, out var decoded))
{
return (true, decoded);
}
return (false, value);
}
if (valueAsString != null)
{
return WrappedString.TryDecode(valueAsString, out var decoded)
? (true, decoded)
: TryConvertToKnownType(valueAsString);
}
return (false, value);
}
internal static (bool IsConverted, object ConvertedValue) TryConvertToKnownType(string value)
{
if (bool.TryParse(value, out var boolResult))
{
return (true, boolResult);
}
if (int.TryParse(value, out var intResult))
{
return (true, intResult);
}
if (long.TryParse(value, out var longResult))
{
return (true, longResult);
}
if (double.TryParse(value, out var doubleResult))
{
return (true, doubleResult);
}
if (Guid.TryParseExact(value, "D", out var guidResult))
{
return (true, guidResult);
}
if (TimeSpan.TryParse(value, out var timeSpanResult))
{
return (true, timeSpanResult);
}
if (DateTime.TryParse(value, out var dateTimeResult))
{
return (true, dateTimeResult);
}
if ((value.StartsWith("ftp://", StringComparison.OrdinalIgnoreCase) ||
value.StartsWith("ftps://", StringComparison.OrdinalIgnoreCase) ||
value.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) &&
Uri.TryCreate(value, UriKind.RelativeOrAbsolute, out var uriResult))
{
return (true, uriResult);
}
return (false, value);
}
}
@@ -0,0 +1,208 @@
// Copyright © WireMock.Net
using System.Collections;
using System.Text.Json;
using System.Text.Json.Nodes;
using HandlebarsDotNet.Helpers.Models;
using JetBrains.Annotations;
using JsonConverter.System.Text.Json;
using WireMock.Settings;
using WireMock.Types;
using WireMock.Util;
namespace WireMock.Transformers;
/// <summary>
/// JSON body transformer implementation based on System.Text.Json.
/// </summary>
[PublicAPI]
public class SystemTextJsonBodyTransformer(WireMockServerSettings settings) : IJsonBodyTransformer
{
private static readonly SystemTextJsonConverter _jsonConverter = new();
/// <inheritdoc />
public BodyData TransformBodyAsJson(
ITransformerContext transformerContext,
ReplaceNodeOptions options,
object model,
IBodyData original)
{
JsonNode? jsonNode = null;
switch (original.BodyAsJson)
{
case JsonObject bodyAsJsonObject:
jsonNode = CloneNode(bodyAsJsonObject);
jsonNode = WalkNode(transformerContext, options, jsonNode, model);
break;
case JsonArray bodyAsJsonArray:
jsonNode = CloneNode(bodyAsJsonArray);
jsonNode = WalkNode(transformerContext, options, jsonNode, model);
break;
case var bodyAsEnumerable when bodyAsEnumerable is IEnumerable and not string:
jsonNode = JsonSerializer.SerializeToNode(bodyAsEnumerable);
if (jsonNode != null)
{
jsonNode = WalkNode(transformerContext, options, jsonNode, model);
}
break;
case string bodyAsString:
jsonNode = ReplaceSingleNode(transformerContext, options, bodyAsString, model);
break;
case not null:
jsonNode = JsonSerializer.SerializeToNode(original.BodyAsJson);
if (jsonNode != null)
{
jsonNode = WalkNode(transformerContext, options, jsonNode, model);
}
break;
}
return new BodyData
{
Encoding = original.Encoding,
DetectedBodyType = original.DetectedBodyType,
DetectedBodyTypeFromContentType = original.DetectedBodyTypeFromContentType,
ProtoDefinition = original.ProtoDefinition,
ProtoBufMessageType = original.ProtoBufMessageType,
BodyAsJson = jsonNode
};
}
private JsonNode ParseAsJsonObject(string stringValue)
{
if (_jsonConverter.IsValidJson(stringValue))
{
try
{
var parsed = JsonNode.Parse(stringValue);
if (parsed is JsonObject)
{
return parsed;
}
}
catch
{
settings.Logger.Warn("Failed to parse string ''{0}'' as JSON. Returning the original string value.", stringValue);
}
}
return JsonValue.Create(stringValue)!;
}
private JsonNode? ReplaceSingleNode(ITransformerContext transformerContext, ReplaceNodeOptions options, string stringValue, object model)
{
var transformedString = transformerContext.ParseAndRender(stringValue, model);
if (!string.Equals(stringValue, transformedString))
{
return ReplaceNodeValue(options, transformedString);
}
return JsonValue.Create(stringValue);
}
private JsonNode? WalkNode(ITransformerContext transformerContext, ReplaceNodeOptions options, JsonNode? node, object model)
{
switch (node)
{
case JsonObject jsonObject:
foreach (var property in jsonObject.ToArray())
{
jsonObject[property.Key] = WalkNode(transformerContext, options, property.Value, model);
}
return jsonObject;
case JsonArray jsonArray:
for (var i = 0; i < jsonArray.Count; i++)
{
jsonArray[i] = WalkNode(transformerContext, options, jsonArray[i], model);
}
return jsonArray;
case JsonValue jsonValue when jsonValue.TryGetValue<string>(out var stringValue):
if (string.IsNullOrEmpty(stringValue))
{
return jsonValue;
}
var transformed = transformerContext.ParseAndEvaluate(stringValue!, model);
return !Equals(stringValue, transformed) ? ReplaceNodeValue(options, transformed) ?? jsonValue : jsonValue;
default:
return node;
}
}
private JsonNode? ReplaceNodeValue(ReplaceNodeOptions options, object? transformedValue)
{
switch (transformedValue)
{
case JsonNode jsonNode:
return CloneNode(jsonNode);
case string transformedString:
var (isConvertedFromString, convertedValueFromString) = TryConvert(options, transformedString);
return isConvertedFromString
? JsonSerializer.SerializeToNode(convertedValueFromString)
: ParseAsJsonObject(transformedString);
case WireMockList<string> strings:
switch (strings.Count)
{
case 1:
return ParseAsJsonObject(strings[0]);
case > 1:
return JsonSerializer.SerializeToNode(strings.ToArray());
}
break;
case { }:
var (isConverted, convertedValue) = TryConvert(options, transformedValue);
if (isConverted)
{
return JsonSerializer.SerializeToNode(convertedValue);
}
break;
}
return null;
}
private static JsonNode? CloneNode(JsonNode? node)
{
#if NET8_0_OR_GREATER
return node?.DeepClone();
#else
return node == null ? null : JsonNode.Parse(node.ToJsonString());
#endif
}
private static (bool IsConverted, object ConvertedValue) TryConvert(ReplaceNodeOptions options, object value)
{
var valueAsString = value as string;
if (options == ReplaceNodeOptions.Evaluate)
{
if (valueAsString != null && WrappedString.TryDecode(valueAsString, out var decoded))
{
return (true, decoded);
}
return (false, value);
}
if (valueAsString != null)
{
return WrappedString.TryDecode(valueAsString, out var decoded)
? (true, decoded)
: NewtonsoftJsonBodyTransformer.TryConvertToKnownType(valueAsString);
}
return (false, value);
}
}