Demystifying the OpenAPI Document

tagged with .NET 6 .NET 7 .NET Core API Open API Swagger

A deep dive into OpenAPI document

In her fantastic article which can be read here, Rachel told us all about making our API documentation friendlier and better. But ever wondered what is the magic that drives the nice SwaggerUI behind the scenes? Let me introduce you to the OpenAPI document with my article.

OpenAPI Specification (OAS) is a standard that defines a format to describe and document RESTful APIs. While HATEOAS (Hypermedia as the Engine of Application State), says you don't need documentation for your RESTful APIs, you have to implement me, OpenAPI goes the exact opposite way - let us document RESTful APIs and make them discoverable and descriptive enough that the consumers of the API can focus on communicating with the API rather than worrying about the server implementation of the API. The specification helps in describing the endpoints and available operations on each endpoint, input and output parameters for each operation, authentication methods and much more.

An OpenAPI document is a document or a set of documents that describes an API. The document is usually written in JSON or YAML. When using Swashbuckle, the information about the API - routes, controllers and actions are gathered for you from the annotations and the middleware exposes it as a JSON endpoint. This JSON file (the OpenAPI document) confirms to the OpenAPI spec and is the heart and soul of your API documentation. The file can usually be found at swagger/v1/swagger.json and the Swashbuckle SwaggerUI tooling reads and emits the nice interactive UI which helps us test our APIs. But can we manage the file ourselves, handcraft it and gain more out of it? Let us find out!

Before I start creating the OpenAPI file, let us talk about my API for the day. My API is a festive API which returns a list of Christmas messages - translations of "Merry Christmas" in various languages. The API model looks as shown below. I am using an in-memory list registered into my DI container as a singleton dependency as my data. My API has a single endpoint /advent with 3 operations - two GET operations to retrieve messages or to retrieve a single message by it's id, and a POST operation to add a new message to the list.


public class AdventMessage
{
    public int Id { get; set; }

    public string? Message { get; set; }
}

The AdventMessage Model


public class FestiveAdventData
{
    public List<AdventMessage> AdventMessages = new()
    {
        new AdventMessage() { Id = 1, Message = "Veselé Vánoce!" },
        new AdventMessage() { Id = 2, Message = "Glædelig Jul!" },
        new AdventMessage() { Id = 3, Message = "¡Feliz navidad!" },
        new AdventMessage() { Id = 4, Message = "Geseënde Kersfees!" },
        new AdventMessage() { Id = 5, Message = "krisamas kee badhaee!" },
        new AdventMessage() { Id = 6, Message = "Nollaig Shona!" },
        new AdventMessage() { Id = 7, Message = "Christumas aashamsakal" },
        new AdventMessage() { Id = 8, Message = "Joyeux noël!" },
        new AdventMessage() { Id = 9, Message = "God Jul!" },
        new AdventMessage() { Id = 10, Message = "Mutlu Noeller!" },
        new AdventMessage() { Id = 11, Message = "Maligayang Pasko!" },
        new AdventMessage() { Id = 12, Message = "Frohe Weihnachten!" },
        new AdventMessage() { Id = 13, Message = "Merīkurisumasu!" },
        new AdventMessage() { Id = 14, Message = "Vrolijk kerstfeest!" },
        new AdventMessage() { Id = 15, Message = "Glædelig jul!" },
        new AdventMessage() { Id = 16, Message = "Craciun Fericit" }
    };
}

Advent Messages data


[ApiController]
[Route("[controller]")]
public class AdventController : ControllerBase
{
    private readonly FestiveAdventData _festiveAdventData;

    private readonly ILogger<AdventController> _logger;

    public AdventController(ILogger<AdventController> logger,FestiveAdventData festiveAdventData)
    {
        _logger = logger;
        _festiveAdventData = festiveAdventData;
    }

    [HttpGet]
    public ActionResult<List<AdventMessage>> GetMessages()
    {
        return _festiveAdventData.AdventMessages;
    }

    [HttpGet("{id}")]
    public ActionResult<AdventMessage> GetMessage(int id)
    {
        return _festiveAdventData.AdventMessages.Find(a => a.Id  == id);
    }

