OpenAPI Tips - Query Parameters & Serialization
Tags: openapi
Published: 2023-09-17 15:54:28

## The Problem

The OpenAPI spec is best known for descriptions of RESTful APIs, but it’s designed to be capable of describing any HTTP API whether that be REST or something more akin to RPC based calls.

That leads to the spec having a lot of flexibility baked-in: there's a lot of ways to achieve the exact same result that are equally valid in the eyes of the spec. Because of this, [the OpenAPI documentation](https://spec.openapis.org/oas/v3.1.0#operation-object) is very ambiguous when it comes to how you should define your API.

That’s why we’re taking the time to eliminate some of the most common ambiguities that you’ll encounter when you build your OpenAPI schema. In this case we’ll be taking a look at **how to serialize query parameters in your OpenAPI 3.0.X schema.**

## Recommended Practices

The OpenAPI spec grants quite a bit of flexibility in defining query parameters for any operation. There are many serialization options and defaults, therefore it’s advisable you **define query parameters as strictly as possible** in your schema. This will **improve your API documentation** thereby reducing ambiguity for end-users. In addition, explicit definitions will **aid any OpenAPI tooling you may be using to produce artifacts**, such as client SDKs.

As an API developer, strict definitions will also give you a more intuitive understanding of each operation’s intended behavior as you iterate on your OpenAPI schema. Concretely, we recommend that you:

- Describe your query parameters as explicitly as possible by using OpenAPI defined formats.
- Use additional validation attributes as much as possible: mark properties as required, allowReserved, allowEmptyValue, and indicate when fields are nullable.

It’s also important to note that OpenAPI considers a unique operation as a combination of a path and HTTP method, so it is not possible to have multiple operations that only differ by query parameters. In this case, it’s advisable to use unique paths as shown below:

```yaml
GET /users/findByName?name=anuraag
GET /users/findByRole?role=developer
```

## Query Parameters

Query parameters are criteria which appear at the end of a request URL demarcated by a question mark (?), with different key=value pairs usually separated by ampersands (&). They may be required or optional, and can be specified in an OpenAPI schema by specifying **in: query**. Consider the following operation for an event catalog:

```yaml
GET /events?offset=100&limit=50
```

Query parameters could be defined in the schema as follows:

```yaml
parameters:
  - in: query
    name: offset
    schema:
      type: integer
    description: The number of items to skip before starting to collect the result set
  - in: query
      name: limit
      schema:
        type: integer
      description: The numbers of items to return
```

When you’re working with query parameters, it’s important to understand serialization. Let’s explore what serialization is, and the variety of ways the OpenAPI specification supports serialization of query parameters.

## Serialization

Serialization is responsible for transforming data into a format that can be used in transit and reconstructed later. For query parameters specifically, this format is the query string for requests of that operation. The serialization method allows us to define this through the use of the following keywords:

