Writing OpenAPI (Swagger) Specification Tutorial Series - Part 4
Advanced Data
By Arnaud Lauret, April 17, 2016
After learning how to simplify specification files, let’s start delving into the OpenAPI specification’s and discover how to describe a high accuracy API’s data model.
Writing OpenAPI (Swagger) Specification Tutorial Series
This tutorial teaches everything about the OpenAPI 2.0 Specification (fka. as Swagger), most of what you’ll read here can still be applied on version 3.
If you’re a bit lost in the specification (version 2 or 3), take a look at the OpenAPI Map:
In this fourth part you will discover all the tips and tricks you can use to describe properties and definitions to describe an accurate API’s data model.
Tailor made properties
Primitive data types in the Swagger Specification are based on the types supported by the JSON-Schema Draft 4. Models are described using the Schema Object which is a subset of JSON Schema Draft 4. OpenAPI Specification Data Types
Using the JSON Schema Draft 4, the OpenAPI Specification allows to define every aspects of any type of property.
Strings length and pattern
When defining a string property, we can specify its length range and its pattern:
Property | Type | Description |
---|---|---|
minLength | number | String’s minimum length |
maxLength | number | String’s maximum length |
pattern | string | Regular expression (if you’re not a regex expert, you should try Regex 101) |
The username in the Person definition is a string which length is between 8 and 64 and composed of lower case alphanumeric characters:
Dates and times
Date and time are handled with string properties conforming to RFC 3339, all you need to do is to use the appropriate format:
Format | Property contains | Property’s value example |
---|---|---|
date | ISO8601 full-date | 2016-04-01 |
date-time | ISO8601 date-time | 2016-04-16T16:06:05Z |
In the Person definition, dateOfBirth is a date and lastTimeOnline is a timestamp:
You should read the 5 laws of API dates and times by Jason Harmon to learn how to handle date and time with an API.
Numbers type and range
When defining a number property, we can specify if this property is an integer, a long, a float or a double by using the appropriate type and format combination.
Value | Type | Format |
---|---|---|
integer | integer | int32 |
long | integer | int64 |
float | number | float |
double | number | double |
And just like string, we can define additional properties like the value range and also indicate if the value is a multiple of something:
Property | Type | Description |
---|---|---|
minimum | number | Minimum value |
maximum | number | Maximum value |
exclusiveMinimum | boolean | Value must be > minimum |
exclusiveMaximum | boolean | Value must be < maximum |
multipleOf | number | Value is a multiple of multipleOf |
The pageSize parameter is an integer > 0 and <= 100 and a multiple of 10:
pageSize:
name: pageSize
in: query
description: Number of persons returned
type: integer
format: int32
minimum: 0
exclusiveMinimum: true
maximum: 100
exclusiveMaximum: false
multipleOf: 10
The maxPrice property of CollectingItem definition is a double value > 0 and <= 10000:
maxPrice:
type: number
format: double
minimum: 0
maximum: 10000
exclusiveMinimum: true
exclusiveMaximum: false
Enumerations
On each property we can define a set of accepted value with the enum property.
The property code of definition Error can take only three value (DBERR, NTERR and UNERR):
Arrays size and uniqueness
Arrays size and uniqueness are defined by these properties:
Property | Type | Description |
---|---|---|
minItems | number | Minimum number of items in the array |
maxItem | number | Maximum number of items in the array |
uniqueItems | boolean | Indicate if all array’s elements are unique |
The Person definition contains a property items which is an array of Person. This array contain only unique elements and can have between 10 and 100 items:
Persons:
properties:
items:
type: array
minItems: 10
maxItems: 100
uniqueItems: true
items:
$ref: "#/definitions/Person"
Binary data
Binary data can be handled with string properties using the appropriate format:
Format | Property contains |
---|---|
byte | Base64 encoded characters |
binary | Any sequence of octets |
The property avatarBase64PNG of Person definition is a base64 encoded PNG image:
Advanced definition modeling
Using same definitions on read and write operations
It’s not unusual that reading a resource returns more than the data needed when creating or updating it. To solve this problem you’ll probably end having two different definitions, one for creating or updating and the other for reading. Fortunately, it can be avoided.
When describing a property in a definition, we can set a readOnly property to true to explain that this property may be sent in a response and must not be sent in a request.
In the example below, the lastTimeOnline property in the Person definition does not have sense when creating an Person. With readOnly set to true on this property, we can use the same Person definition in both post /persons and get /persons/{username}, lastTimeOnline will only be of interest when using get:
Combining multiple definitions to ensure consistency
When designing an API, it is highly recommended to propose a consistent design. You can for example decide that paged collection data should always be accompanied on the root level by the same data explaining the current paging status (totalItems, totalPage, pageSize, currentPage).
A first option would be to define this attribute on every single collection:
PagedPersonsV1:
properties:
items:
type: array
items:
$ref: "#/definitions/Person"
totalItems:
type: integer
totalPages:
type: integer
pageSize:
type: integer
currentPage:
type: integer
Having to describe again and again, endlessly the same properties on each collection is not only boring but also dangerous: you can forget some properties or misspelled them. And what will happen when you’ll want to add a new information on paging? You’ll have to update every single collection.
A better option would be to define a Paging model and then use it in every collection:
PagedPersonsV2:
properties:
items:
type: array
items:
$ref: "#/definitions/Person"
paging:
$ref: "#/definitions/Paging"
Paging:
properties:
totalItems:
type: integer
totalPages:
type: integer
pageSize:
type: integer
currentPage:
type: integer
But the paging attributes are not on the root level anymore. The allOf JSON Schema v4 property can provide an elegant and simple solution to this problem:
The allOf property allow to create a new definition composed of all referenced definitions attributes. It also functions perfectly with inline definitions:
PagedCollectingItems:
allOf:
- properties:
items:
type: array
minItems: 10
maxItems: 100
uniqueItems: true
items:
$ref: "#/definitions/CollectingItem"
- $ref: "#/definitions/Paging"
Create a hierarchy between definitions to implement inheritance (highly experimentatl)
As stated in the OpenAPI Specification, composition do not imply hierarchy. The use of discriminator indicate the property used to know which is the type of the sub-definition or sub-class (this property MUST be in the required list).
Here we define a CollectingItem super-definition, which is subclassed using the allOf property. The consumer will determine which sub-definition used by scanning the itemType property.
CollectingItem:
discriminator: itemType
required:
- itemType
properties:
itemType:
type: string
enum:
- Vinyl
- VHS
imageId:
type: string
maxPrice:
type: number
format: double
minimum: 0
maximum: 10000
exclusiveMinimum: true
exclusiveMaximum: false
Vinyl:
allOf:
- $ref: "#/definitions/CollectingItem"
- required:
- albumName
- artist
properties:
albumName:
type: string
artist:
type: string
VHS:
allOf:
- $ref: "#/definitions/CollectingItem"
- required:
- movieTitle
properties:
movieTitle:
type: string
This is highly experimental as the content of discriminator field is not clear in the specification’s current version (issue 403) and as far as I know it is not supported by any tool using OpenAPI specification.
Maps, Hashmap, Associative array
A map is structure that can map key to value Hash table on Wikipedia
A string/string JSON map:
A string/object JSON map:
In an OpenAPI specification the key is always a string and do not need to be defined (if the key is an integer, it will be considered as a string). The value’s type is defined within the property: additionalProperties.
String to String Hashmap:
If you want to have a string to string map property in the Person definition explaining which languages this person speaks, the resulting data would look like this:
Defining the spokenLanguage property in the Person definition is done this way:
definitions:
Person:
required:
- username
properties:
firstName:
type: string
lastName:
type: string
username:
type: string
pattern: "[a-z0-9]{8,64}"
minLength: 8
maxLength: 64
dateOfBirth:
type: string
format: date
lastTimeOnline:
type: string
format: date-time
readOnly: true
avatarBase64PNG:
type: string
format: byte
spokenLanguages:
$ref: "#/definitions/SpokenLanguages"
SpokenLanguages:
additionalProperties:
type: string
String to Object Map
If you want to have a string to object map property in the Error definition to provide a multilingual long and short error message, the resulting data would look like this:
{
"code": "UNERR",
"message": {
"en": {
"shortMessage":"Error",
"longMessage":"Error. Sorry for the inconvenience."
},
"fr": {
"shortMessage":"Erreur",
"longMessage":"Erreur. Désolé pour le dérangement."
}
}
}
Defining the message property in the Error definition is done this way:
ErrorMessage:
properties:
longMessage:
type: string
shortMessage:
type: string
MultilingualErrorMessage:
additionalProperties:
$ref: "#/definitions/ErrorMessage"
Error:
required:
- code
- message
properties:
code:
type: string
enum:
- DBERR
- NTERR
- UNERR
message:
$ref: "#/definitions/MultilingualErrorMessage"
Hashmap with default value(s)
And finally, if you want to add a default language multilingual error message in your map (i.e. adding a default value in the map).
The returned structure do not differs from the precedent example:
{
"code": "UNERR",
"message": {
"defaultLanguage": {
"shortMessage":"Error",
"longMessage":"Error. Sorry for the inconvenience."
},
"en": {
"shortMessage":"Error",
"longMessage":"Error. Sorry for the inconvenience."
},
"fr": {
"shortMessage":"Erreur",
"longMessage":"Erreur. Désolé pour le dérangement."
}
}
}
But in the OpenAPI Specification, we add a defaultLanguage property to the MultilingualErrorMessage definition to the explicitly declare this value in the map:
ErrorMessage:
properties:
longMessage:
type: string
shortMessage:
type: string
MultilingualErrorMessage:
additionalProperties:
$ref: "#/definitions/ErrorMessage"
properties:
defaultLanguage:
$ref: "#/definitions/ErrorMessage"
Error:
required:
- code
- message
properties:
code:
type: string
enum:
- DBERR
- NTERR
- UNERR
message:
$ref: "#/definitions/MultilingualErrorMessage"
You can define as many as “default” values as you want. This also can be used for string to string map.
Inspired by this stackoverflow question answered by Ron Ratovsky
Conclusion
You now have mastered the art of defining an accurate data model with the OpenAPI Specification. Be aware that even if the OpenAPI Specification defines all this possibilities, some may not be supported/used by every tool working with OpenAPI specification files. But at least your API data model’s description will be highly accurate. In the next post we’ll continue our delving in the specification and learn all tips and tricks to describe the input and outputs of an API.