Response Templating complex objects #603

Closed
opened 2025-12-29 15:28:01 +01:00 by adam · 11 comments
Owner

Originally created by @dotnetprofessional on GitHub (May 23, 2024).

Originally assigned to: @StefH on GitHub.

I'm trying to get templating working, taking data from the incoming request and mapping it to the response. I'm sure I'm not understanding the examples as I just can't get anything but a very simple {{request.path}} to work. So at least I know its transforming :)

Any advice on how to map complex objects would be helpful. I tried looking at the samples, but none seemed to cover this scenario. They dealt with collections. I'm also confused with when to use body or bodyAsJson, the samples use body but the request is json. I've tried both without success. I get this is using the JsonPath syntax, but from what I can tell the syntax I have should work.

Here is a sample, that returns the error:

{"Status":"Value cannot be null. (Parameter 'value')"}

formatted request body:

{
  "PricingContext": {
    "Market": "USA"
  }
}
        var requestJson = JsonConvert.SerializeObject(new { PricingContext = new { Market = "USA" } });
        var responseJson = JsonConvert.SerializeObject(new { Market = "{{JsonPath.SelectToken request.body \"$.pricingContext.market\"}}" });
        var _wireMockServer = Server.WireMockServer.Start(new WireMockServerSettings
        {
            Port = 9091,
            AllowPartialMapping = true,
        });
        _wireMockServer
            .Given(WireMock.RequestBuilders.Request.Create()
                    .WithPath("/pricing")
                    .WithBody(requestJson)
                    .UsingPost())
                .RespondWith(Response.Create()
                    .WithHeader("Content-Type", "application/json")
                    .WithBody(responseJson)
                    .WithTransformer(true)
                );
        var http = new HttpClient();
        var response = await http.GetAsync($"{_wireMockServer.Url}/pricing");
        var value = await response.Content.ReadAsStringAsync();
Originally created by @dotnetprofessional on GitHub (May 23, 2024). Originally assigned to: @StefH on GitHub. I'm trying to get templating working, taking data from the incoming request and mapping it to the response. I'm sure I'm not understanding the examples as I just can't get anything but a very simple {{request.path}} to work. So at least I know its transforming :) Any advice on how to map complex objects would be helpful. I tried looking at the samples, but none seemed to cover this scenario. They dealt with collections. I'm also confused with when to use `body` or `bodyAsJson`, the samples use `body` but the request is json. I've tried both without success. I get this is using the `JsonPath` syntax, but from what I can tell the syntax I have should work. Here is a sample, that returns the error: `{"Status":"Value cannot be null. (Parameter 'value')"}` formatted request body: ``` json { "PricingContext": { "Market": "USA" } } ``` ``` c# var requestJson = JsonConvert.SerializeObject(new { PricingContext = new { Market = "USA" } }); var responseJson = JsonConvert.SerializeObject(new { Market = "{{JsonPath.SelectToken request.body \"$.pricingContext.market\"}}" }); var _wireMockServer = Server.WireMockServer.Start(new WireMockServerSettings { Port = 9091, AllowPartialMapping = true, }); _wireMockServer .Given(WireMock.RequestBuilders.Request.Create() .WithPath("/pricing") .WithBody(requestJson) .UsingPost()) .RespondWith(Response.Create() .WithHeader("Content-Type", "application/json") .WithBody(responseJson) .WithTransformer(true) ); var http = new HttpClient(); var response = await http.GetAsync($"{_wireMockServer.Url}/pricing"); var value = await response.Content.ReadAsStringAsync(); ```
adam added the question label 2025-12-29 15:28:01 +01:00
adam closed this issue 2025-12-29 15:28:01 +01:00
Author
Owner

@StefH commented on GitHub (May 23, 2024):

