Overview of Smithy, an API Description Language From Amazon

Overview of Smithy, an API Description Language From Amazon

Posted in

Description languages are a highly effective and efficient way to define your development cycle. They can help unlock new iteration, granular control, and a deep understanding of the logic and underlying systems. One new definition language, Smithy, has seen a steadily increasing user base. Backed by Amazon, this interface description language (IDL) offers an efficient and portable model-based solution.

Below, we’ll examine Smithy to see what makes it unique. We’ll also consider some specific benefits and drawbacks of using Smithy and get an idea of how it works in practice.

What is Smithy?

So, what exactly is Smithy? Smithy is a protocol-agnostic IDL. At its most basic, IDLs are designed to connect systems by defining the interface itself, removing the need for language similarity or cohesion. Smithy is both an IDL and a set of related tools that promise to unlock client, server, and documentation through models that can leverage code generation to create new artifacts.

Smithy was developed by a team at Amazon to create a code-generating modeling system that solved many of the current issues of similar systems. Code generation has often been protocol or framework-dependent, and the specificity of what environment or protocol could work in these systems was hugely limiting. With Smithy, the intent was to create something protocol-agnostic that could work in various environments.

How Does Smithy Work?

Smithy works by creating models. These models are the fundamental blocks upon which everything else is built. They are structured around the idea of resources and operations, which is a common paradigm that most developers should be familiar with.

The Smithy model as defined in its documentation.

The Smithy model as defined in its documentation.

Additional extension and customizability are granted through the use of shapes and traits within the Semantic Model. These traits can help constrain or define specific elements of shapes, allowing for more granular model creation and development.

The Smithy model as defined in its documentation.

The Smithy model as defined in its documentation.

This use of traits allows for higher customization and constraint, and with Smithy, meta-model trait evolution is a key function. This combination between resource-based model specification and evolution through traits allows Smithy to morph and be incredibly extensible. Smithy also offers a pretty substantial validation system to ensure that these morphing changes are still valid within the greater model rules and policies.

Going a step further, Smithy even allows the model itself to be segmented and morphed according to interest. Different parts of the model can be owned by different teams, allowing for higher collaboration and segmentation of impact to pieces of the whole. This is a huge step towards building collaboration into the underlying system. The projection system offered by Smithy also allows for views of the model to be created for different audiences without segmenting or altering the core model itself. This allows for the creation of different branches and purpose-built models, for instance, as a B2B play.

Ultimately, Smithy is focused on creating a single model that can then be used to create, morph, and extend regardless of environment — a lofty goal that mirrors the movement to microservices and agnostic deployment. The IDL is split into three sections that help constrain and define the output:

  • Control: Defines the version of the IDL as well as other controls on the generative process.
  • Metadata: Defines and applies metadata under which the model will be described.
  • Shape: Defines the shapes and traits of the data structure.

Smithy in Practice

The following is a basic example provided in the documentation — this code forms a weather service in Smithy:

$version: "2"

namespace example.weather

/// Provides weather forecasts.
@paginated(inputToken: "nextToken", outputToken: "nextToken", pageSize: "pageSize")
service Weather {
    version: "2006-03-01"
    resources: [
        City
    ]
    operations: [
        GetCurrentTime
    ]
}

resource City {
    identifiers: { cityId: CityId }
    properties: { coordinates: CityCoordinates }
    read: GetCity
    list: ListCities
    resources: [
        Forecast
    ]
}

resource Forecast {
    identifiers: { cityId: CityId }
    properties: { chanceOfRain: Float }
    read: GetForecast
}

//"pattern" is a trait.
@pattern("^[A-Za-z0-9 ]+$")
string CityId

@readonly
operation GetCity {
    input := for City {
        //"cityId" provides the identifier for the resource and
        // has to be marked as required.
        @required
        $cityId
    }

    output := for City {
        //"required" is used on output to indicate if the service
        // will always provide a value for the member.
        //"notProperty" indicates that top-level input member "name"
        // is not bound to any resource property.
        @required
        @notProperty
        name: String

        @required
        $coordinates
    }

    errors: [
        NoSuchResource
    ]
}

// This structure is nested within GetCityOutput.
structure CityCoordinates {
    @required
    latitude: Float

    @required
    longitude: Float
}

//"error" is a trait that is used to specialize
// a structure as an error.
@error("client")
structure NoSuchResource {
    @required
    resourceType: String
}

// The paginated trait indicates that the operation may
// return truncated results.
@readonly
@paginated(items: "items")
operation ListCities {
    input := {
        nextToken: String
        pageSize: Integer
    }

    output := {
        nextToken: String

        @required
        items: CitySummaries
    }
}

// CitySummaries is a list of CitySummary structures.
list CitySummaries {
    member: CitySummary
}

// CitySummary contains a reference to a City.
@references([
    {
        resource: City
    }
])
structure CitySummary {
    @required
    cityId: CityId

    @required
    name: String
}

@readonly
operation GetCurrentTime {
    output := {
        @required
        time: Timestamp
    }
}

@readonly
operation GetForecast {
    input := for Forecast {
        //"cityId" provides the only identifier for the resource since
        // a Forecast doesn't have its own.
        @required
        $cityId
    }

    output := for Forecast {
        $chanceOfRain
    }
}

Breaking this into individual pieces, we can see the control part of the code at the top:

$version: "2"

Following this is the substantive code:

namespace example.weather

/// Provides weather forecasts.
@paginated(inputToken: "nextToken", outputToken: "nextToken", pageSize: "pageSize")
service Weather {
    version: "2006-03-01"
    resources: [
        City
    ]
    operations: [
        GetCurrentTime
    ]
}

