Search code examples
javascriptjsonstringquery-stringx-www-form-urlencoded

How to convert query string to multi level object


I'm currently trying to convert a URL query string to a JavaScript Object, which isn't going so well. I've looked around Stack Overflow for possible solutions but haven't have quite found what I was looking for. This is the query string:

"class%5Blocation_id%5D=122&student%5Bgender%5D=&student%5Bpicture%5D=&class%5Bquestions%5D%5B2775%5D%5Banswers%5D%5B%5D=Black+canary&ids%5B%5D=32&class%5Bquestions%5D%5B2775%5D%5Banswer%5D=&class%5Bquestions%5D%5B2776%5D%5Banswers%5D%5B%5D=Blue+Whistle&class%5Bquestions%5D%5B2776%5D%5Banswer%5D=&class%5Bdescription%5D="

I'm looking for something like this:

{
   class: {
     description: '',
     location_id: '122',
     questions: {
       2275: {
         answer: '',
         answers: ['Black canary']
       },
       2276: {
         answer: '',
         answers: ['Blue Whistle']
      }
     }
   },
   ids: ['32']
   student: {
     gender: '',
     picture: ''
   }
}

I tried using a library called query-string, but this is what I get:

{
  'class[description]': '',
  'class[location_id]': '122',
  'class[questions][2775][answer]': '',
  'class[questions][2775][answers][]': 'Black canary',
  'class[questions][2776][answer]': '',
  'class[questions][2776][answers][]': 'Blue Whistle',
  'ids[]': '32',
  'student[gender]': '',
  'student[picture]': '' 
}

And I tried using two implementations I found here:

  function form2Json(str) {
    "use strict";
    var obj, i, pt, keys, j, ev;
    if (typeof form2Json.br !== 'function') {
      form2Json.br = function (repl) {
        if (repl.indexOf(']') !== -1) {
          return repl.replace(/\](.+?)(,|$)/g, function ($1, $2, $3) {
            return form2Json.br($2 + '}' + $3);
          });
        }
        return repl;
      };
    }
    str = '{"' + (str.indexOf('%') !== -1 ? decodeURI(str) : str) + '"}';
    obj = str.replace(/\=/g, '":"').replace(/&/g, '","').replace(/\[/g, '":{"');
    obj = JSON.parse(obj.replace(/\](.+?)(,|$)/g, function ($1, $2, $3) { return form2Json.br($2 + '}' + $3); }));
    pt = ('&' + str).replace(/(\[|\]|\=)/g, '"$1"').replace(/\]"+/g, ']').replace(/&([^\[\=]+?)(\[|\=)/g, '"&["$1]$2');
    pt = (pt + '"').replace(/^"&/, '').split('&');
    for (i = 0; i < pt.length; i++) {
      ev = obj;
      keys = pt[i].match(/(?!:(\["))([^"]+?)(?=("\]))/g);
      for (j = 0; j < keys.length; j++) {
        if (!ev.hasOwnProperty(keys[j])) {
          if (keys.length > (j + 1)) {
            ev[keys[j]] = {};
          }
          else {
            ev[keys[j]] = pt[i].split('=')[1].replace(/"/g, '');
            break;
          }
        }
        ev = ev[keys[j]];
      }
    }
    return obj;
  }

But ended up with this:

  {
    class: {
      description: "",
      location_id: "122"
    },
    questions:{
         2775: {answers: "Black+canary", answer: ""}
         2776: {answers: "Blue+Whistle", answer: ""}
    },
    ids: {
         "": "32"
    },
    student: {
         gender: ""
         picture: ""
    }
   }

ids becoming an object instead of an array and answers becoming a string. The closest I could get to it was using this:

   function form2Json(str) {
    "use strict";
    var pairs = str.split('&'),
      result = {};

    for (var i = 0; i < pairs.length; i++) {
      var pair = pairs[i].split('='),
        key = decodeURIComponent(pair[0]),
        value = decodeURIComponent(pair[1]),
        isArray = /\[\]$/.test(key),
        dictMatch = key.match(/^(.+)\[([^\]]+)\]$/);

      if (dictMatch) {
        key = dictMatch[1];
        var subkey = dictMatch[2];

        result[key] = result[key] || {};
        result[key][subkey] = value;
      } else if (isArray) {
        key = key.substring(0, key.length - 2);
        result[key] = result[key] || [];
        result[key].push(value);
      } else {
        result[key] = value;
      }
    }

    return result;
  }

With this result:

{
   class: {location_id: "122", description: ""},
   class[questions][2775]: {answer: ""},
   class[questions][2775][answers]: ["Black+canary"],
   class[questions][2776]: {answer: ""},
   class[questions][2776][answers]: ["Blue+Whistle"],
   ids: ["32"],
   student: {gender: "", picture: ""}
}

...with only up to first level association, and the array of ids being assigned properly as an array. How can I make this work?


Solution

  • You can use URLSearchParams:

    function form2Object(str) {
        let result = {};
        for (let [path, value] of new URLSearchParams(str).entries()) {
             let keys = path.match(/[^[\]]+/g);
             let prop = keys.pop();
             let acc = result;
             for (let key of keys) acc = (acc[key] = acc[key] || {});
             acc[prop] = path.endsWith("[]") 
                       ? (acc[prop] || []).concat(value)
                       : value;
        }
        return result;
    }
    // Demo
    let str = "class%5Blocation_id%5D=122&student%5Bgender%5D=&student%5Bpicture%5D=&class%5Bquestions%5D%5B2775%5D%5Banswers%5D%5B%5D=Black+canary&ids%5B%5D=32&class%5Bquestions%5D%5B2775%5D%5Banswer%5D=&class%5Bquestions%5D%5B2776%5D%5Banswers%5D%5B%5D=Blue+Whistle&class%5Bquestions%5D%5B2776%5D%5Banswer%5D=&class%5Bdescription%5D=";
    console.log(form2Object(str));

    For older browsers

    This is EcmaScript 2015 compatible:

    function form2Object(str) {
        var result = {};
        str.split("&").map(function(eq) {
            return eq.split("=").map(decodeURIComponent);
        }).forEach(function (entry) {
            var path = entry[0];
            var value = entry[1];
            var keys = path.match(/[^[\]]+/g);
            var prop = keys.pop();
            var acc = keys.reduce(function (acc, key) {
                return (acc[key] = acc[key] || {});
            }, result);
            acc[prop] = path.slice(-2) === "[]"
                      ? (acc[prop] || []).concat(value)
                      : value;
        });
        return result;
    }
    // Demo
    let str = "class%5Blocation_id%5D=122&student%5Bgender%5D=&student%5Bpicture%5D=&class%5Bquestions%5D%5B2775%5D%5Banswers%5D%5B%5D=Black+canary&ids%5B%5D=32&class%5Bquestions%5D%5B2775%5D%5Banswer%5D=&class%5Bquestions%5D%5B2776%5D%5Banswers%5D%5B%5D=Blue+Whistle&class%5Bquestions%5D%5B2776%5D%5Banswer%5D=&class%5Bdescription%5D=";
    
    console.log(form2Object(str));