    [HttpPost]
    public  ActionResult<AdventMessage> AddMessage(AdventMessage adventMessage)
    {
        _festiveAdventData.AdventMessages.Add(adventMessage);

        return CreatedAtAction(nameof(GetMessage), new { id = adventMessage.Id }, adventMessage);
    }
}

The controller

OpenAPI recommends using YAML over JSON to create OpenAPI documents as YAML is slightly smaller in size. I am not keen on YAML but I am going to give it a try here.

It is recommended to name the file as openapi.yaml.

The OpenAPI Object

The root object of an OpenAPI document is the OpenAPI object. This object has a few fields as documented here. We will discuss some of the fixed fields and come up with the OpenAPI document for my sample api.


openapi: 3.0.3

The OpenAPI Object

The info field

The info field provides metadata about the api. The field type is Info object and is made up of further fixed fields as shown in the OpenAPI document below.

The metadata presented by the Info object is presented to the end user using the SwaggerUI as well.


# The info object
info:
  # The title of the API
  title: Advent Message API
  # A good description for the API
  description: The festive advent API sample I wrote for [24 Days in Umbraco 2022](https://24days.in/umbraco-cms/2022/). This the 11th year of the advent calendar.
  # Version of the OpenAPI document (different from the OpenAPI Spec Version or the API Implementation Version)
  version: 1.0.0
  # Contact information for the API (Contact Object Type)
  contact:
    name: Poornima Nayar # Name of the contact/organisation
    email: hello@email.com # Email address of the contact
    url: https://www.poornimanayar.co.uk # Url pointing to contact information
  # License information for the API (License Object Type)
  license:
    # The license name for the API
    name: Apache 2.0
    # Url to the license
    url: http://www.apache.org/licenses/LICENSE-2.0.html

The Info Object

The ExternalDocs object

You can also add external documentation for the api using the ExternalDocs object.


externalDocs:
  # Url for the external documentation
  url: https://archive.24days.in/umbraco-cms/about/
  # Description of the external documentation
  description: The external documentation of the api resides here...

The ExternalDocs object

The Tags Object

Tags help in logically grouping operations together. You can centrally define tags using this object and apply one or more of the tags to operations. This is purely from a documentation perspective and has nothing to do with the implementation of the API. For example if you have a customers tag and orders tag you can tag an operation to get all orders by a customer with both customers and orders tag and the operation will show up in both customers and orders tags. The order of tags drives the order of display of operations.

The tags are picked up by various documentation tools in different ways, for e.g. SwaggerUI groups operations by tags and displays them.


# Tags that can be applied to various operations, an array of tags expected
tags:
  # name of the tag
  - name: Advent
    # description of the tag
    description: Advent Operations
    # external documentation for this tag, same as the external documentation object
    externalDocs:
      url: https://archive.24days.in/umbraco-cms/about/
      description: Learn more about the tag

The Tags object

The Server Object

The server object specifies the API server. Multiple servers can be configured. A single server object has a url and a description. The API Paths are built relative to the server url.

Please note that I have not added this to my final YAML document.


# The server object
servers:
  # A single server object specifying the dev url and description
  - url: https://dev.url
    description: Dev Server
  # A single server object specifying the test url and description
  - url: https://test.url
    description: Test Server
  # A single server object specifying the localhost url and description
  - url: https://localhost:7112
    description: Localhost

The Server Object

The Components Object

The Components object helps define reusable objects for different aspects of the document. You can define reusable objects for responses, parameters, request body, schema (data models) and more. In my example, I am using it to create reusable schema (data models) - The AdventMessage which serves as the return type of my GET operations and also the request body (I am resorting to some lazy effort, should be a DTO in real cases!).

The Components is defined in a separate section. The schemas are defined against the schema field. The schema field is made up of Schema object items. Each object has a name, a type - object in our case as we are defining a schema. Our object has properties that can be defined using the properties field, each property having a name and a type.


# The components section
components:
  # reusable schemas
  schemas:
    # The reusable AdventMessage schema
    AdventMessage:
      # Type of schema
      type: object
      # Properties in the object
      properties:
        # The Id field
        Id:
          # Type of the Id field
          type: integer
        # The Message field
        Message:
          # Type of the Message field
          type: string

The Components Section

Reusables created using components can then be used elsewhere using the $ref keyword, setting the value to #/components/schemas/{schema_name}.


 $ref: "#/components/schemas/AdventMessage"

Using a component

The Paths Object

The Paths object holds relative paths to the individual endpoints and their operations. The path is appended to the URL from the Server Object to construct the full url. This is the most crucial part of the documentation.


# The Paths object to list endpoints and operations on the endpoints
paths:
  # The advent endpoint
  /advent:  
  # The another endpoint
  /another:

The Paths object

You can specify multiple endpoints, each having its own set of operations.

The Path Item Object

Each field in a Paths object is a Path Item object that describes the operations available on the endpoint. Each Path Item object contains a set of fields as shown here but I am only going to cover the operations. The allowed operations are GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD and TRACE.


# The Paths object to list endpoints and operations on the endpoints
paths:
  # The advent endpoint
  /advent:
    # The GET operation
    get:

    # The POST operation
    post:

    # The PUT operation
    put:

Each available operation on the endpoint is specified as an Operation object.

The Operation object

This object describes the operation on an endpoint. An API has various paths or endpoints described by the Paths object. Each field in a Paths object is a Path Item object and each available operation on an endpoint is specified using an Operation object.

In our case a GET operation (to retrieve all messages) and a POST operation is available on the path /advent.

The GET operation can be specified as shown below in YAML. Note the use of the tags in the operation


# The Paths object to list endpoints and operations on the endpoints
paths:
  # The advent endpoint
  /advent:
    # The GET operation on the path /advent
    get:
      # Tag with Advent tag discussed above, putting it in the logical group Advent
      tags:
        - Advent
      # Summary of the operation
      summary: Get advent messages
      # Description of the operation
      description: Get a list of advent messages from the in-memory list
      # An operation id for the operation, typically the method name, used to uniquely identify the operation
      operationId: GetMessages
      # The responses
      responses:
      # The 200 OK response definition
      "200":
        ...

To define the responses we use the Responses Object, that helps specify the expected responses for an operation. Responses are mapped by status code, so for each expected status code from the operation, a response definition can be specified. In my example, I have a response definition for 200 OK.

The Responses object is made up of the content field that specifies the return type, and the Media Type object that defines the mapping for an individual return media type. I have defined a single media type application/json using the Media Type object.


responses:
# The 200 OK response definition
"200":
  # Description of the response
  description: List of Advent Messages
  # Potential response payloads, multiple media types can be specified
  content:
  # Details of the JSON response (Media Type Object)
  application/json:
    # Details of the response body
    schema:
    # type of response
    type: array
    # type of items in the response
    items:
      # reference a schema defined globally in the response section for the return type
      $ref: "#/components/schemas/AdventMessage"

The Response object

The schema object defines the data type returned. It can be a primitive (integer, string), object or an array. In our case the response body is an array of JSON data as specified by the type field in the schema object. The type of items in the array is specified by the items field which is expected if the type is set to array. I am using a reference to a global schema for AdventMessage as the type of items in the array.

I can also write the POST operation on the path to add a new advent message in a similar fashion. The only difference here is that there is a requestBody field involved. It is made up of a description and a required field in addition to the Media Type object that we discussed above which forms the content of the request body.


# The request body
requestBody:
  # Description for the request body
  description: The request payload
  # whether the field is required
  required: true
  content:
    # Details of the JSON request payload (Media Type Object)
    application/json:
      schema:
        $ref: "#/components/schemas/AdventMessage"

Putting it all together, the YAML for the path /advent looks as shown below.


# The Paths object to list endpoints and operations on the endpoints
paths:
  # The advent endpoint
  /advent:
    # The GET operation on the path /advent
    get:
      # Tag with Advent tag discussed above, putting it in the logical group Advent
      tags:
        - Advent
      # Summary of the operation
      summary: Get advent messages
      # Description of the operation
      description: Get a list of advent messages from the in-memory list
      # An operation id for the operation, typically the method name, used to uniquely identify the operation
      operationId: GetMessages
      # The responses
      responses:
        # The 200 OK response definition
        "200":
          # Description of the response
          description: List of Advent Messages
          # Potential response payloads, multiple media types can be specified
          content:
            # Details of the JSON response (Media Type Object)
            application/json:
              # Define the return type
              schema:
                # type of response
                type: array
                # type of items in the response
                items:
                  # reference a schema defined globally in the response section for the return type
                  $ref: "#/components/schemas/AdventMessage"
    # The POST operation
    post:
      # Tag with Advent tag discussed above, putting it in the logical group Advent
      tags:
        - Advent
      # Summary of the operation
      summary: Create advent message
      # Description of the operation
      description: Adds a new advent message to the in-memory list
      # An operation id for the operation, typically the method name, used to uniquely identify the operation
      operationId: AddMessage
      # The request body
      requestBody:
        # Description for the request body
        description: The request payload
        # whether the field is required
        required: true
        content:
          # Details of the JSON request payload (Media Type Object)
          application/json:
            schema:
              $ref: "#/components/schemas/AdventMessage"
      # The responses
      responses:
        # The 200 OK response definition
        "201":
          # Description of the response
          description: Added advent message successfully
          content:
            # Details of the JSON response (Media Type Object)
            application/json:
              # Define the return type
              schema:
                # reference a schema defined globally in the response section for the return type
                $ref: "#/components/schemas/AdventMessage"

The advent endpoint in YAML

The GET operation to retrieve a single advent message by its id is on the path /advent/{id} and needs a new path defined. There is a single GET operation on the path which returns a 200 OK response or a 404 Not Found response if the advent message cannot be found. Note the use of the tags again to group the operation together despite the path. But we have a route parameter in our path here. So we need to define a parameter on the operation as shown below.


# Parameters for the operation
parameters:
# name of the parameter
    - name: id
      # A description for the field
      description: The id of the advent message to be retrieved
      # required
      required: true
      # location of parameter. can be path, header, query or cookie
      in: path
      # Details of the paramater type
      schema: 
      # type of parameter
      type: integer  

Specifying parameters using YAML

Parameters are specified using Parameter object. Multiple parameters can be specified. Each Parameter object takes a name field, a description field, the required field denoting whether it is a required field, the in field denoting the location of the parameter and the schema object that defines the type as discussed previously.

Putting it all together it looks as shown below.


/advent/{id}:
  get:
    tags:
      - Advent
    summary: Get a single advent message
    description: Get an advent messages from the in-memory list
    operationId: GetMessage
    # Parameters for the operation
    parameters:
      # name of the parameter
      - name: id
        # A description for the field
        description: The id of the advent message to be retrieved
        # required
        required: true
        # location of parameter. can be path, header, query or cookie
        in: path
        schema:
          # type of parameter
          type: integer
    responses:
      "200":
        description: An Advent Message object
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AdventMessage"
      "404":
        description: Advent message not found

Putting it all together

Now that we have learnt about the various objects in the YAML file, we can put it together and the resulting file is shown below. I have only covered the basic here but it can be made more elaborate.


openapi: 3.1.0
info:
  title: Tic Tac Toe
  description: |
    This API allows writing down marks on a Tic Tac Toe board
    and requesting the state of the board or of individual squares.
  version: 1.0.0
tags:
  - name: Gameplay
paths:
  # Whole board operations
  /board:
    get:
      summary: Get the whole board
      description: Retrieves the current state of the board and the winner.
      tags:
        - Gameplay
      operationId: get-board
      responses:
        "200":
          description: "OK"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/status"

  # Single square operations
  /board/{row}/{column}:
    parameters:
      - $ref: "#/components/parameters/rowParam"
      - $ref: "#/components/parameters/columnParam"
    get:
      summary: Get a single board square
      description: Retrieves the requested square.
      tags:
        - Gameplay
      operationId: get-square
      responses:
        "200":
          description: "OK"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/mark"
        "400":
          description: The provided parameters are incorrect
          content:
            text/html:
              schema:
                $ref: "#/components/schemas/errorMessage"
              example: "Illegal coordinates"
    put:
      summary: Set a single board square
      description: Places a mark on the board and retrieves the whole board and the winner (if any).
      tags:
        - Gameplay
      operationId: put-square
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/mark"
      responses:
        "200":
          description: "OK"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/status"
        "400":
          description: The provided parameters are incorrect
          content:
            text/html:
              schema:
                $ref: "#/components/schemas/errorMessage"
              examples:
                illegalCoordinates:
                  value: "Illegal coordinates."
                notEmpty:
                  value: "Square is not empty."
                invalidMark:
                  value: "Invalid Mark (X or O)."

components:
  parameters:
    rowParam:
      description: Board row (vertical coordinate)
      name: row
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/coordinate"
    columnParam:
      description: Board column (horizontal coordinate)
      name: column
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/coordinate"
  schemas:
    errorMessage:
      type: string
      maxLength: 256
      description: A text message describing an error
    coordinate:
      type: integer
      minimum: 1
      maximum: 3
      example: 1
    mark:
      type: string
      enum: [".", "X", "O"]
      description: Possible values for a board square. `.` means empty square.
      example: "."
    board:
      type: array
      maxItems: 3
      minItems: 3
      items:
        type: array
        maxItems: 3
        minItems: 3
        items:
          $ref: "#/components/schemas/mark"
    winner:
      type: string
      enum: [".", "X", "O"]
      description: Winner of the game. `.` means nobody has won yet.
      example: "."
    status:
      type: object
      properties:
        winner:
          $ref: "#/components/schemas/winner"
        board:
          $ref: "#/components/schemas/board"

Complete YAML file

As I said above I have gone for the recommended YAML version, but all the objects discussed above stays the same for JSON. JSON being a subset of YAML, the two formats are interchangeable. I also can use comments if I using YAML, which I cannot with JSON. I have only covered the minimal objects needed but you can enhance it further using the documentation here.

I know writing such a file by hand is not an easy task. But it is not that complicated either. I used the OpenAPI (Swagger) Editor VS Code Extension. It gives you intellisense, SwaggerUI preview and much more.

If you don't wish to use the extension you can use the Swagger Editor or even the Swagger Hub. While Swagger Editor is Open Source and can be installed and run locally, Swagger Hub is a pro tool and brings all the features of Swagger Editor and more as a cloud product enabling collaboration and sharing as well. The full comparison can be found here.

Why OpenAPI Document???

Why resort to writing YAML when we have Swashbuckle to generate the JSON for us. It's all about choice and what suits your project. With Swashbuckle the file is generated for you from the annotations. But this means that the documentation is quite tightly coupled with your API. For smaller projects it works really well. But for bigger projects this might not be the best way, especially if you have a public API.

With OpenAPI document authored in YAML, you can separate out the documentation from your API and even source control the file. You have the opportunity to create automated documentation, more than what Swagger UI can offer, and even tie it up to your CI-CD pipelines to deploy the API documentation separately from your API.

Gaining more from OpenAPI Document

We have learnt about the OpenAPI document but can we go that extra mile with the documentation? Yes!!! There are various tools other than SwaggerUI that can parse and generate documentation using this file. It can be particularly useful, if you wish to separate your documentation from your API and maintain it separately. Redocly is one such tool. There are other tools like Elements as well but I am going to demo the Redocly tooling.

Redocly and the CLI Tool

Redocly helps you generate beautiful API documentation from an OpenAPI document. It can be themed according to your branding and the documentation can be hosted anywhere. Redocly has an open source CLI tool called the Redocly CLI tool. It can be installed via npm or yarn.

npm i -g redoc-cli

The CLI tool ships with a list of commands, but I am using the build command to build a single, zero-dependency HTML file.

redoc-cli build openapi.yaml

This command generates a file called redoc-static.html for me. I am hosting this on GitHub pages which you can see here

I am using the community version of the tool here which is much simpler and is open source. But the premium option provides a lot more features like better customisation, liting the OAS definitions, theming, code samples and so on. GitHub uses Redoc for their API documentation. The demo here fetaures all the features of Redoc.

I hope you picked up a trick or two from my article this year :-) Happy festive season to all!!!

Thank you Mark :-)

A special thanks to Mark Rendle, who taught us all about Redoc and many such cool tools at his talk at the London .NET User Group in October. The talk really inspired me and helped me to have a go at YAML and Redoc myself and learn a bit more about OpenAPI and Swagger in general. 

An aside about the first edition of Umbraco Community Day :-)

Santa is also leaving you a New Year's gift. Umbraco Community Day is happening soon! Make sure you register and attend the virtual event


Poornima Nayar
Poornima Nayar

Tech Tip: JetBrains Rider

Check it out here