I was asked recently about the limitations of Swagger et al that had encouraged me to write Taxi.

I think it’s a great question, and one that warrants a detailed answer. Both Swagger & RAML are obvious influences into the design of Taxi. I’ve been a big proponent, heavy user, & OSS contributor around Swagger for a number of years. They’re great tools, and I having a huge respect for them.

However, there are limitations that I’d hit which frustrated me as user of Swagger, and there are things I was trying to achieve when building Vyne that just weren’t possible with current tooling.

It’s worth mentioning that Swagger & RAML are both mature tools, whereas Taxi is sitting at v0.1. So, some of the goals outlined here indicate where we’re going, rather than where we are today.

Read & Writeability

Software is first & foremost a communication & storytelling medium. Good code is always measured by how well other people - not machines — are able to understand it.

Whether that’s through self documenting code, ubiquitous language, documentation hubs, BDD test suites, etc. Whatever techniques you choose to employ, the verdict is clear — The ability to capture, share and replay ideas between colleagues is a key factor in successful projects.

Swagger & RAML aren’t good communication formats for people — they’re simply too verbose. It’s a symptom of electing JSON & YAML as their languages — the content of the message gets lost in the noise of the syntax.

Look at these two snippets, both which communicate the same concept:

Swagger vs Taxi — short & sweet

Through brevity, Taxi becomes more writable by hand — which, it turns out, is a key facet for promoting readability, and encourages using Taxi to exchange ideas.

Types as a documentation tool

Taxi also has microtypes as a first class concept — aliasing primitives with more descriptive names. This promotes embedding the UbiquitousLanguage directly within the DSL, and the language as a first class citizen within the type system.

Consider the same example, this time with Microtypes added in:

type Pet {
  id : PetId as Int
  name : Name as String
  tag : Tag as String
}

By leveraging microtypes, the rest of our API documentation becomes much clearer too:

operation findPet(String):Pet
// vs
operation findPet(Name):Pet

operation deletePet(Int)
// vs
operation deletePet(PetId)

Note that as Taxi is a documentation tool, (not a run-time), often we can omit parameter names in operations entirely, and the result is actually more readable.

It also has the benefit that tooling can now make smarter choices around what to provide to the deletePet() operation — not just any old Int will do, it needs to be a PetId.

Extensibility

API documentation exists to help consumers integrate. However, integration is a two-party party, and Swagger ignores one of those parties.

Swagger provides no way for consumers of an API to take the spec, and mix-in consumer specific information.

For example, dumping an object straight to a DB, using JPA — a fairly common task. Given our models are generated from the Swagger spec, it’d be nice as consumer to simply mix-in the annotations for JPA, and be done with it.

Sometimes, Swagger models are a boundary concern, and consumers will convert the inbound object to a type specific to the domain. When that adds value, it’s a sensible pattern to apply. However, if your domain model and the swagger type are the same, then that’s just pure duplication, without merit.

Taxi supports type extensions to fulfil this need, allowing consumers to mix in definitions that are meaningful to them:

type extension Pet {
   @Id id
}

The compiler / generator API is designed to allow plugins, so consumers can connect a JPA specific annotation processor, and bosh — JPA annotations on your generated model. Lovely.

Describing behaviour, not just existence.

Swagger and RAML describe the existence of operations— the address you can find a service at, and broadly what parameters you need to pass.

Taxi aims to go beyond, and describe the purpose of the operation — what happens when you call an operation, and what constraints must you satisfy before doing so.

// Constraints on inputs describe preconditions...
operation calculateCreditRisk(Money(currency = 'GBP')) : CreditRisk

// ...which others can then help satisfy:
operation convertCurrency(source : Money, target : Currency) : Money( from source,  currency = target )

This provides a model that tooling — such as Vyne — can use to understand how & when to call services, and how to chain services together to achieve goals.

The contract DSL is one of the most interesting aspects of Taxi, but it’s also fairly early in it’s implementation — I expect to see this evolve significantly over the coming months. I’m also watching with a keen eye how others in similar spaces are tackling the issue — Kotlin’s new Contracts DSL is really interesting.

Summary

Taxi is just getting going, but I think it offers real strengths over Swagger & RAML.

Strong interoperability with the existing tools is a design goal of Taxi. Ultimately, Taxi should exist as a superset of what’s expressible with Swagger.

Now’s a great time to provide feedback and input into the language as it evolves, to make sure we’re building a first-class DSL for describing API’s. We’ve just created our community over on Spectrum, so stop by and join the conversation.