Did you try using BodyAsJson.

        var request = new { PricingContext = new { Market = "USA" } };
        var response = new { Market = "{{JsonPath.SelectToken request.bodyAsJson \"$.pricingContext.market\"}}" };
        var _wireMockServer = Server.WireMockServer.Start(new WireMockServerSettings
        {
            Port = 9091,
            AllowPartialMapping = true,
        });
        _wireMockServer
            .Given(WireMock.RequestBuilders.Request.Create()
                    .WithPath("/pricing")
                    .WithBodyAsJson(request)
                    .UsingPost())
                .RespondWith(Response.Create()
                    .WithHeader("Content-Type", "application/json")
                    .WithBodyAsJson(response)
                    .WithTransformer(true)
                );
        var http = new HttpClient();
        var response = await http.GetAsync($"{_wireMockServer.Url}/pricing");
        var value = await response.Content.ReadAsStringAsync();
@StefH commented on GitHub (May 23, 2024): Did you try using `BodyAsJson`. ``` c# var request = new { PricingContext = new { Market = "USA" } }; var response = new { Market = "{{JsonPath.SelectToken request.bodyAsJson \"$.pricingContext.market\"}}" }; var _wireMockServer = Server.WireMockServer.Start(new WireMockServerSettings { Port = 9091, AllowPartialMapping = true, }); _wireMockServer .Given(WireMock.RequestBuilders.Request.Create() .WithPath("/pricing") .WithBodyAsJson(request) .UsingPost()) .RespondWith(Response.Create() .WithHeader("Content-Type", "application/json") .WithBodyAsJson(response) .WithTransformer(true) ); var http = new HttpClient(); var response = await http.GetAsync($"{_wireMockServer.Url}/pricing"); var value = await response.Content.ReadAsStringAsync(); ```
Author
Owner

@dotnetprofessional commented on GitHub (May 23, 2024):

Yeah, but I'm actually wanting to have this work with the .json files in the final solution. However, I've included this sample as it seems to have the same issue, so assuming its not specific to using .json files.

So I did update the code to use .WithBodyAsJson rather than .WithBody and I'm getting the same result. You should be able to paste the code above into your test project (that's what I'm doing) and validate the error.

@dotnetprofessional commented on GitHub (May 23, 2024): Yeah, but I'm actually wanting to have this work with the .json files in the final solution. However, I've included this sample as it seems to have the same issue, so assuming its not specific to using .json files. So I did update the code to use `.WithBodyAsJson` rather than `.WithBody` and I'm getting the same result. You should be able to paste the code above into your test project (that's what I'm doing) and validate the error.
Author
Owner

@StefH commented on GitHub (May 24, 2024):

When you use .WithBody(requestJson), the requestJson is just a string, it's not possible to do any JsonPath.SelectToken on a string.

In your case you could try:
1:

var requestJson = @"{ 'Name': 'John Doe', 'Age': 30 }"; // read json file
var request = JObject.Parse(requestJson);

2:
And use .WithBodyAsJson(request)

@StefH commented on GitHub (May 24, 2024): When you use `.WithBody(requestJson)`, the requestJson is just a string, it's not possible to do any JsonPath.SelectToken on a string. In your case you could try: 1: ``` c# var requestJson = @"{ 'Name': 'John Doe', 'Age': 30 }"; // read json file var request = JObject.Parse(requestJson); ``` 2: And use `.WithBodyAsJson(request)`
Author
Owner

@dotnetprofessional commented on GitHub (May 24, 2024):

