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:

      username:
        type: string
        pattern: "[a-z0-9]{8,64}"
        minLength: 8
        maxLength: 64

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:

      dateOfBirth:
        type: string
        format: date
      lastTimeOnline:
        type: string
        format: date-time

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):

      code:
        type: string
        enum:
          - DBERR
          - NTERR
          - 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:

      avatarBase64PNG:
        type: string
        format: byte

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:

      lastTimeOnline:
        type: string
        format: date-time
        readOnly: true

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:

  PagedPersons:
    allOf:
      - $ref: "#/definitions/Persons"
      - $ref: "#/definitions/Paging"

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:

{ 
  "key1": "value1",
  "key2": "value2"
}

A string/object JSON map:

{ 
  "key1": {"complexValue1": "value1"},
  "key2": {"complexValue2": "value2"}
}

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:

{
"username": "apihandyman",
"spokenLanguage": { 
    "en": "english",
    "fr": "French"
  }
}

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.

By continuing to use this web site you agree with the API Handyman website privacy policy (effective date , June 28, 2020).