JSON to Query String: How to Serialize an Object into URL Params
Turn a JSON object into a clean URL query string. Learn how keys and values get encoded, how nested objects and arrays flatten, and how to build API URLs.
JSON to Query String: How to Serialize an Object into URL Params
You have a JavaScript object. You need a URL. Somewhere between those two things sits a small, fiddly conversion that everyone gets wrong at least once: turning {q: "opus 4", tags: ["ai", "tools"]} into ?q=opus%204&tags=ai&tags=tools. Miss an encoding step and the link breaks. Pick the wrong array convention and the backend reads garbage. This post walks through exactly how the conversion works, where it trips people up, and how to do it without counting ampersands by hand.
The core rule: key=value pairs joined by &
A query string is the part of a URL after the ?. At its simplest it is a flat list of pairs. Each key and its value become key=value, and the pairs are joined with &:
?name=ada&role=admin&page=2
That is the whole grammar for flat data. The catch is that almost nothing about real data is safe to drop into a URL as-is. A space, a slash, an ampersand inside a value — any of these will either break the URL or get misread as structure. So the real rule has a second half: every key and every value is run through encodeURIComponent before being joined. That function percent-encodes anything that isn't URL-safe.
- A space becomes
%20. - A slash becomes
%2F. - An embedded
&becomes%26, and an embedded=becomes%3D, so they can't be mistaken for separators. - Unicode is UTF-8 percent-encoded:
你好becomes%E4%BD%A0%E5%A5%BD.
That last point about embedded separators is the one people forget. If a value is literally x=1&y=2, you cannot paste it raw — the receiving parser would see three pairs instead of one. Encoded, it comes out as raw=x%3D1%26y%3D2, a single safe value. The same encodeURIComponent logic powers a dedicated URL encoder if you just need to escape one string, but for a whole object you want the full walk described below.
Flattening nested objects with bracket paths
Flat pairs are easy. Real config objects are not flat — they nest. A query string has no native concept of nesting, so we borrow a convention that several frameworks agree on: bracket path notation. Each level of nesting becomes a [key] segment appended to the parent key.
{ "user": { "name": "ada" } } → user[name]=ada
{ "page": { "size": 20 } } → page[size]=20
Go deeper and the brackets stack:
{ "user": { "address": { "city": "oslo" } } } → user[address][city]=oslo
The structural brackets stay literal — they are not percent-encoded — so the string is still readable and, crucially, parses back the same way. Only the names and values inside get encoded. This is the convention the qs library, PHP, and Rails all understand, which is why bracket-path output drops into most backends with no extra parsing.
Arrays: repeat, bracket, or comma
Arrays are where conventions split, because there is no single standard. The same tags: ["red", "blue"] can legitimately serialize three ways, and the right one depends entirely on what is reading the URL:
- Repeat:
tags=red&tags=blue. The same key appears twice. Express, Rails, and Go'sr.URL.Query()read this natively. - Bracket:
tags[]=red&tags[]=blue. Theqslibrary and PHP expect this form. - Comma:
tags=red,blue. Compact and common in search APIs.
There is a real trade-off here, not just a style preference. Comma form is the shortest, but it is one-way lossy: if a value itself contains a comma, you can't tell on parse whether a,b is one value or two. So for a guaranteed round trip — JSON to query string and back to identical JSON — pick repeat or bracket. Reserve comma for cases where you are only ever writing the URL, never reading it back into structured data.
A worked example
Here is a single object passed through the full conversion with repeat-style arrays and a leading ?:
Input JSON:
{
"q": "opus 4",
"tags": ["ai", "tools"],
"page": { "size": 20 },
"raw": "x=1&y=2"
}
Output query string:
?q=opus%204&tags=ai&tags=tools&page[size]=20&raw=x%3D1%26y%3D2
Read it left to right and every rule from above is visible. opus 4 became opus%204 because of the space. The array tags repeated its key once per element. The nested page.size flattened into the bracket path page[size]. And the booby-trapped raw value, full of separators, got fully escaped so it stays one value. That string drops straight behind your endpoint and works in a browser, in curl, or in any HTTP client.
Building API request URLs in practice
I reach for this conversion most often when I am debugging an endpoint by hand. My code already holds the request shape as an object — a search config, a filter set, pagination — and I need the literal URL to paste into curl and watch the raw response. I used to build those URLs by hand and lose ten minutes to a forgotten %20 or a tags[] I typed as tags. Now I paste the object, pick the array style the service expects, flip on the leading ?, and copy the result. The number of ampersand-counting mistakes dropped to zero, which sounds trivial until you've chased a 400 for fifteen minutes only to find an un-escaped space.
Two habits make the building reliable. First, match the array convention to the target before you copy — a Rails API wanting tags[]=a&tags[]=b will quietly ignore tags=a,b, and no error tells you why. Second, remember that a query string is text: {n: 42, ok: true} serializes to n=42&ok=true, and when the backend parses it, those come back as the strings "42" and "true", not a number and a boolean. Cast them on the receiving end. A good converter never guesses types, because guessing corrupts things like leading zeros and big integers.
Why URLSearchParams isn't enough
The browser ships URLSearchParams, so it is fair to ask why you need anything more. The answer is that it only handles a flat list of string pairs. Hand it a nested object and it gives up: new URLSearchParams({ user: { name: "ada" } }) produces the literal text user=[object Object], because it stringifies the value instead of walking into it. It also won't repeat array entries the way most backends expect. Anything with depth or arrays needs a real tree walk that emits bracket paths and lets you choose the array style — and then re-parses the result back. That round-trip piece is exactly what URLSearchParams leaves you to write yourself.
If you want to try the full conversion — both directions, all three array styles, sort and skip-empty toggles, and a live JSON-to-query-and-back preview — the JSON to Query String converter runs entirely in your browser. Nothing is uploaded, which matters when the object you are converting carries a token or an internal redirect you'd rather not log on someone else's server.
Made by Toolora · Updated 2026-06-13