Skip to the code
cf.

Parse a query string

Write parseQueryString(query). It turns a URL query string into an object that maps each key to its value. Write the parsing yourself, without URLSearchParams.

Ignore one leading ?. Split the rest into pairs at &, skipping empty pairs such as the ones && or a trailing & leave. Split each pair at its first =: what comes before is the key, and everything after is the value, even if it contains more =. A pair with no = is a key whose value is "".

Decode the key and the value after splitting: first turn every + into a space, then decode percent-escapes with decodeURIComponent, so %26 is a literal & and %2B a literal +. If a key or value has malformed percent-encoding, keep it as it is (with its + turned into spaces) instead of throwing.

A key that appears once maps to its string value; a key that appears more than once maps to an array of its values, in order. Every key, __proto__ included, must become an ordinary own property of the result, so a query can never change the object's prototype.

parseQueryString("?q=red+shoes&tag=a&tag=b&debug");
// { q: "red shoes", tag: ["a", "b"], debug: "" }

What the tests check

  1. parses key=value pairs, with or without a leading ?
    expect(parseQueryString("?a=1&b=two")).toEqual({ a: "1", b: "two" });
    expect(parseQueryString("a=1")).toEqual({ a: "1" });
    
  2. decodes + as a space, and percent-escapes
    expect(parseQueryString("q=red+shoes&city=S%C3%A3o%20Paulo&sum=1%2B1")).toEqual({ q: "red shoes", city: "São Paulo", sum: "1+1" });
    
  3. decodes keys and values only after splitting
    expect(parseQueryString("a=1%262&b%3Dc=x&first+name=Ada")).toEqual({ a: "1&2", "b=c": "x", "first name": "Ada" });
    
  4. splits each pair at its first = only
    expect(parseQueryString("token=abc==&eq=a=b")).toEqual({ token: "abc==", eq: "a=b" });
    
  5. gives a key without = an empty value, and skips empty pairs
    expect(parseQueryString("debug&&a=&")).toEqual({ debug: "", a: "" });
    expect(parseQueryString("")).toEqual({});
    expect(parseQueryString("?")).toEqual({});
    
  6. collects repeated keys into an array, in order
    expect(parseQueryString("tag=a&x=1&tag=b&tag=c")).toEqual({ tag: ["a", "b", "c"], x: "1" });
    expect(parseQueryString("k=1&k=1")).toEqual({ k: ["1", "1"] });
    
  7. keeps malformed percent-encoding as it is
    expect(parseQueryString("a=100%&b=%E0%A4%A&c=%41&d=50%+off")).toEqual({ a: "100%", b: "%E0%A4%A", c: "A", d: "50% off" });
    
  8. turns __proto__ into an ordinary key
    const result = parseQueryString("__proto__=x&__proto__=y&a=1");
    expect(Object.keys(result)).toEqual(["__proto__", "a"]);
    expect(Object.getOwnPropertyDescriptor(result, "__proto__").value).toEqual(["x", "y"]);
    expect(Array.isArray(Object.getPrototypeOf(result))).toBe(false);
    expect(result.a).toBe("1");
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).