OpenAPI Tips
OpenAPI Tips - Query Parameters & Serialization
Anuraag Nalluri
December 20, 2022
The Problem
The OpenAPI spec is best known for descriptions of RESTful APIs, but it'is 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 (opens in a new tab) 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 operatio'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:
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:
GET /events?offset=100&limit=50
Query parameters could be defined in the schema as follows:
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 (opens in a new tab), query (opens in a new tab), header (opens in a new tab) or cookie (opens in a new tab).
- 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 (opens in a new tab).
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¶mName[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 |
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:
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 (opens in a new tab), 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:
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:
GET /events
GET /events?offset=0&limit=100
This would be specified in the schema like so:
- 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:
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:
GET /events?metadata
Use allowEmptyValue to describe such parameters:
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):
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:
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.