Search code examples
javascriptdecodeencodeurl-parameters

Javascript: recursively encode and decode querystring. (object to querystring, querystring to object)


I want to encode a complex json/javascript object into the standard querystring encoding. And i want to decode this querystring back to an json/javascript object. It should be recursively, with arrays, objects, strings, booleans and numbers.

I thought this should be easy, but was proven wrong. Does anyone have an idea, how to solve this problem? Either in Javascript or preferably in Typescript.


Solution

  • I think, what you want to do, is encode and decode nested objects.

    There is no single standard, but very often, QS (Query String) syntax is used:

    {
        "attribute": "value",
        "array": ["apples", "bananas"],
        "object": { "number": 55 },
    }
    

    will become:

    ?attribute=value&array[0]=apples&array[1]=bananas&object[number]=55
    

    Example code:

    function decode(querystring: string): object {
        function parseValue(value: string): any {
            if (value === 'TRUE') return true;
            if (value === 'FALSE') return false;
            return isNaN(Number(value)) ? value : Number(value);
        }
    
        function dec(list: any[], isArray = false): object {
            let obj: any = isArray ? [] : {};
    
            let recs: any[] = list.filter((item) => {
                if (item.keys.length > 1) return true;
                obj[item.keys[0]] = parseValue(item.value);
            });
    
            let attrs = {};
            recs.map((item) => {
                item.key = item.keys.shift();
                attrs[item.key] = [];
                return item;
            }).forEach((item) => attrs[item.key].push(item));
    
            Object.keys(attrs).forEach((attr) => {
                let nextKey = attrs[attr][0].keys[0];
                obj[attr] = dec(attrs[attr], typeof nextKey === 'number');
            });
    
            return obj;
        }
    
        return dec(
            querystring
                .split('&')
                .map((item) => item.split('=').map((x) => decodeURIComponent(x)))
                .map((item) => {
                    return {
                        keys: item[0]
                            .split(/[\[\]]/g)
                            .filter((n) => n)
                            .map((key) => (isNaN(Number(key)) ? key : Number(key))),
                        value: item[1],
                    };
                })
        );
    }
    
    export function encode(object: object): string {
        function reducer(obj, parentPrefix = null) {
            return function (prev, key) {
                const val = obj[key];
                key = encodeURIComponent(key);
                const prefix = parentPrefix ? `${parentPrefix}[${key}]` : key;
    
                if (val == null || typeof val === 'function') {
                    prev.push(`${prefix}=`);
                    return prev;
                }
    
                if (typeof val === 'boolean') {
                    prev.push(`${prefix}=${val.toString().toUpperCase()}`);
                    return prev;
                }
    
                if (['number', 'string'].includes(typeof val)) {
                    prev.push(`${prefix}=${encodeURIComponent(val)}`);
                    return prev;
                }
    
                prev.push(
                    Object.keys(val).reduce(reducer(val, prefix), []).join('&')
                );
                return prev;
            };
        }
    
        return Object.keys(object).reduce(reducer(object), []).join('&');
    }