JSON to Query String
Turn a JSON object into a URL query string with correct encoding and array handling.
Turning a JSON object into query parameters is unambiguous only while the object is flat and every value is a scalar. Add an array or a nested object and you are picking a convention the receiving framework may not share. This page covers which conventions exist, where they disagree, and what truncates a long query string.
Nothing standardises what comes after the question mark
RFC 3986 section 3.4 defines the query as the characters between ? and # and describes it as non-hierarchical data. That is the entire specification. It does not mandate key=value, does not reserve & as the separator, and says nothing about arrays, objects, or types. The convention everyone uses was inherited from HTML forms, not from the URI standard.
The WHATWG URL Standard does specify application/x-www-form-urlencoded precisely, but the model it defines is an ordered list of name/value pairs in which both halves are strings. There is no array type, no nested object, no number, and no null. Anything richer than a flat map of strings has to be smuggled through inside the key names.
Every nesting scheme is therefore a private agreement between one serialiser and one parser, with nothing in between to validate it. To a URL parser, user[name]=x is simply a parameter whose name happens to be the nine characters user[name]; the brackets carry no meaning until some server-side library chooses to give them one. Test against the shape your server actually receives, because there is no specification to appeal to when the two ends disagree.
Four incompatible spellings of the same array
A two-element list has at least four common encodings: repeated keys (tag=a&tag=b), empty brackets (tag[]=a&tag[]=b), explicit indices (tag[0]=a&tag[1]=b), and a single delimited value (tag=a,b). Servers accept different subsets, and the forms they do not understand rarely produce an error.
PHP is the sharpest illustration: a[]=1&a[]=2 populates an array, but a=1&a=2 keeps only the last value and discards the first without complaint. Rack — and therefore Rails and Sinatra — parses through Rack::Utils.parse_nested_query and expects the bracket forms. On the client side, URLSearchParams.get() returns only the first match while getAll() returns every one, so code reaching for .get() on a repeated key reads a single element and believes it has the whole list. Java servlets behave identically: getParameter returns the first value only.
If you publish an API, make the choice explicit rather than emergent. OpenAPI 3 encodes it directly through style and explode: form with explode: true is the repeated-key form and is the default, explode: false produces the comma-delimited form, and deepObject selects bracket notation for nested objects. Writing it down is the only thing that makes the two ends agree.
The qs defaults that change your data type
qs is the de facto nested parser in the Node ecosystem, and two of its defaults reshape input rather than rejecting it. arrayLimit defaults to 20: any index of 20 or greater causes the entire array to be materialised as an object keyed by index instead. qs.parse('a[100]=b') yields { a: { "100": "b" } }, so the .map() waiting downstream throws on a payload whose only sin was being large.
The second is depth, which defaults to 5. Nesting beyond five levels is not parsed into structure; the remaining bracket text is folded into the key. Both limits exist to stop input like a[999999999]=b from allocating an enormous sparse array, so raising them is a deliberate trade rather than a free fix. Passing throwOnLimitExceeded: true converts the array case into an error instead of a silent change of type.
Framework defaults have moved recently too. Express 4 set the query parser application setting to extended, which routes through qs. Express 5 changed the default to simple, which uses Node's built-in querystring module and does not parse nesting at all — so ?price[eq]=5 now arrives as { "price[eq]": "5" } rather than a nested object. Restoring the old behaviour takes app.set('query parser', 'extended'), and forgetting it is one of the more common surprises in that upgrade.
URLSearchParams stringifies instead of nesting
The browser built-in will not help with structure, and it fails quietly. new URLSearchParams({ user: { name: "x" } }).toString() returns user=%5Bobject+Object%5D, because the constructor coerces every value with String(). Pass an array and you get one comma-joined parameter: { tags: ["a", "b"] } serialises to tags=a%2Cb, not two tags entries. Producing repeated keys requires calling append() in a loop.
Its escaping also differs from encodeURIComponent, because it implements the form-urlencoded serialiser rather than the URI rules. Spaces become + instead of %20, and some characters that the URI spec leaves alone are escaped anyway: new URLSearchParams({ a: "~" }).toString() gives a=%7E, where encodeURIComponent returns the tilde untouched.
Both outputs are correct inside a query string. Neither is correct inside a path segment, where + is a literal plus rather than a space. If the destination is anything other than a query, escape the pieces with the URL encoder and assemble the URL yourself.
Where a long query string gets cut off
The URI specification sets no length limit, but every hop between the client and your handler does. nginx requires the whole request line to fit inside a single large_client_header_buffers buffer, which defaults to 4 8k, and answers 414 when it does not fit. Apache httpd caps the request line with LimitRequestLine, default 8190 bytes, counting the method and protocol version as well as the URL. Internet Explorer's old 2,083-character ceiling is why roughly 2,000 characters remains the conservative target for any link that might be shared or emailed.
Percent-encoding makes the count worse than it looks. Each non-ASCII byte expands to three characters, so a four-byte emoji occupies twelve, and bracket notation wraps %5B and %5D around every level of nesting. A JSON object that looks compact can produce a query string several times its own length.
Length is not the only reason to keep structure out of the query. Query strings are written to access logs, sent to third-party origins in the Referer header, and kept in browser history, so a token or an email address placed there leaks in three directions at once. CDNs also key their caches on the full query string, so two requests differing only in parameter order are two separate cache entries unless you normalise the order first. Once a payload has genuine structure, a request body is the better home for it.
Related tools
Try these tools that work well with this one
URL Builder
Free online URL builder tool for constructing URLs with parameters
CSV to JSON Converter
Convert CSV data to JSON
JSON Editor
Free online JSON editor, formatter, and validator
JSON to CSV Converter
Convert JSON arrays of objects to CSV
JSON to TOON Converter
Convert JSON to TOON to save tokens
JSON to YAML Converter
Convert JSON to YAML
Related guides
Tutorials and guides to get more from this tool
JSON to Query String: simplifying API Requests in 2025
Learn how to convert nested JSON objects into URL-encoded query strings significantly simplifying your API development workflow.
The Ultimate Guide to Data Serialization: JSON, YAML, XML, and TOON
From legacy XML to modern JSON and TOON, explore the evolution of data serialization formats and when to use them.
URL Validation: Why Regular Expressions Aren't Enough
Validating URLs is harder than it looks. Explore why simple regex fails and how to ensure URLs are properly formatted and safe.
ThenCatch is a free developer toolkit. Learn more about us.