Putting JSON in a URL is not a single encoding task. The first decision is whether the data should remain a JSON object at all. A small set of independent filters may belong in ordinary query parameters, while nested state may be easier to transport as one serialized value.
If the object stays intact, the reliable sequence is validate → minify → serialize once → percent-encode or Base64url-encode → verify the complete round trip. Changing that order can preserve invalid input, add unnecessary bytes, or produce a value that one layer decodes before another layer sees it.
A query string also has operational consequences. It can appear in browser history, infrastructure records, copied links, analytics systems, and some Referer headers. Encoding changes the representation—not the sensitivity of the underlying data.
Decide whether you need a JSON parameter
Use separate query parameters when each field has a clear meaning at the URL level:
/search?category=books&sort=price&direction=asc
This shape is readable, easy to modify, and available to routing, caching, observability, and analytics systems without parsing an embedded document. It also lets the server validate each field against an explicit contract.
Keep one JSON value when the state is genuinely nested, fields must travel as one unit, or the structure changes too often for a stable flat parameter list:
{
"filters": {
"category": ["books", "games"],
"price": { "min": 10, "max": 50 }
},
"sort": [{ "field": "price", "direction": "asc" }]
}
Flattening that object requires conventions for arrays, nested keys, missing values, null, and repeated parameters. Neither RFC 3986 nor the WHATWG URL standard defines one universal mapping from nested JSON to query parameters, so the producer and consumer would need their own agreement.
A hybrid is often clearer: keep routing fields such as page and sort separate, then place only the nested filter state in a filters parameter. Do not duplicate the same field inside and outside the JSON unless the contract specifies which value wins.
Follow one encoding pipeline
Treat encoding as a pipeline with a defined inverse:
- Validate the JSON or the in-memory value that will become JSON.
- Remove insignificant whitespace by serializing to compact JSON.
- Convert the resulting text to UTF-8 bytes where the chosen encoding requires bytes.
- Apply exactly one transport representation: percent-encoding or Base64url.
- Insert that result as one query parameter.
- On receipt, reverse those operations once and parse the JSON.
- Compare the decoded value with the original value.
Validation belongs before transport encoding because a successful URL decode says nothing about whether the result is valid JSON. Use the JSON Formatter and Validator to catch syntax problems and produce a compact representation before measuring or encoding the payload.
Minification removes insignificant JSON whitespace but does not create a canonical representation. Two objects can be semantically equivalent while differing in member order or number formatting. If the bytes will be signed or hashed across separate implementations, use an agreed canonicalization scheme; RFC 8785 defines the JSON Canonicalization Scheme for that purpose.
Choose percent-encoding for transparent JSON
Percent-encoding is the direct choice when the query parameter conceptually contains JSON text. The encoded URL is not attractive to read, but one normal query decode returns the original JSON without another representation layer.
In browser JavaScript, either let URLSearchParams encode the raw JSON value:
const json = JSON.stringify(value);
const params = new URLSearchParams({ state: json });
const url = `/view?${params.toString()}`;
Or, if constructing the parameter directly, apply encodeURIComponent once:
const json = JSON.stringify(value);
const url = `/view?state=${encodeURIComponent(json)}`;
Do not combine those forms by passing an already percent-encoded value to URLSearchParams. On the receiving side, URL.searchParams.get("state") returns the decoded parameter value, so it should normally go directly to JSON.parse:
const raw = new URL(location.href).searchParams.get("state");
const value = JSON.parse(raw);
The URL Encoder and Decoder is useful for inspecting one layer at a time. If decoding once still leaves sequences such as %7B, determine whether the producer encoded twice rather than automatically decoding again. Repeated decoding can alter legitimate percent sequences that belong to the data.
Choose Base64url for an opaque URL-safe value
Base64url is useful when a system handles the state as an opaque token, or when avoiding a dense collection of percent escapes makes the surrounding URL easier to process. RFC 4648 section 5 defines its alphabet by replacing standard Base64’s + and / with - and _.
The input should be the UTF-8 bytes of the compact JSON, not an implementation-specific sequence of character code units:
JSON value
→ compact JSON text
→ UTF-8 bytes
→ Base64url text
→ query parameter
RFC 4648 encoding emits four characters for each complete group of three input bytes, before any padding is omitted under the surrounding protocol’s rules. Percent-encoding has no comparable fixed expansion because its size depends on which bytes require escaping. Base64url can therefore be shorter for punctuation-heavy or non-ASCII JSON and longer for JSON dominated by URL-safe ASCII. Measure the actual complete URL rather than choosing from appearance alone.
Padding is part of the contract. RFC 4648 requires padding in the general case unless the referring specification explicitly permits it to be omitted. If your application uses unpadded Base64url, document that choice and restore the required padding before calling a decoder that expects it.
The Base64 Encoder and Decoder can help verify the intermediate representation. Base64url is reversible transport encoding—not encryption, authorization, signing, or tamper detection.
Prevent double-encoding at API boundaries
Double-encoding usually happens because two components both believe they own serialization. For example, application code calls encodeURIComponent, then a query builder percent-encodes the already encoded string. A literal { first becomes %7B; the percent sign can then become %25, producing %257B.
Assign each transformation to one boundary:
- The JSON serializer owns object-to-text conversion.
- One URL-building API owns query percent-encoding.
- The HTTP framework owns parsing the query string.
- The application owns
JSON.parseor Base64url decoding after extraction.
Log or test the value at those boundaries during development. Do not create a decoder that keeps decoding until the text “looks right.” That behavior is ambiguous and can turn valid data into different data.
A version or encoding marker can make long-lived links easier to migrate:
?state_v=1&state=<encoded-value>
The version can define whether state contains percent-encoded JSON, unpadded Base64url, or a later schema. It should not be inferred from the first character because different representations can overlap.
Verify the round trip, not just the encoded string
A useful test starts with a real value, builds the entire URL through the production URL API, parses that URL through the production receiving API, reverses one encoding layer, and parses the JSON. Compare values structurally rather than comparing formatted JSON text.
Include cases with nested arrays, null, empty strings, quotes, backslashes, percent signs, ampersands, equals signs, spaces, emoji, and non-Latin text. These cases expose mistaken character encodings, manual query concatenation, and extra decoding layers.
Also test the actual route through gateways, proxies, redirects, and server frameworks. A unit test that only calls an encoder and decoder can pass while an intermediary rejects, truncates, normalizes, or records the complete request target.
Treat URL length as an end-to-end constraint
RFC 3986 defines URI syntax but does not establish one universal maximum URI length. RFC 9110 recommends that senders and recipients support URIs of at least 8,000 octets in protocol elements, but that interoperability recommendation is not a guarantee for every browser, proxy, framework, server configuration, redirect, or downstream integration.
For that reason, a practical limit belongs to your deployed path. Test the full encoded URL through every component that must accept it and set an application-level ceiling with explicit error handling. Count the scheme, authority, path, question mark, parameter names, separators, and encoded values—not merely the source JSON.
Minification can reduce the source before either encoding method, but it cannot make an unbounded payload appropriate for a URL. If users can add arbitrary filters, selections, editor state, or imported data, impose a schema and size policy before building the URL.
Assume query data can be observed
A URL is often copied, bookmarked, synchronized, recorded in browser history, included in screenshots, or captured by monitoring and analytics systems. Apache HTTP Server’s documented %r field for access-log formats records the request line, which can include the query string.
The Referrer Policy standard’s default policy is strict-origin-when-cross-origin. Under that policy, cross-origin requests normally receive only the origin, but same-origin requests can receive the full referring URL, including its query. Applications can set a stricter Referrer-Policy, but that does not remove copies already stored elsewhere.
Do not put passwords, session tokens, private keys, authorization credentials, personal records, or other sensitive JSON in a query string. Percent-encoding and Base64url only transform the data. If confidentiality is required, use an authenticated application flow and keep the sensitive payload out of the URL.
Move unsuitable payloads into a request body
Use a request body when the JSON is sensitive, large enough to challenge the application’s tested URL ceiling, frequently mutated, or meaningful as submitted content rather than as a resource identifier. Send it with an appropriate media type such as application/json and apply authentication, authorization, validation, and request-size controls independently.
Do not solve the problem by attaching a body to GET. RFC 9110 states that content in a GET request has no generally defined semantics and can cause some implementations to reject the request because of request-smuggling concerns. Use a method whose semantics fit the operation, commonly POST when submitting JSON for processing.
If the state must remain shareable, store it server-side and place a random identifier in the URL. The identifier should be authorized on every read and should not expose the stored content by itself. This trades a self-contained link for revocation, access control, and a shorter request target.
For a self-contained URL, finish with four checks: the object has a defined schema, one component owns encoding, the deployed route accepts the complete URL, and the decoded value matches the original. Start with ordinary parameters when they describe the state cleanly; otherwise validate and minify the JSON, then choose percent-encoding for transparent text or Base64url for an agreed opaque representation.