Skip to main content

Putting JSON in a URL: Validate, Minify, Percent-Encode, or Base64url?

A decision guide for flattening, encoding, checking, and safely transporting JSON in a query string.

By Mohamed Zakrya

Updated · 9 min read

Share
Putting JSON in a URL: the decision, the pipeline, and the two representations Putting JSON in a URL Three tools do the steps. Nothing owns the order — until now. encoding ≠ encryption STEP 1 · DECIDE THE SHAPE Separate parameters Fields independent and readable ?category=books&sort=price Routing and caching can read them. One JSON parameter Nested state that travels as a unit ?state=%7B%22filters%22... No standard flattening exists. Request body Sensitive, large, or often mutated POST + application/json A GET body has no defined semantics. STEP 2 · RUN THE PIPELINE, THEN RUN IT BACKWARDS Validate is it JSON at all? Minify compact serialize Encode once one layer only Insert one parameter Round-trip compare values Encode twice and a brace becomes unreadable { → %7B → %257B Do not decode “until it looks right” — that turns valid data into different data. STEP 3 · PICK THE REPRESENTATION Percent-encoding The parameter still contains JSON text. One ordinary decode returns it. Size depends on content. Base64url An opaque token. Four characters per three bytes, a fixed expansion — and still not encryption.
A decision guide for flattening, encoding, checking, and safely transporting JSON in a query string.

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.

Choosing between separate parameters, one JSON parameter, and a request body Where does the data go? Three answers, and the question that separates them Data to place in a URL fields independent? nested, safe to share? sensitive or oversized? Separate query parameters readable, cacheable, validated field by field One encoded JSON parameter travels as one unit, schema can change Request body, or state + identifier keeps the payload out of history and logs A hybrid is usually the honest answer Keep routing fields such as page and sort as ordinary parameters; put only the nested filter state in one encoded parameter. Do not carry the same field in both without saying which wins.
Choose ordinary parameters for independent fields, a single encoded JSON parameter for nested portable state, or a request body when the payload is sensitive or unsuitable for a URL.

Follow one encoding pipeline

Treat encoding as a pipeline with a defined inverse:

  1. Validate the JSON or the in-memory value that will become JSON.
  2. Remove insignificant whitespace by serializing to compact JSON.
  3. Convert the resulting text to UTF-8 bytes where the chosen encoding requires bytes.
  4. Apply exactly one transport representation: percent-encoding or Base64url.
  5. Insert that result as one query parameter.
  6. On receipt, reverse those operations once and parse the JSON.
  7. 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.

The forward encoding pipeline and its exact inverse One pipeline, run backwards Each step on the way out has exactly one step on the way back SENDING Validate is it JSON at all? Minify serialize compact UTF-8 bytes where required Encode once %-escape or b64url Insert one parameter the same URL, received RECEIVING Extract read the parameter Decode once not until it looks right UTF-8 text back to characters Parse JSON syntax check Validate against a schema Only one layer owns query encoding If application code escapes the value and a URL builder escapes it again, a literal { becomes %7B and then %257B. Decoding "until it looks right" can turn valid data into different data.
The forward path is validate, minify, encode, and insert; the reverse path is extract, decode once, parse, and compare.

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.parse or 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.

Round-trip test through the real application boundaries Test the whole trip, not the encoder A test that calls encode and decode can pass while a proxy in between truncates the request Original value in memory Production URL builder Intermediaries gateway, proxy, redirect Receiving parser decode once, parse Recovered value in memory Compare structurally, not as text member order and number formatting may differ legitimately WHAT THE LENGTH BUDGET COUNTS https:// host /path ?state_v=1&state=%7B%22filters%22%3A%7B...%7D%7D All of it — scheme, host, path, parameter names, separators and the encoded value. Not the source JSON. RFC 9110 recommends supporting at least 8,000 octets; your deployed path decides the real ceiling.
Build and parse the complete URL through the real application boundaries, then decode once, parse the JSON, and compare the recovered value structurally with the original.

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.