From 1e591d5f8a625519d843f0e0799a8ec2dec1316e Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Fri, 17 Apr 2026 13:32:26 +0200 Subject: [PATCH] Fix ExactMatcher and JsonMatcher not working for ISO dates as string (#1443) --- .../WireMock.Net.Extensions.Routing.csproj | 2 +- .../Serialization/MappingSerializer.cs | 9 +- .../Server/WireMockServer.Admin.cs | 4 - .../WireMock.Net.NUnit.csproj | 2 +- .../WireMock.Net.RestClient.csproj | 2 +- .../WireMock.Net.Shared.csproj | 2 +- .../Serialization/MappingSerializerTests.cs | 89 +++++++++++++++++++ 7 files changed, 101 insertions(+), 9 deletions(-) diff --git a/src/WireMock.Net.Extensions.Routing/WireMock.Net.Extensions.Routing.csproj b/src/WireMock.Net.Extensions.Routing/WireMock.Net.Extensions.Routing.csproj index 222fffc0..11578493 100644 --- a/src/WireMock.Net.Extensions.Routing/WireMock.Net.Extensions.Routing.csproj +++ b/src/WireMock.Net.Extensions.Routing/WireMock.Net.Extensions.Routing.csproj @@ -25,7 +25,7 @@ - + diff --git a/src/WireMock.Net.Minimal/Serialization/MappingSerializer.cs b/src/WireMock.Net.Minimal/Serialization/MappingSerializer.cs index 6386dbef..5de20e49 100644 --- a/src/WireMock.Net.Minimal/Serialization/MappingSerializer.cs +++ b/src/WireMock.Net.Minimal/Serialization/MappingSerializer.cs @@ -1,6 +1,7 @@ // Copyright © WireMock.Net using JsonConverter.Abstractions; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; #if NETSTANDARD2_0_OR_GREATER || NETCOREAPP3_1_OR_GREATER || NET6_0_OR_GREATER || NET461 using System.Text.Json; @@ -10,9 +11,15 @@ namespace WireMock.Serialization; internal class MappingSerializer(IJsonConverter jsonConverter) { + private static readonly JsonConverterOptions JsonConverterOptions = new JsonConverterOptions + { + DateParseHandling = (int) DateParseHandling.None + }; + internal T[] DeserializeJsonToArray(string value) { - return DeserializeObjectToArray(jsonConverter.Deserialize(value)!); + // DeserializeObject + return DeserializeObjectToArray(jsonConverter.Deserialize(value, JsonConverterOptions)!); } internal static T[] DeserializeObjectToArray(object value) diff --git a/src/WireMock.Net.Minimal/Server/WireMockServer.Admin.cs b/src/WireMock.Net.Minimal/Server/WireMockServer.Admin.cs index 83f8aa60..80e12db6 100644 --- a/src/WireMock.Net.Minimal/Server/WireMockServer.Admin.cs +++ b/src/WireMock.Net.Minimal/Server/WireMockServer.Admin.cs @@ -1,6 +1,5 @@ // Copyright © WireMock.Net -using System.Linq; using System.Net; using System.Text; using JetBrains.Annotations; @@ -26,9 +25,6 @@ using WireMock.Util; namespace WireMock.Server; -/// -/// The fluent mock server. -/// public partial class WireMockServer { private const int EnhancedFileSystemWatcherTimeoutMs = 1000; diff --git a/src/WireMock.Net.NUnit/WireMock.Net.NUnit.csproj b/src/WireMock.Net.NUnit/WireMock.Net.NUnit.csproj index f5114c73..de0ac225 100644 --- a/src/WireMock.Net.NUnit/WireMock.Net.NUnit.csproj +++ b/src/WireMock.Net.NUnit/WireMock.Net.NUnit.csproj @@ -25,7 +25,7 @@ - + diff --git a/src/WireMock.Net.RestClient/WireMock.Net.RestClient.csproj b/src/WireMock.Net.RestClient/WireMock.Net.RestClient.csproj index cd79ab5f..688103bd 100644 --- a/src/WireMock.Net.RestClient/WireMock.Net.RestClient.csproj +++ b/src/WireMock.Net.RestClient/WireMock.Net.RestClient.csproj @@ -33,7 +33,7 @@ - + diff --git a/src/WireMock.Net.Shared/WireMock.Net.Shared.csproj b/src/WireMock.Net.Shared/WireMock.Net.Shared.csproj index 05d0f585..b71a5cca 100644 --- a/src/WireMock.Net.Shared/WireMock.Net.Shared.csproj +++ b/src/WireMock.Net.Shared/WireMock.Net.Shared.csproj @@ -30,7 +30,7 @@ - + diff --git a/test/WireMock.Net.Tests/Serialization/MappingSerializerTests.cs b/test/WireMock.Net.Tests/Serialization/MappingSerializerTests.cs index 1cd66b3c..2c74b205 100644 --- a/test/WireMock.Net.Tests/Serialization/MappingSerializerTests.cs +++ b/test/WireMock.Net.Tests/Serialization/MappingSerializerTests.cs @@ -3,6 +3,8 @@ using JsonConverter.Newtonsoft.Json; using WireMock.Admin.Mappings; using WireMock.Serialization; +using Newtonsoft.Json.Linq; + #if NET8_0_OR_GREATER using JsonConverter.System.Text.Json; #endif @@ -319,5 +321,92 @@ public class MappingSerializerTests act.Should().Throw() .WithMessage("Cannot deserialize the provided value to an array or object."); } + + [Fact] + public void MappingSerializer_DeserializeJsonToArray_WithNewtonsoftJson_DateTimeStringInQueryParamExactMatcherPattern_ShouldPreservePatternAsString() + { + // Arrange + var jsonConverter = new NewtonsoftJsonConverter(); + var serializer = new MappingSerializer(jsonConverter); + var mappingJson = + """ + { + "Guid": "12345678-1234-1234-1234-aaaaaaaaaaaa", + "Request": { + "Path": "/api/report", + "Methods": ["GET"], + "Params": [ + { + "Name": "asOfDate", + "Matchers": [ + { + "Name": "ExactMatcher", + "Pattern": "2021-11-10T13:39:13.705" + } + ] + } + ] + }, + "Response": { + "StatusCode": 200 + } + } + """; + + // Act + var result = serializer.DeserializeJsonToArray(mappingJson); + + // Assert + result.Should().HaveCount(1); + var matcher = result[0].Request!.Params![0].Matchers![0]; + matcher.Name.Should().Be("ExactMatcher"); + matcher.Pattern.Should().BeOfType() + .Which.Should().Be("2021-11-10T13:39:13.705", + "datetime-format strings in ExactMatcher Pattern fields must survive deserialization as strings, " + + "not be auto-converted to DateTime by Newtonsoft.Json's DateParseHandling.DateTime"); + } + + [Fact] + public void MappingSerializer_DeserializeJsonToArray_WithNewtonsoftJson_DateTimeStringInJsonMatcherBodyPattern_ShouldPreservePatternAsString() + { + // Arrange + var jsonConverter = new NewtonsoftJsonConverter(); + var serializer = new MappingSerializer(jsonConverter); + // Pattern is an INLINE JSON object (not a string) - this is how WireMock mapping files store + // JsonMatcher patterns when recorded. Newtonsoft with DateParseHandling.DateTime will convert + // the datetime value inside the JObject to JTokenType.Date during deserialization. + var mappingJson = + """ + { + "Guid": "12345678-1234-1234-1234-bbbbbbbbbbbb", + "Request": { + "Path": "/api/report", + "Methods": ["POST"], + "Body": { + "Matcher": { + "Name": "JsonMatcher", + "Pattern": {"Date": "2021-09-30T00:00:00Z", "Names": ["Cash"]} + } + } + }, + "Response": { + "StatusCode": 200 + } + } + """; + + // Act + var result = serializer.DeserializeJsonToArray(mappingJson); + + // Assert - datetime values inside the JObject pattern must remain JTokenType.String. + result.Should().HaveCount(1); + var matcher = result[0].Request!.Body!.Matcher!; + matcher.Name.Should().Be("JsonMatcher"); + var patternJObject = matcher.Pattern.Should().BeOfType().Subject; + patternJObject["Date"]!.Type.Should().Be(JTokenType.String, + "datetime-format strings inside an inline JsonMatcher body pattern must retain JTokenType.String " + + "after deserialization; if DateParseHandling.DateTime auto-converts them to JTokenType.Date, " + + "JToken.DeepEquals will fail against incoming request bodies parsed with DateParseHandling.None"); + } #endif } \ No newline at end of file