Ok, so that didn't resolve my issue. I've been digging a little deeper, and it seems the issue is that the request body is null when it hits the Handlebars evaluation (btw curious why you're walking the node tree rather than just give it the full text). Anyway, I see that you serialize, when an object is passed, so using that so I'm using more of the library, here's the updated code I'm using:

        var requestJson = new { PricingContext = new { Market = "USA" } };
        var responseJson = new { Market = "{{JsonPath.SelectToken request.bodyAsJson \"$.PricingContext.Market\"}}" };
        var _wireMockServer = Server.WireMockServer.Start(new WireMockServerSettings
        {
            Port = 9091,
            AllowPartialMapping = true,
        });
        _wireMockServer
            .Given(WireMock.RequestBuilders.Request.Create()
                    .WithPath("/pricing")
                    .WithBodyAsJson(requestJson)
                    .UsingGet())
                .RespondWith(Response.Create()
                    .WithHeader("Content-Type", "application/json")
                    .WithBodyAsJson(responseJson)
                    .WithTransformer(true)
                );
        var http = new HttpClient();
        var response = await http.GetAsync($"{_wireMockServer.Url}/pricing");
        var value = await response.Content.ReadAsStringAsync();

So I can see that its correctly serializing the request object and adding it to the _requestMatchers:

image

However, when it evaluates the request and finds it, all the body values are null.

image

So when the handlebars attempt to evaluate there's no data to use. At least this is my understanding of the code. I'm unclear what I've done that is making the request data unavailable.

image
@dotnetprofessional commented on GitHub (May 24, 2024): Ok, so that didn't resolve my issue. I've been digging a little deeper, and it seems the issue is that the request body is null when it hits the Handlebars evaluation (btw curious why you're walking the node tree rather than just give it the full text). Anyway, I see that you serialize, when an object is passed, so using that so I'm using more of the library, here's the updated code I'm using: ```cs var requestJson = new { PricingContext = new { Market = "USA" } }; var responseJson = new { Market = "{{JsonPath.SelectToken request.bodyAsJson \"$.PricingContext.Market\"}}" }; var _wireMockServer = Server.WireMockServer.Start(new WireMockServerSettings { Port = 9091, AllowPartialMapping = true, }); _wireMockServer .Given(WireMock.RequestBuilders.Request.Create() .WithPath("/pricing") .WithBodyAsJson(requestJson) .UsingGet()) .RespondWith(Response.Create() .WithHeader("Content-Type", "application/json") .WithBodyAsJson(responseJson) .WithTransformer(true) ); var http = new HttpClient(); var response = await http.GetAsync($"{_wireMockServer.Url}/pricing"); var value = await response.Content.ReadAsStringAsync(); ``` So I can see that its correctly serializing the request object and adding it to the _requestMatchers: <img width="802" alt="image" src="https://github.com/WireMock-Net/WireMock.Net/assets/5588330/824bdb4e-dafb-4af8-b33f-4f70db6ded60"> However, when it evaluates the request and finds it, all the body values are null. <img width="878" alt="image" src="https://github.com/WireMock-Net/WireMock.Net/assets/5588330/e6bb841e-26bf-40cd-8d01-fe7012cd7227"> So when the handlebars attempt to evaluate there's no data to use. At least this is my understanding of the code. I'm unclear what I've done that is making the request data unavailable. <img width="708" alt="image" src="https://github.com/WireMock-Net/WireMock.Net/assets/5588330/12f725c3-d152-4a2a-9161-b74920cdf059">
Author
Owner

@StefH commented on GitHub (May 24, 2024):

The correct code is:

var requestJson = new { PricingContext = new { Market = "USA" } };
var responseJson = new { Market = "{{JsonPath.SelectToken request.body \"$.PricingContext.Market\"}}" };
server
    .Given(Request.Create()
        .WithBody(new JsonMatcher(requestJson))
        .WithPath("/pricing")
        .UsingPost()
    )
    .RespondWith(Response.Create()
        .WithHeader("Content-Type", "application/json")
        .WithBodyAsJson(responseJson)
        .WithTransformer(true)
    );

image

There were some issues in your example code:

  1. AllowPartialMapping = true --> this means that any mappng will be returned, and probably the wrong mapping was found (https://github.com/WireMock-Net/WireMock.Net/wiki/Request-Matching-Tips)
  2. use .UsingPost() instead of .UsingGet() because you can only send a body using POST

The .WithBodyAsJson(requestJson) is actually wrongly implemented, I will fix this.

@StefH commented on GitHub (May 24, 2024): The correct code is: ``` c# var requestJson = new { PricingContext = new { Market = "USA" } }; var responseJson = new { Market = "{{JsonPath.SelectToken request.body \"$.PricingContext.Market\"}}" }; server .Given(Request.Create() .WithBody(new JsonMatcher(requestJson)) .WithPath("/pricing") .UsingPost() ) .RespondWith(Response.Create() .WithHeader("Content-Type", "application/json") .WithBodyAsJson(responseJson) .WithTransformer(true) ); ``` ![image](https://github.com/WireMock-Net/WireMock.Net/assets/249938/adcf4842-90d4-4539-aae1-11661e59db10) There were some issues in your example code: 1. `AllowPartialMapping = true` --> this means that any mappng will be returned, and probably the wrong mapping was found (https://github.com/WireMock-Net/WireMock.Net/wiki/Request-Matching-Tips) 2. use `.UsingPost()` instead of `.UsingGet()` because you can only send a body using POST The `.WithBodyAsJson(requestJson)` is actually wrongly implemented, I will fix this.
Author
Owner

@StefH commented on GitHub (May 24, 2024):

https://github.com/WireMock-Net/WireMock.Net/pull/1111

@StefH commented on GitHub (May 24, 2024): https://github.com/WireMock-Net/WireMock.Net/pull/1111
Author
Owner

@dotnetprofessional commented on GitHub (May 25, 2024):

Thanks, appreciate your prompt replies. So I must apologize, I was so focused on the Wiremock code I ignored my actual request. I wasn't passing the request that I was trying to parse. Anyway your code helped me identify that. So, I was able to validate that using the WithBodyAsJson works as expected now. However, I think there's a bug when using the Admin files and just Json. I have two different tests, one which works, the other that doesnt. I'll explain where I see the issue at the end:

Working Sample

    [Fact]
    public async Task Handlebars_SelectToken_complex_object_using_fluentAPI()
    {
        var responseTemplate = new
        {
            market = "{{JsonPath.SelectToken request.bodyAsJson \"$.pricingContext.market\"}}",
            languages = "en"
        };

        var responseBody = new
        {
            pricingContext = new
            {
                market = "US"
            }
        };

        var _wireMockServer = Server.WireMockServer.Start(new WireMockServerSettings
        {
            Port = 9091,
            StartAdminInterface = true,
            AllowPartialMapping = true,
            DisableJsonBodyParsing = false,
        });

        _wireMockServer
            .Given(WireMock.RequestBuilders.Request.Create()
                    .WithPath("/prices")
                    .UsingPost())
            .RespondWith(Response.Create()
                .WithHeader("Content-Type", "application/json")
                .WithBodyAsJson(responseTemplate)
                .WithTransformer(true)
            );

        var http = new HttpClient();
        var httpContent = new StringContent(JsonConvert.SerializeObject(responseBody), Encoding.UTF8, "application/json");
        var response = await http.PostAsync($"{_wireMockServer.Url}/prices", httpContent);
        var value = await response.Content.ReadAsStringAsync();
    }

Failing Sample

    [Fact]
    public async Task Handlebars_SelectToken_complex_object_using_FileFromBody()
    {
        var responseJson = File.ReadAllText("c:\\temp\\generic-template-request-body.json");
        var _wireMockServer = Server.WireMockServer.Start(new WireMockServerSettings
        {
            Port = 9091,
            FileSystemHandler = new CustomLocalFileSystemHandler("c:\\temp"),
            StartAdminInterface = true,
            ReadStaticMappings = true,
            WatchStaticMappings = true,
            WatchStaticMappingsInSubdirectories = true,
            AllowPartialMapping = true,
            DisableJsonBodyParsing = false,
        });

        var http = new HttpClient();
        var httpContent = new StringContent(responseJson, Encoding.UTF8, "application/json");
        var response = await http.PostAsync($"{_wireMockServer.Url}/prices", httpContent);
        var value = await response.Content.ReadAsStringAsync();
    }

The issue appears to be in the parsing of the arguments (couldn't work out where this is happening). The image below shows how the argurments is populated for the working example:

image

In the sample that doesn't work we see extra trailing \. If I manually remove/fix this while debugging it works.

image
@dotnetprofessional commented on GitHub (May 25, 2024): Thanks, appreciate your prompt replies. So I must apologize, I was so focused on the Wiremock code I ignored my actual request. I wasn't passing the request that I was trying to parse. Anyway your code helped me identify that. So, I was able to validate that using the `WithBodyAsJson` works as expected now. However, I think there's a bug when using the Admin files and just Json. I have two different tests, one which works, the other that doesnt. I'll explain where I see the issue at the end: __Working Sample__ ```cs [Fact] public async Task Handlebars_SelectToken_complex_object_using_fluentAPI() { var responseTemplate = new { market = "{{JsonPath.SelectToken request.bodyAsJson \"$.pricingContext.market\"}}", languages = "en" }; var responseBody = new { pricingContext = new { market = "US" } }; var _wireMockServer = Server.WireMockServer.Start(new WireMockServerSettings { Port = 9091, StartAdminInterface = true, AllowPartialMapping = true, DisableJsonBodyParsing = false, }); _wireMockServer .Given(WireMock.RequestBuilders.Request.Create() .WithPath("/prices") .UsingPost()) .RespondWith(Response.Create() .WithHeader("Content-Type", "application/json") .WithBodyAsJson(responseTemplate) .WithTransformer(true) ); var http = new HttpClient(); var httpContent = new StringContent(JsonConvert.SerializeObject(responseBody), Encoding.UTF8, "application/json"); var response = await http.PostAsync($"{_wireMockServer.Url}/prices", httpContent); var value = await response.Content.ReadAsStringAsync(); } ``` __Failing Sample__ ```cs [Fact] public async Task Handlebars_SelectToken_complex_object_using_FileFromBody() { var responseJson = File.ReadAllText("c:\\temp\\generic-template-request-body.json"); var _wireMockServer = Server.WireMockServer.Start(new WireMockServerSettings { Port = 9091, FileSystemHandler = new CustomLocalFileSystemHandler("c:\\temp"), StartAdminInterface = true, ReadStaticMappings = true, WatchStaticMappings = true, WatchStaticMappingsInSubdirectories = true, AllowPartialMapping = true, DisableJsonBodyParsing = false, }); var http = new HttpClient(); var httpContent = new StringContent(responseJson, Encoding.UTF8, "application/json"); var response = await http.PostAsync($"{_wireMockServer.Url}/prices", httpContent); var value = await response.Content.ReadAsStringAsync(); } ``` The issue `appears` to be in the parsing of the arguments (couldn't work out where this is happening). The image below shows how the argurments is populated for the working example: <img width="646" alt="image" src="https://github.com/WireMock-Net/WireMock.Net/assets/5588330/1410840f-3725-4267-9a86-3edae816dd83"> --- In the sample that doesn't work we see extra trailing \\. If I manually remove/fix this while debugging it works. <img width="691" alt="image" src="https://github.com/WireMock-Net/WireMock.Net/assets/5588330/be3fb58f-4a95-4c6b-84d9-51ef2f0e7af2">
Author
Owner

@StefH commented on GitHub (May 25, 2024):

Can you please provide the full C# code for Task Handlebars_SelectToken_complex_object_using_FileFromBody?
Because I don't see the mapping.

@StefH commented on GitHub (May 25, 2024): Can you please provide the full C# code for `Task Handlebars_SelectToken_complex_object_using_FileFromBody`? Because I don't see the mapping.
Author
Owner

@dotnetprofessional commented on GitHub (May 25, 2024):

Sure, here's my custom file handler, mainly so I can change the name of the directory and ignore non .json file, so not sure I need that.

public class CustomLocalFileSystemHandler : LocalFileSystemHandler
{
    private string _serviceMockPath;
    // have to duplicate what's in the base as it's private
    private string _rootFolder = string.Empty;
    public CustomLocalFileSystemHandler() : this(Directory.GetCurrentDirectory(), "_serviceMocks") { }

    public CustomLocalFileSystemHandler(string serviceMocks) : this(Directory.GetCurrentDirectory(), serviceMocks) { }

    public CustomLocalFileSystemHandler(string rootPath, string serviceMocks) : base(rootPath)
    {
        _rootFolder = rootPath;
        _serviceMockPath = serviceMocks;
    }

    public override string GetMappingFolder()
    {
        return Path.Combine(_rootFolder, _serviceMockPath);
    }

    public override IEnumerable<string> EnumerateFiles(string path, bool includeSubdirectories)
    {
        return includeSubdirectories ? Directory.EnumerateFiles(path, "*.json", SearchOption.AllDirectories) : Directory.EnumerateFiles(path);
    }
}

I've attached the three files I'm using, that in the C:\temp. Let me know if you need any more details.

generic-template-request.json
generic-template-request-body.json
generic-template-response.json

@dotnetprofessional commented on GitHub (May 25, 2024): Sure, here's my custom file handler, mainly so I can change the name of the directory and ignore non .json file, so not sure I need that. ```cs public class CustomLocalFileSystemHandler : LocalFileSystemHandler { private string _serviceMockPath; // have to duplicate what's in the base as it's private private string _rootFolder = string.Empty; public CustomLocalFileSystemHandler() : this(Directory.GetCurrentDirectory(), "_serviceMocks") { } public CustomLocalFileSystemHandler(string serviceMocks) : this(Directory.GetCurrentDirectory(), serviceMocks) { } public CustomLocalFileSystemHandler(string rootPath, string serviceMocks) : base(rootPath) { _rootFolder = rootPath; _serviceMockPath = serviceMocks; } public override string GetMappingFolder() { return Path.Combine(_rootFolder, _serviceMockPath); } public override IEnumerable<string> EnumerateFiles(string path, bool includeSubdirectories) { return includeSubdirectories ? Directory.EnumerateFiles(path, "*.json", SearchOption.AllDirectories) : Directory.EnumerateFiles(path); } } ``` I've attached the three files I'm using, that in the C:\temp. Let me know if you need any more details. [generic-template-request.json](https://github.com/WireMock-Net/WireMock.Net/files/15444451/generic-template-request.json) [generic-template-request-body.json](https://github.com/WireMock-Net/WireMock.Net/files/15444452/generic-template-request-body.json) [generic-template-response.json](https://github.com/WireMock-Net/WireMock.Net/files/15444453/generic-template-response.json)
Author
Owner

@StefH commented on GitHub (May 27, 2024):

@dotnetprofessional
It's related to HandleBars.Net, it seems that the escaped " are not correct parsed.

A solution is to use ', like:

{
  "market": "{{JsonPath.SelectToken request.bodyAsJson '$.pricingContext.market'}}",
  "languages": "en"
}

See
https://github.com/Handlebars-Net/Handlebars.Net.Helpers/blob/master/test/Handlebars.Net.Helpers.Tests/Templates/JsonPathHelpersTemplateTests.cs#L54

@StefH commented on GitHub (May 27, 2024): @dotnetprofessional It's related to HandleBars.Net, it seems that the escaped `"` are not correct parsed. A solution is to use `'`, like: ``` json { "market": "{{JsonPath.SelectToken request.bodyAsJson '$.pricingContext.market'}}", "languages": "en" } ``` See https://github.com/Handlebars-Net/Handlebars.Net.Helpers/blob/master/test/Handlebars.Net.Helpers.Tests/Templates/JsonPathHelpersTemplateTests.cs#L54
Author
Owner

@dotnetprofessional commented on GitHub (May 29, 2024):

Thanks for your help, this was the issue! I'd like to recommend the docs be updated to use single quotes, it should avoid others experiencing the same issue.

@dotnetprofessional commented on GitHub (May 29, 2024): Thanks for your help, this was the issue! I'd like to recommend the docs be updated to use single quotes, it should avoid others experiencing the same issue.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/WireMock.Net-wiremock#603