Repository: angular
Issue: Support for object in queryParams
Labels: feature,area: router,feature: under consideration

### Which @angular/* package(s) are relevant/related to the feature request?

router

### Description

When we create an url tree, the ["tree" function](https://github.dev/angular/angular/blob/main/packages/router/src/create_url_tree.ts#L180-L199
) will [serialize the queryParams](https://github.dev/angular/angular/blob/main/packages/router/src/create_url_tree.ts#L186) for us.

While this serialization is mandatory for the object to fit into an url, that one is in my understanding not done in the right place. The `UrlSerializer` is supposed to do that job, but it cannot do it properly anymore because the `queryParams`  has already been serialized. For instance, if you want to pass an object into a `queryParam`, you will end up having a serialized string: `[object Object]`. There is [another ticket](https://github.com/angular/angular/issues/40937) suggesting allowing overriding this serializing.

The typing of `queryParams` is also confusing. Here is the type
```typescript
export declare type Params = {
    [key: string]: any;
};
```
While accessing the params through ParamMap returns a `string`

```typescript
export declare interface ParamMap {
    /**
     * Reports whether the map contains a given parameter.
     * @param name The parameter name.
     * @returns True if the map contains the given parameter, false otherwise.
     */
    has(name: string): boolean;
    /**
     * Retrieves a single value for a parameter.
     * @param name The parameter name.
     * @return The parameter's single value,
     * or the first value if the parameter has multiple values,
     * or `null` when there is no such parameter.
     */
    get(name: string): string | null;
    /**
     * Retrieves multiple values for a parameter.
     * @param name The parameter name.
     * @return An array containing one or more values,
     * or an empty array if there is no such parameter.
     *
     */
    getAll(name: string): string[];
    /** Names of the parameters in the map. */
    readonly keys: string[];
}
```
While I understand that we have at some point to serialize the `queryParams`, the current way is not flexible enough for my needs.

### Proposed solution

The first solution I do think about is keeping the queryParams as is, i.e. without serializing them. A Tree has Params which are of type `Record<string, any>` just keep them so that we can retrieve the original value later.

But I can imagine there are other problems because we want to keep a snapshot of the routes, and we therefore need to serialize those parameters at some point (and this is not even related to the URL but the internal snapshot). I can think there are other as well as internal `queryParams` comparison from one route to another (where strings or array of strings are easy to compare). Keeping both (the original and the stringified version) would also perhaps be an overkill. Note that I did not browse all the code base to see if there are such problems.

The cleanest solution I can think about would be to delegate the job of serializing to a service 'ParamsSerializer' so that we can then inject any logic we want.

```typescript
// bonus to explicitly show those are generically parsable values, but using "Params" would be fine too
type JSONValue =
    | string
    | number
    | boolean
    | { [x: string]: JSONValue }
    | Array<JSONValue>;

class ParamsSerializer {
  serialize(params: Params): string
  parse(stringParams: string): Record<string, JSONValue>
}
```

The serialize method would be called [at that point](https://github.dev/angular/angular/blob/main/packages/router/src/create_url_tree.ts#L185-L187)

And the parse function to reconstruct the Params (there is no equivalent in the code yet). The ParamMap would also be able to return the JSONValue instead (currently `string`).

```typescript
export declare interface ParamMap {
...
    get(name: string): JSONValue | null;
    getAll(name: string): JSONValue[];
}
```



### Alternatives considered

What I do really want is to pass any object within the `queryParams` and being able to retrieve it back later.

We have 3 problems to solve:
1. The object we pass must be serializable: we need a generic or custom way to convert our object to a JSONValue one. 
2. Once serialized, they should fit the URL : This is the responsibility of the [UrlSerializer](https://angular.io/api/router/UrlSerializer)
3. We should be able to revive those objects from the URL: 

Currently, I'm simply passing queryParams which are of type `Record<string, string>;`
I just manually serialize my objects and then use my custom parser function.

Here is my helper:

```typescript
import { Injectable } from '@angular/core';
import JsonURL from '@jsonurl/jsonurl';
import { mapValues } from 'lodash';
import { JSONValue } from '@medulla-core/utils/json';

@Injectable({
  providedIn: 'root',
})
export class QueryParamsService {
  static encode(queryParams?: Record<string, JSONValue>): Record<string, string> | undefined | null {
    if (queryParams === undefined) {
      return undefined;
    }

    if (queryParams === null) {
      return null;
    }

    return mapValues(queryParams, (value) => JsonURL.stringify(value, { noEmptyComposite: true }));
  }

  static decode(queryParams?: Record<string, string>): Record<string, JSONValue> {
    if (queryParams === undefined) {
      return undefined;
    }

    if (queryParams === null) {
      return null;
    }

    return mapValues(queryParams, (value) => JsonURL.parse(value, { noEmptyComposite: true }));
  }
}

```

I'm using [jsonurl](https://jsonurl.org/) to keep a valid, consistent and human readable json into the URL.

For instance, here is an example of the `queryParams` of a `userSearch` page:

```typescript
queryParams = {
filter: {name:"", surname:""},
page: {offset: 0, count: 100},
}
```

If I try passing this object to the queryParams, without my helper I will get something like this:

`?filter=%5Bobject%20Object%5D&page=%5Bobject%20Object%5D`

Using my helper, the queryParams becomes:
```typescript
queryParams = {
filter: "(name:'',surname:'')",
page: "(offset:0,count:100)"
}
```

Which is url friendly and gives me this clean url

`?filter=(name:'',surname:'')&page=(offset:0,count:100)`

To get back the object, I'm simply sending the parsed queryParams into my helper.

The goal of this ticket is to automate this process without using my helper.

Side note: As queryParams are related to routes, perhaps it be consistent to allow registering an optional parser / serialize to the routes themselves. We could then even register a reviver for the activeroute's query params.