resource City {
    identifiers: { cityId: CityId }
    properties: { coordinates: CityCoordinates }
    read: GetCity
    list: ListCities
    resources: [
        Forecast
    ]
}

resource Forecast {
    identifiers: { cityId: CityId }
    properties: { chanceOfRain: Float }
    read: GetForecast
}

Here we have the definition of the service (along with its pagination) as well as the resources that are leveraged and the actions taken upon those resources.

//"pattern" is a trait.
@pattern("^[A-Za-z0-9 ]+$")
string CityId

@readonly
operation GetCity {
    input := for City {
        //"cityId" provides the identifier for the resource and
        // has to be marked as required.
        @required
        $cityId
    }

    output := for City {
        //"required" is used on output to indicate if the service
        // will always provide a value for the member.
        //"notProperty" indicates that top-level input member "name"
        // is not bound to any resource property.
        @required
        @notProperty
        name: String

        @required
        $coordinates
    }

    errors: [
        NoSuchResource
    ]
}

// This structure is nested within GetCityOutput.
structure CityCoordinates {
    @required
    latitude: Float

    @required
    longitude: Float
}

//"error" is a trait that is used to specialize
// a structure as an error.
@error("client")
structure NoSuchResource {
    @required
    resourceType: String
}

// The paginated trait indicates that the operation may
// return truncated results.
@readonly
@paginated(items: "items")
operation ListCities {
    input := {
        nextToken: String
        pageSize: Integer
    }

    output := {
        nextToken: String

        @required
        items: CitySummaries
    }
}

// CitySummaries is a list of CitySummary structures.
list CitySummaries {
    member: CitySummary
}

// CitySummary contains a reference to a City.
@references([
    {
        resource: City
    }
])
structure CitySummary {
    @required
    cityId: CityId

    @required
    name: String
}

@readonly
operation GetCurrentTime {
    output := {
        @required
        time: Timestamp
    }
}

@readonly
operation GetForecast {
    input := for Forecast {
        //"cityId" provides the only identifier for the resource since
        // a Forecast doesn't have its own.
        @required
        $cityId
    }

    output := for Forecast {
        $chanceOfRain
    }
}

From here, we have a series of trait definitions and nested structures that allow for the permutation of output and function within the model, allowing for greater control of the fundamental output and functionality. Looking at this code, you can see how definition by trait unlocks functional control. In essence, traits allow for constraining and formatting data and services without fundamentally altering the underlying entity.

Pros and Cons of Smithy

With this in mind, what are the pros and cons of using something like Smithy?

Pros

The biggest gain from adopting Smithy for most users is going to be the fact that it is protocol agnostic. Not all development is thought out so far in advance that the protocol you utilize today is the right one for all of time. Adopting a solution like Smithy allows you to create models for your work today that can morph, change, and alter with new development. This gives you a large amount of flexibility, ultimately breaking your development from heavy constraints.

Focusing on a model-driven system allows for both machine and human readability that is highly efficient. Models can be ported from system to system and converted into other formats and types, freeing up the underlying system from the weight of external control. Models are also easy to compare, allowing for better automation, thereby increasing efficiency.

Smithy is also open source, which is a huge benefit! A protocol-agnostic solution is great, but when the development of such a solution is also decoupled from source control that might make it closed off and centrally controlled, that gives it a greater velocity for development and iteration. Smithy is a tool that can do a lot on its own, but opening it to community development and iteration could make it a platform for some pretty amazing things in the long-term!

Cons

While Smithy does a lot of excellent work with its model-based system, it offers code generation as a big feature. The fact of the matter is that code generation still has significant problems, and even with the sheer number of fixes and systems on offer here, those issues don’t disappear. Code gen with Smithy works more often than not, but as with any code-gen tool, 99% of the time, the output is OK, but that 1% means you need to pay attention very closely and put resources into the review, which mitigates some of the benefit gained from code gen.

Smithy is fully featured. In some cases, this could be a negative. If you’re looking to build something very simple that only has a handful of people interacting with it, the idea of projecting for other viewers or creating model access rules for multiple teams might not be in your wheelhouse. In such a case, the cost of bringing Smithy on board may not be justified. As with anything, you should consider whether Smithy is something you need to use or something you only want to use.

Finally, it should be briefly noted that this is a product developed by Amazon. While this certainly should not be the sole reason to adopt or not adopt a tool, the long-term maintenance and health of this project are tied to the long-term maintenance and health of Amazon itself. While Amazon doesn’t look to be going anywhere soon, stories of corporate development languishing and breaking ten or twenty years after its first release are a dime a dozen for those who have been in the web development space for any amount of time, and this should certainly be a consideration. Even if the offering is open source, open source does not mean developed in perpetuity, and at any given moment it could be decided that Smithy is just not worth the time or resources.

There is also a trend of open-source software converting into more commercial licenses. As of version 2.15, Buoyant Enterprise for Linkerd has announced that Linkerd will no longer produce an open-source stable release. As of 2024, Redis controversially has moved away from its BSD license, causing many in the community to raise serious concerns. These stories are unfortunately common and illuminate a huge issue with a solution like Smithy — it fits into the open source framework until it doesn’t, and that shift can be hugely impactful down the line.

Conclusion

Ultimately, Smithy is a great option for systems adopting a powerful model-based solution. While it might be too heavy for all implementations, its flexibility and extensibility make it a solid value proposition for many situations.

For a view of what these models look like, check out the Smithy Quick Start Guide. Let us know if you’ve used Smithy and what your thoughts were on it in the comments below!