3

I have the following JSON which has to be converted to URL parameters for a GET request.

An example is given here, however due to the complexity of this object, there can be multiple line_items_attributes each with the given values as shown, I'm having difficulties passing on the correct one.

I've also tried to just serialize the JSON object and pass on that value but that did not solve the issue either.

{
    "purchase_invoice":
    {
        "date":"14/04/2015",
        "due_date":"14/04/2015",
        "contact_id":500,
        "contact_name":"TestContact",
        "reference":"TestReference",
        "line_items_attributes":[
            {
                "unit_price":10.00,
                "quantity":1,
                "description":"TestLineItemAttDesc",
                "tax_code_id":1,
                "ledger_account_id":501,
                "tax_rate_percentage":19.0,
                "tax_amount":1.60

            }]
    }
}

I've been searching for a while now but without much luck. Any insights are appreciated and most welcome!

This is calling an API which does not support the incoming data in JSON format, so doing this server-side or changing the web service to support data in JSON format is not possible.

AStopher
  • 4,207
  • 11
  • 50
  • 75
  • Is the problem that you don't know what the final URL string should look like, or you just don't know how to achieve it in C#? – StriplingWarrior Apr 17 '15 at 23:03
  • The problem is that I don't know what the final URL should look like. Based on the URL example I could try to build it in C# :). –  Apr 17 '15 at 23:10
  • One way to go is to encode it in base64 and replace '/' characters with, for instance, an underscore. Otherwise, you could just url-encode it, like @Bas suggested in his answer. – Grx70 Apr 18 '15 at 04:41
  • @AStopher - you've added a bounty for this, but form encoded payloads are a sequence of name/value pairs; how do you want complex hierarchical objects (possibly including arrays) to be represented in such a situation? See [How do I use FormUrlEncodedContent for complex data types?](https://stackoverflow.com/q/33061506/3744182) for which the accepted answer states, *There is no convention or standard that transforms a multi-dimension key-value structure into a single-dimension one.* – dbc May 26 '20 at 20:59
  • @AStopher - but if you are asking, *How do I create a query string like the one shown in the accepted answer*, then you can do it using Json.NET as shown in this fiddle: https://dotnetfiddle.net/F90qLD. Is that what you want? – dbc May 26 '20 at 22:04
  • @AStopher - Also, if you are indeed using the *Stripe API*, then it seems as though [Stripe.net](https://github.com/stripe/stripe-dotnet) hides these encoding details from you, see e.g. [this documentation example](https://stripe.com/docs/api/metadata?lang=dotnet) which simply shows `Metadata = new Dictionary { /* Contents */ }` – dbc May 27 '20 at 16:44
  • Most, if not all, web servers truncate urls (or querystring) arbitrarily (like 1024 or 2048 characters), so in the general case, this is not possible with the GET verb, only with POST. But as already said, stripe.net is the way to go for stripe in the .net context. – Simon Mourier May 28 '20 at 13:05

3 Answers3

7

x-www-form-urlencoded content is, essentially, a flat sequence of key/value tuples, and as explained in this answer to How do I use FormUrlEncodedContent for complex data types? by Tomalak, there is no canonical way to transform a hierarchical, nested key/value structure into a flat one.

Nevertheless, from the accepted answer to this question, this example from the Stripe API, and the question mentioned above, it seems that it is common to flatten parameters inside complex nested objects by surrounding their keys in brackets and appending them to the topmost key like so:

{
    { "purchase_invoice[date]", "14/04/2015" } 
    { "purchase_invoice[due_date]", "14/04/2015" } 
    { "purchase_invoice[contact_id]", "500" } 
    { "purchase_invoice[contact_name]", "TestContact" } 
    { "purchase_invoice[reference]", "TestReference" } 
    { "purchase_invoice[line_items_attributes][0][unit_price]", "10" } 
    { "purchase_invoice[line_items_attributes][0][quantity]", "1" } 
    { "purchase_invoice[line_items_attributes][0][description]", "TestLineItemAttDesc" } 
    { "purchase_invoice[line_items_attributes][0][tax_code_id]", "1" } 
    { "purchase_invoice[line_items_attributes][0][ledger_account_id]", "501" } 
    { "purchase_invoice[line_items_attributes][0][tax_rate_percentage]", "19" } 
    { "purchase_invoice[line_items_attributes][0][tax_amount]", "1.6" } 
}

If this is what you want, you can generate such key/value pairs with using the following extension methods:

public static partial class JsonExtensions
{
    public static string ToUrlEncodedQueryString(this JContainer container)
    {
        return container.ToQueryStringKeyValuePairs().ToUrlEncodedQueryString();
    }

    public static IEnumerable<KeyValuePair<string, string>> ToQueryStringKeyValuePairs(this JContainer container)
    {
        return container.Descendants()
            .OfType<JValue>()
            .Select(v => new KeyValuePair<string, string>(v.ToQueryStringParameterName(), (string)v));
    }

    public static string ToUrlEncodedQueryString(this IEnumerable<KeyValuePair<string, string>> pairs)
    {
        return string.Join("&", pairs.Select(p => HttpUtility.UrlEncode(p.Key) + "=" + HttpUtility.UrlEncode(p.Value)));
        //The following works but it seems heavy to construct and await a task just to built a string:
        //return new System.Net.Http.FormUrlEncodedContent(pairs).ReadAsStringAsync().Result;
        //The following works and eliminates allocation of one intermediate string per pair, but requires more code:
        //return pairs.Aggregate(new StringBuilder(), (sb, p) => (sb.Length > 0 ? sb.Append("&") : sb).Append(HttpUtility.UrlEncode(p.Key)).Append("=").Append(HttpUtility.UrlEncode(p.Value))).ToString();
        //Answers from https://stackoverflow.com/questions/3865975/namevaluecollection-to-url-query that use HttpUtility.ParseQueryString() are wrong because that class doesn't correctly escape the keys names.
    }

    public static string ToQueryStringParameterName(this JToken token)
    {
        // Loosely modeled on JToken.Path
        // https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Linq/JToken.cs#L184
        // By https://github.com/JamesNK
        if (token == null || token.Parent == null)
            return string.Empty;
        var positions = new List<string>();
        for (JToken previous = null, current = token; current != null; previous = current, current = current.Parent)
        {
            switch (current)
            {
                case JProperty property:
                    positions.Add(property.Name);
                    break;
                case JArray array:
                case JConstructor constructor:
                    if (previous != null)
                        positions.Add(((IList<JToken>)current).IndexOf(previous).ToString(CultureInfo.InvariantCulture)); // Don't localize the indices!
                    break;
            }
        }
        var sb = new StringBuilder();
        for (var i = positions.Count - 1; i >= 0; i--)
        {
            var name = positions[i];
            // TODO: decide what should happen if the name contains the characters `[` or `]`.
            if (sb.Length == 0)
                sb.Append(name);
            else
                sb.Append('[').Append(name).Append(']');
        }

        return sb.ToString();
    }
}

Then if you have a JSON string, you can parse it into a LINQ-to-JSON JObject and generate the query string like so:

var obj = JObject.Parse(jsonString);
var queryString = obj.ToUrlEncodedQueryString();

Alternatively, if you have some hierarchical data model POCO, you can generate your JObject from the model using JObject.FromObject():

var obj = JObject.FromObject(myModel);
var queryString = obj.ToUrlEncodedQueryString();

Demo fiddle here.

dbc
  • 104,963
  • 20
  • 228
  • 340
-1

So the final URL would be easy to compute using any URL Encoding mechanism. In C#, we could do the following:

string json = "...";
string baseUrl = "http://bla.com/somepage?myJson="
string urlWithJson = baseUrl + System.Net.WebUtility.UrlEncode(json)

Is there any way you can POST the data or otherwise send a request body instead? It would seem slightly easier/cleaner.

Bas
  • 26,772
  • 8
  • 53
  • 86
-2

Sounds like you need something which is x-www-form-urlencoded.

From your example, it would look like this:

purchase_invoice%5Bdate%5D=14%2F04%2F2015&purchase_invoice%5Bdue_date%5D=14%2F04%2F2015&purchase_invoice%5Bcontact_id%5D=500&purchase_invoice%5Bcontact_name%5D=TestContact&purchase_invoice%5Breference%5D=TestReference&purchase_invoice%5Bline_items_attributes%5D%5B0%5D%5Bunit_price%5D=10&purchase_invoice%5Bline_items_attributes%5D%5B0%5D%5Bquantity%5D=1&purchase_invoice%5Bline_items_attributes%5D%5B0%5D%5Bdescription%5D=TestLineItemAttDesc&purchase_invoice%5Bline_items_attributes%5D%5B0%5D%5Btax_code_id%5D=1&purchase_invoice%5Bline_items_attributes%5D%5B0%5D%5Bledger_account_id%5D=501&purchase_invoice%5Bline_items_attributes%5D%5B0%5D%5Btax_rate_percentage%5D=19&purchase_invoice%5Bline_items_attributes%5D%5B0%5D%5Btax_amount%5D=1.6

The best reference for this encoding that I'm aware of is the undocumented jQuery.param method on the jQuery JavaScript library.

Nick
  • 1,799
  • 13
  • 13