Swagger: live it to the fullest

by Julien Kirch, March 1, 2016

Introducing Swagger

Swagger is a technical description of a REST API which is readable yet powerful. You can use it to define precisely your services: what parameters should be used, return codes and types, characteristics of each fields, etc.

Example:

"/api/source/{source_id}/element": {
  "get": {
    "summary": "Get API elements for a source, ordered by creation date",
    "parameters": [
      {
        "name": "source_id",
        "in": "path",
        "required": true,
        "type": "integer",
        "description": "The unique id"
      },
      {
        "name": "page",
        "in": "query",
        "required": false,
        "type": "integer",
        "description": "The page index",
        "default": 1,
        "example": 100,
        "minimum": 1,
        "exclusiveMinimum": false
      },
      {
        "name": "per_page",
        "in": "query",
        "required": false,
        "type": "integer",
        "description": "Number of items per page",
        "default": 100,
        "example": 100,
        "minimum": 1,
        "maximum": 100,
        "exclusiveMinimum": false,
        "exclusiveMaximum": false
      }
    ],
    "responses": {
      "200": {
        "schema": {
          "$ref": "#/definitions/Elements"
        }
      },
      "404": {
        "schema": {
          "$ref": "#/definitions/Error"
        },
        "description": "If the API source is not found"
      }
    }
  }
}
"Element": {
  "type": "object",
  "properties": {
    "id": {
      "description": "The unique element id",
      "type": "integer"
    },
    "source": {
      "description": "The source id",
      "type": "integer"
    },
    "created_at": {
      "description": "The creation date",
      "type": "dateTime"
    },
    "timestamp": {
      "description": "The element timestamp",
      "type": "dateTime"
    }
  },
  "required": [
    "id",
    "source",
    "created_at"
  ]
}

What is mainly viewed as a documentation for the API consumers is actually a technical specification of your API, and this has many consequences on what you should do with it.

The only thing worse than no specification is a wrong specification

If you want your specification to be useful you need it to be right. In this case, it means the swagger content should be coherent with your code.

The best way to do it is to make your services unit tests swagger-aware:

  • Read your swagger info at the beginning of your tests.

  • Each time you call a service in a test, validate that the parameters and results match the swagger’s content.

  • If it doesn’t, fail the test.

Example:

petstore.rb
class Petstore < Sinatra::Base

  type 'Pet', {
    :required => [:id, :name],
    :properties => {
      :id => {
        :type => Integer,
        :format => 'int64',
      },
      :name => {
        :type => String,
        :example => 'doggie',
        :description => 'The pet name',
        :maxLength => 2048,
      }
    }
  }

  endpoint_summary 'Finds a pet by its id'
  endpoint_response 200, 'Pet', 'Standard response'
  endpoint_response 404, 'Error', 'Pet not found'
  endpoint_parameter :id, 'The pet id', :path, true, Integer, { example => 1234, }
  endpoint_path '/pet/{id}'
  get %r{/pet/(\d+)} do |id|
    content_type :json
    # The name is too long compared to the swagger declaration
    {:id => 1, :name => ('A' * 2049)}
  end
end
test_petstore.rb
# Patch the app to do the validation
class Petstore

  # This will validate the data against swagger
  set :swagger_validator, SwaggerValidator.new('swagger.json')

  # Patch get method to add swagger validation
  # Pseudo code, sinatra doesn't work this way
  def get(path, opts = {}, &block)
    result = super
    settings.swagger_validator.check_result(path, options, result)
    result
  end

class TestPetstore < Minitest::Test

  before do
    @swagger = read_swagger_configuration
  end

  it 'shoulen\'t have too long name' do
    get("/api/api/source/1/element?per_page=1")
    # you should get an error from the swagger validator
    # because the name is too long in the result
  end

end

There’s existing tool for several frameworks doing it for you; and if it doesn’t you can hack one in a few hours (I’ve done it myself).

The generated swagger case

If you use a statically-typed language like java that use annotation-like metadata to define your REST endpoints, you probably generate the swagger content from your code instead of writing it.

As the swagger is generated from your code, you may think you don’t need this kind of tests because the compiler and/or your framework validate everything for you.

But I have a bad news for you, there’s two reasons why you still should do it:

All metadata are not checked

All the metadata information is not checked, for example if you specify the minimum number for the value of the max length of a string, it won’t be checked automatically.

Don’t break the API

When you publish an API the last thing you want is to break it unknowingly: you should version your API and carefully manage the incompatible changes.

When the swagger is manually written, validating your services against it ensure that you don’t introduce any change in your API. But if you generate the swagger, a bad "rename attribute" in your code can break your API.

Even if code reviews should prevent this kind of things, it’s better to rely on an automatic validation:

  • Each time you publish a new API version, export the generated swagger content, and store it with your code and don’t touch it.

  • Apply the testing approach described before with the archived swagger.

  • When you want to add a change:

    • export the new swagger,

    • compare it with the archived one,

    • if the change is ok for you, you can replace the archived version.

This way you’ll be sure that you won’t break your API.

The best thing about a specification is when you can leverage it in your runtime code

After adding the test validation I just described, you decide to add another endpoint :

  • You first add the description to the swagger, specifying that a person’s name should be maxlength: 400 characters (because the CRAM can’t process longer names).

  • You write the service code, the first line being ensureLength(person.name, 400);

  • You just wrote the same exact thing two times: something is wrong.

You probably guessed what I’m going to say : you should also rely on the swagger information at runtime to act like a application firewall : each query should be matched by a technical layer with the swagger content before reaching your application code.

Example :

petstore.rb
class Petstore < Sinatra::Base

  type 'PetInput', {
    :required => [:name],
    :properties => {
      :name => {
        :type => String,
        :example => 'doggie',
        :description => 'The pet name',
        :maxLength => 2048,
      }
    }
  }


  # Patch post method to add swagger validation
  # Pseudo code, sinatra doesn't work this way
  def post(path, opts = {}, &block)
    settings.swagger_validator.check_params(path, options)
    super
  end

  endpoint_summary 'Create a pet'
  endpoint_response 200, 'Pet', 'Standard response'
  endpoint_parameter :pet, 'The pet', :body, true, 'PetInput'
  post '/pet/' do |id|
    content_type :json
    # we don't need to validate that the pet name's length is < 2048
    # since it's already specified in the swagger file
    ...
  end
end

If you hacked a tool for the testing case, updating it to manage this case should be a simple task since all the complicated stuff is already done.

(In my opinion this should be a perfect use case for API management solutions. I hope they’ll add this feature soon and talk about it so people will hear about it.)

Again, if you use a statically-typed language, your framework should already cover parts of the validations but not all of them, so you still should do it for the missing ones.

Take Away

  • Test that your swagger content is synchronized with your code, even if the swagger is generated from the code

  • Rely on the swagger content to validate the request to your app