Flatten Deep JavaScript Objects for AJAX Requests

It’s easy and convenient to use objects when we want to send named parameters with an AJAX request. With simple objects that work well, however, with deep/nested JavaScript objects don’t really work the way they should. Let’s see how to convert them!


Query String Basics

So, let’s start with the basics, and explore the anatomy of a query string.

The query string – simply and briefly – provides the possibility to pass data and parameters to the endpoint without changing the path of the URI. What does a query string look like then?

https://example.com/api/products?sort[by]=created_at&sort[order]=desc&status=pending&exclude[]=1&exclude[]=2
^^^^^^^|^^^^^^^^^^^|^^^^^^^^^^^^|^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
schema | host      | path       | query string

We can access the query string’s parameters with the $_GET  variable. If we would print, the example query string in PHP, we would get the following:

[
    'sort' => ['by' => 'created_at', 'order' => 'desc'],
    'status' => 'pending',
    'exclude' => [1, 2]
]

It’s visible that we can create nested query string parameters, that we can use perfectly on our back-end. One good example would be filtering results by the parameters of the query string.

But, since this works well with the traditional HTML forms, how can we achieve this query string behavior with JavaScript and AJAX? How can we convert JavaScript objects into a query string? Well, let’s take a look!

The problem with converting objects

The basic problem is simple. When we work for example with Vue and Axios, we might want to pass an object to Axios’ AJAX call, that we also use for two-way data binding with Vue. Let’s stick with the example before and take and how should the example query string look like in a JS object:

query: {
    sort: {
        by: 'created_at',
        order: 'desc'
    },
    status: 'pending',
    exclude: [1, 2]
}

Why is it important to keep the object like this? Well, because we can use two-way data binding and watching if the object changes. It’s a good technique to watch the query object and fire a new AJAX request with the new parameters when the object changes.

Now, let’s move on the issue with this solution. If we passing the object itself like this to the Axios request, the deep objects will be converted to JSON. It means the query string will look like the following:

?sort={"by":"created_at","order":"desc"}&status=pending&exclude[]=1&exclude[]=2

This means, on the back-end, the $_GET[‘sort’] will be a string and not an array. We need to apply the json_decode function to be able to use the sort parameter.

This is not the best solution, because this forces us to apply at least one extra step in our back-end. Of course, once it’s fine, but if we work with complex APIs that uses complex query strings for filtering, we might be very strict with the data we accept. So let’s see a solution that allows us to keep simple our back-end and forces the front-end to send a “valid” data.

Flattening objects and preserving the keys

The basic idea is not more, but to loop through the complex and deep object and flatten it, but also keep the keys. This means we will have a one-level flat object that we can pass to the Axios request and be sure, nothing will be converted to JSON. Here is the function to flatten the given object:

flatten(object, prefix = '') => {
    return Object.keys(object).reduce((carry, key) => {
        const pre = prefix ? prefix + `[${key}]` : '';

        if (Array.isArray(object[key])) {
            carry = object[key].reduce((array, value, index) => {
                array[(pre || key) + `[${index}]`] = value;
                return array;
            }, carry);
        } else if (object[key] && typeof object[key] === 'object') {
            Object.assign(carry, flatten(object[key], pre || key));
        } else {
            carry[pre || key] = object[key];
        }

        return carry;
    }, {});
};

It’s a convenient solution to create a new computed property that flattens the original query object we have already.

computed: {
    flattenedQuery() {
        return flatten(this.query);
    }
}

The flattened result of the example query object would be the following:

{
  sort[by]: 'created_at',
  sort[order]: 'desc',
  status: 'pending',
  exclude[0]: 1,
  exclude[1]: 2
}

So, as we see we get one single-level object, that we can easily pass to the request and the result will be the one we would expect, on the back-end we get the same result for the $_GET variable like before.

Summary

This technique can be very useful and handy if you are working with complex queries and requests. However, if you have very simple parameters, you don’t need a solution like this. Also, if you want to keep your back-end cleaner a bit – because you want to leave converting values there and back – this approach can be a good start.

Need a web developer? Maybe we can help, get in touch!