Skip to content

Latest commit

 

History

History
325 lines (222 loc) · 6.15 KB

spec.md

File metadata and controls

325 lines (222 loc) · 6.15 KB

Specification

For a kitchen sink example, see Kitchen Sink at the bottom of this document.

Objects

The root object is unbounded, its properties are separated by ampersands (&), and its property names end with an equals sign (=).

The following object…

{ a: 0, b: 1 }

…is encoded into the following query string:

a=0&b=1

Nested objects

Nested objects are bounded by curly braces ({}), their properties are separated by commas (,), and their property names end with a colon (:).

The following object…

{
  a: { b: 0, c: 1 }
}

…is encoded into the following query string:

a={b:0,c:1}

Property names

At the root level, property names are percent-encoded, like any other query string. This ensures the new URLSearchParams constructor can parse the root object for you.

In nested objects, property names are encoded the same way strings are encoded. The only exception is related to numbers and the minus sign (-), which are never escaped in property names.

The following object…

{
  a: { 1: 2 }
}

…is encoded into the following query string:

a={1:2}

We don't quote property names, because it takes more space. Also, neither apostrophes nor double quotes are URI-safe unless percent-encoded (which we try to avoid).

Prototype pollution

A property name of __proto__ is forbidden.

Empty objects

An empty object is encoded as {}, unless it's the root object, in which case it's encoded as nothing.

Object coercion

If an object has a toJSON method, it will be invoked and the result will be encoded instead of the object itself. The toJSON method may have properties whose values also have toJSON methods, and those will be invoked recursively.

{
  a: { ignoredKey: true, toJSON: () => ({ b: 1 }) }
}

…is encoded into the following query string:

a={b:1}

Note: If the root object has a toJSON method, it needs to return an object.

Strings

Strings are not wrapped in quotes. If a string would lead to ambiguity, its characters may be escaped with a backslash (\) as required.

The following string…

{
  theme: 'dark'
}

…is encoded into the following query string:

theme=dark

Escaping

Since strings aren't wrapped in quotes, many characters require special handling.

For example, these characters are always escaped with a backslash (\):

  • curly braces
  • parentheses
  • commas
  • colons

And these characters are escaped if they're the first character, since they would otherwise imply another data type:

  • digits (if not escaped, implies a number)
  • hyphens, but only if followed by a digit (if not escaped, implies a negative number)
  • backslashes (if not escaped, implies an escape sequence)

The following object…

{
  a: '{b:0}',
}

…is encoded into the following query string:

a=\{b:0\}

Percent-encoding

Some characters have special meaning in query strings, so they must be percent-encoded:

  • ampersands &
  • percent signs %
  • plus signs +
  • hash signs #

Note that while non-ASCII characters (e.g. accented letters, Chinese, Japanese, emojis, etc.) are not explicitly handled by this specification, they will be percent-encoded by the fetch API or similar.

Empty strings

An empty string is encoded as nothing.

Arrays

Arrays are bounded by parentheses () and their elements are separated by commas ,.

The following array…

{
  a: [0, 1]
}

…is encoded into the following query string:

a=(0,1)

Why use parentheses and not square brackets?

While square brackets are more aligned with the JSON syntax, using them here would require escaping square brackets in JSON paths, because we don't wrap string values with quotes or some other delimiter.

We've decided it's better for readability if JSON paths aren't littered with escapes as often.

Empty arrays

An empty array is encoded as ().

Undefined Values

Like in JSON, undefined values are ignored in objects.

{
  a: undefined,
  b: 2,
}

…is encoded into the following query string:

b=2

Arrays with undefined values

Like in JSON, undefined values are coerced to null in arrays.

{
  a: [undefined]
}

…is encoded into the following query string:

a=(null)

Bigints

Bigints are simply stringified with an "n" suffix.

The following object…

{
  a: 9007199254740992n
}

…is encoded into the following query string:

a=9007199254740992n

Dates

Dates are stringified to their ISO 8601 string representation. If a date is at midnight UTC, the time component is omitted. If the year exceeds 9999, there may exist a + prefix, which needs to be percent-encoded.

The following objects…

{
  a: new Date('2024-10-27T00:00:00.000Z')
}

{
  b: new Date('2024-10-27T12:34:56.789Z')
}

{
  c: new Date('+10000-01-01T00:00:00.000Z')
}

…are encoded into the following query strings:

a=2024-10-27

b=2024-10-27T12:34:56.789Z

c=%2B010000-01-01

Unlike in strings, colons are not escaped in dates, since they're unlikely to be misread as a property name separator when judged at a glance.

Everything Else

The remaining JSON types are merely stringified:

  • boolean
  • number
  • null

Unsupported Values

These values are coerced to null just like in JSON:

  • NaN
  • ±Infinity
  • undefined (ignored by objects)

Kitchen Sink

The following object…

{
  object: { a: 0, b: 1 },
  array: [-0, -1],
  string: 'hello',
  fraction: 1.23,
  true: true,
  false: false,
  null: null,
  undefined: undefined,
  infinity: Infinity,
  nan: NaN,
  bigint: 9007199254740992n,
  sciNotation: 1e100,
  sparseArray: [,,],
  nestedArray: [[0, 1], [2, 3]],
  objectInArray: [{ a: 0 }],
  emptyArray: [],
  emptyObject: {},
}

…is encoded into the following query string (formatted for readability):

object={a:0,b:1}
&array=(0,-1)
&string=hello
&fraction=1.23
&true=true
&false=false
&null=null
&infinity=null
&nan=null
&bigint=9007199254740992n
&sciNotation=1e100
&sparseArray=(null,null)
&nestedArray=((0,1),(2,3))
&objectInArray=({a:0})
&emptyArray=()
&emptyObject={}

Note the lack of undefined in the output.