- **style** – defines how multiple values are delimited. Possible styles depend on the parameter location – [path](https://swagger.io/docs/specification/serialization/#path), [query](https://swagger.io/docs/specification/serialization/#query), [header](https://swagger.io/docs/specification/serialization/#header) or [cookie](https://swagger.io/docs/specification/serialization/#cookie).
- **explode** – (true/false) specifies whether arrays and objects should generate separate parameters for each array item or object property.

OpenAPI supports serialization of arrays and objects in all operation parameters (path, query, header, cookie). The serialization rules are based on a subset of URI template patterns defined by [RFC 6570](https://tools.ietf.org/html/rfc6570).

From the OpenAPI Swagger documentation, query parameters support the following style values:

- **form** (default) - ampersand-separated values, also known as form-style query expansion. Corresponds to the {?param_name} URI template.
- **spaceDelimited** – space-separated array values. Has effect only for non-exploded arrays (explode: false), that is, the space separates the array values if the array is a single parameter, as in arr=a b c.
- **pipeDelimited** – pipeline-separated array values. Has effect only for non-exploded arrays (explode: false), that is, the pipe separates the array values if the array is a single parameter, as in arr=a|b|c.
- **deepObject** – simple non-nested objects are serialized as paramName[prop1]=value1&paramName[prop2]=value2&.... The behavior for nested objects and arrays is undefined.

The **default serialization method** is **style: form and explode: true**. As shown in the GET /events call above, the “?offset=100&limit=50” query string is created with this default serialization when the schema has no references to style or explode. However, we recommend explicitly setting these values, even in the default case, to reap the benefits discussed in “Recommended Practices” above.

Given the path, **/events**, with a query parameter, **id**, the query string would be serialized as follows with the above options:

| style | explode | Primitive value: id=3 | Array id=[1,2,3] | Object id = {"type": "music", "location": "CA"} |
| --- | --- | --- | --- | --- |
| form* | true* | /events?id=3 | /events?id=1&id=2&id=3 | /events?type=music&location=CA |
| form | false | /events?id=3 | /events?id=1,2,3 | /events?id=type,music,location,CA |
| spaceDelimited | true | n/a | /events?id=1&id=2&id=3 | n/a |
| spaceDelimited | false | n/a | /events?id=1%202%203 | n/a |
| pipeDelimited | true | n/a | /events?id=1&id=2&id=3 | n/a |
| pipeDelimited | false | n/a | /events?id=1|2|3 | n/a |
| deepObject | true | n/a | n/a | /events?id[type]=music&id[location]=CA |
_*Default serialization method_

style and explode cover the most common serialization methods, but not all. For more complex scenarios (ex. a JSON-formatted object in the query string), you can use the **content** keyword and specify the media type that defines the serialization format. The example schema below does exactly that:

```yaml
parameters:
  - in: query
    name: filter
    # Wrap 'schema' into 'content.'
    content:
      application/json: #media type indicates how to serialize/deserialize parameter content
        schema:
          type: object
          properties:
            type:
              type: string
            location:
              type: string
```

## Additional Attributes

Query parameters can be specified with quite a few additional attributes to further determine their **serialization, optionality, and nullability**.

### AllowReserved

This is the only additional attribute which is specific to query parameters. From the OpenAPI Swagger documentation: The **allowReserved** keyword specifies whether the reserved characters, defined as :**/?#[]@!$&'()*+,;=** by [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986), are allowed to be sent as they are as query parameter values or should be percent-encoded. By default, allowReserved is false, and reserved characters are percent-encoded. For example, / is encoded as %2F (or %2f), so that the parameter value, events/event_info.txt, will be sent as events%2Fevent_info.txt. To preserve the / as is, **allowReserved** would have to be set to true as shown below:

```yaml
parameters:
  - in: query
    name: path
    required: true
    schema:
      type: string
    allowReserved: true
```

### Required

By default, OpenAPI treats all request parameters as optional. You can add **required: true** to mark a parameter as required.

### Default

Use the **default** keyword in the parameter schema to specify the default value for an optional parameter. The default value is the one that the server uses if the client does not supply the parameter value in the request. The value type must be the same as the parameter’s data type.

Consider a simple example, where default used with paging parameters allows these 2 calls from the client to be equivalent:

```yaml
GET /events
GET /events?offset=0&limit=100
```

This would be specified in the schema like so:

```yaml
- in: query
  name: offset
  schema:
    type: integer
    default: 0
  description: The number of items to skip before starting to collect the result set
- in: query
  name: limit
  schema:
    type: integer
    default: 100
  description: The numbers of items to return
```

The **default keyword should not be used with required values**. If a parameter is required, the client must always send it and therefore override the default.

### Enum and Constant Parameters

You can restrict a parameter to a fixed set of values by adding the enum to the parameter’s schema. The **enum** values must be the same type as the parameter data type.

A constant parameter can then be defined as a required parameter with only one possible value as shown below:

```yaml
parameters:
  - in: query
    name: eventName
    required: true
    schema:
      type: string
      enum:
        - coachella
```

The enum property specifies possible values, and in this example, only one value can be used.

It’s important to note a **constant parameter is not the same as the default parameter value**. A constant parameter is always sent by the client, whereas the default value is something that the server uses if the parameter is not sent by the client.

### Empty-Valued and Nullable

Query string parameters may only have a name and no value, like so:

```yaml
GET /events?metadata
```

Use allowEmptyValue to describe such parameters:

```yaml
parameters:
  - in: query
    name: metadata
    required: true
    schema:
      type: boolean
    allowEmptyValue: true
```

The OpenAPI spec also supports **nullable** in schemas, allowing operation parameters to have the null value when **nullable: true**. This simply means the parameter value can be null, and is **not the same as an empty-valued or optional parameter**.

### Deprecated

Use **deprecated: true** to mark a parameter as deprecated.

### Common Parameters Across Methods in Same Path

Parameters may be defined once to be used in multiple methods/paths in an OpenAPI schema. Parameters shared by all operations of a path can be defined on the path level instead of the operation level. These path-level parameters are inherited by all operations (GET/PUT/PATCH/DELETE) of that path. An example is shown below(manipulating the same resource in different ways is a good use case here):

```yaml
paths:
  /events:
    parameters:
      - in: query
        name: filter
        content:
          application/json:
            schema:
              type: object
                properties:
                  type:
                    type: string
                  location:
                    type: string
  get:
    summary: Gets an event by type and location
    ...
  patch:
    summary: Updates the newest existing event with the specified type and location
    ...
  delete:
```

Any extra parameters defined at the operation level are used **in addition** to path-level parameters. Specific path-level parameters **may also be overridden on the operation level, but cannot be removed**.

### Common Parameters Across Multiple Paths

Parameters can also be shared across multiple paths. Pagination is a good candidate for this:

```yaml
components:
  parameters:
    offsetParam:
      - in: query
        name: offset
        required: false
        schema:
          type: integer
        minimum: 0
        default: 0
        description: The number of items to skip before collecting the result set.
    limitParam:
      - in: query
        name: limit
        required: false
        schema:
          type: integer
        minimum: 1
        default: 10
        description: The number of items to return.
paths:
  /events:
    get:
      summary: Gets a list of events
      parameters:
        - $ref: '#/components/parameters/offsetParam'
        - $ref: '#/components/parameters/limitParam'
      responses:
        '200':
          description: OK
  /locations:
    get:
      summary: Gets a list of locations
      parameters:
        - $ref: '#/components/parameters/offsetParam'
        - $ref: '#/components/parameters/limitParam'
      responses:
        '200':
          description: OK
    ...
```

Note the above parameters defined in components are simply global definitions that can be handily referenced. They are not necessarily applied to all methods of an operation.