Quick start#
This document is a tutorial that introduces the Smithy interface definition language (IDL). By reading this tutorial, you will learn:
- How to create a Smithy model
- How to define shapes, including Service, Resource, and Operation shapes
- How to apply traits to shapes
What is Smithy?#
Smithy is an interface definition language and set of tools that allows developers to build clients and servers in multiple languages. Smithy models define a service as a collection of resources, operations, and shapes. A Smithy model enables API providers to generate clients and servers in various programming languages, API documentation, test automation, and example code.
Shapes and traits#
Smithy models consist of shapes and traits. Shapes are instances of types. Traits are used to add more information to shapes that might be useful for clients, servers, or documentation.
Smithy supports the following types:
Type | Description |
---|---|
blob | Uninterpreted binary data |
boolean | Boolean value type |
string | UTF-8 encoded string |
byte | 8-bit signed integer ranging from -128 to 127 (inclusive) |
short | 16-bit signed integer ranging from -32,768 to 32,767 (inclusive) |
integer | 32-bit signed integer ranging from -2^31 to (2^31)-1 (inclusive) |
long | 64-bit signed integer ranging from -2^63 to (2^63)-1 (inclusive) |
float | Single precision IEEE-754 floating point number |
double | Double precision IEEE-754 floating point number |
bigInteger | Arbitrarily large signed integer |
bigDecimal | Arbitrary precision signed decimal number |
timestamp | An instant in time with no UTC offset or timezone. |
document | An untyped JSON-like value. |
List | Homogeneous collection of values |
Map | Map data structure that maps string keys to homogeneous values |
Structure | Fixed set of named heterogeneous members |
Union | Tagged union data structure that can take on several different, but fixed, types |
Service | Entry point of an API that aggregates resources and operations together |
Operation | Represents the input, output and possible errors of an API operation |
Resource | An entity with an identity, set of operations, and child resources |
Weather Service#
In order to demonstrate how Smithy models are defined, we will create a weather service.
- This service provides weather information for cities.
- This service consists of
City
resources andForecast
resources. - The
Weather
service has manyCity
resources, and aCity
resource contains a singleForecast
singleton resource. - This service closure contains the following operations:
ListCities
,GetCity
,GetForecast
,GetCurrentTime
.
Weather
is a Service shape that is defined inside of a
namespace.
$version: "2"
namespace example.weather
/// Provides weather forecasts.
/// Triple slash comments attach documentation to shapes.
service Weather {
version: "2006-03-01"
}
What's that syntax?
Smithy models are defined using either the Smithy IDL or the JSON AST. The JSON AST representation of a model is typically an artifact created by build tools to make them easier to use by other tooling.
//
is used for comments///
is used to add documentation to the following shape.- Keywords like
service
andstructure
start the definition of a shape.
Defining resources#
A resource is contained within a service or another resource. Resources have identifiers, operations, and any number of child resources.
$version: "2"
namespace example.weather
/// Provides weather forecasts.
service Weather {
version: "2006-03-01"
resources: [City]
}
resource City {
identifiers: { cityId: CityId }
read: GetCity
list: ListCities
}
// "pattern" is a trait.
@pattern("^[A-Za-z0-9 ]+$")
string CityId
Because the Weather
service contains many cities, the City
resource
defines an identifier. Identifiers are used
to refer to a specific resource within a service. The "identifiers" property
is a mapping of identifier names to the shape to use for that identifier. If
the input structure of an operation uses the same names and targeted shapes
as the identifiers
property of the resource, the structure is
automatically configured to work with
the resource so that input members of the operation are used to provide the
identity of the resource.
Each City
has a single Forecast
. This can be defined by adding the
Forecast
to the resources
property of the City
.
resource City {
identifiers: { cityId: CityId }
read: GetCity
list: ListCities
resources: [Forecast]
}
resource Forecast {
identifiers: { cityId: CityId }
read: GetForecast
}
Child resources must define the exact same identifiers property of their
parent, but they are allowed to add any number of additional identifiers if
needed. Because there is only one forecast per city, no additional identifiers
were added to the identifiers property that isn't present on the City
resource.
The state of a resource is represented through its
properties. City
contains coordinates, and
Forecast
has a chance of rain represented as a float. Input and output
members of resource operations map to resource properties or identifiers to
perform updates on or examine the state of a resource.
resource City {
identifiers: { cityId: CityId }
properties: { coordinates: CityCoordinates }
read: GetCity
list: ListCities
resources: [Forecast]
}
structure GetCityOutput for City {
$coordinates
}
resource Forecast {
identifiers: { cityId: CityId }
properties: { chanceOfRain: Float }
read: GetForecast
}
structure GetForecastOutput for Forecast {
$chanceOfRain
}
Review
- The
resources
property binds resources to service and resource shapes. - Resources can define identifiers.
- Child resources must define the same identifiers as their parents, and they can also define additional identifiers.
- Resources can define properties.
- Resource properties are set, modified, or read through lifecycle operations.
See also
The target elision syntax for an easy way to define structures that reference resource identifiers and properties without having to repeat the target definition.
Defining operations#
The put
, create
, read
, update
, delete
, and list
properties of a resource are used to define the lifecycle operations of a resource. Lifecycle operations are the canonical
methods used to read and transition the state of a resource using well-defined
semantics. Defining lifecycle operations helps automated tooling reason about
your API.
Let's define the operation used to "read" a City
.
@readonly
operation GetCity {
input: GetCityInput
output: GetCityOutput
errors: [NoSuchResource]
}
@input
structure GetCityInput for City {
// "cityId" provides the identifier for the resource and
// has to be marked as required.
@required
$cityId
}
@output
structure GetCityOutput {
// "required" is used on output to indicate if the service
// will always provide a value for the member.
@required
@notProperty
name: String
@required
$coordinates
}
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
}
And define the operation used to "read" a Forecast
.
@readonly
operation GetForecast {
input: GetForecastInput
output: GetForecastOutput
}
// "cityId" provides the only identifier for the resource since
// a Forecast doesn't have its own.
@input
structure GetForecastInput {
@required
cityId: CityId
}
@output
structure GetForecastOutput {
chanceOfRain: Float
}
Review
- Operations accept and return structured messages.
- Operations are bound to service shapes and resource shapes.
- Operations marked as readonly trait indicate the operation has no side effects.
- Operations should define the errors it can return.
Listing resources#
There are many City
resources contained within the Weather
service.
The list lifecycle operation can be added to the
City
resource to list all of the cities in the service. The list operation
is a collection operation, and as such, MUST NOT
bind the identifier of a City
to its input structure; we are listing
cities, so there's no way we could provide a City
identifier.
/// Provides weather forecasts.
@paginated(inputToken: "nextToken", outputToken: "nextToken",
pageSize: "pageSize")
service Weather {
version: "2006-03-01"
resources: [City]
}
// The paginated trait indicates that the operation may
// return truncated results. Applying this trait to the service
// sets default pagination configuration settings on each operation.
@paginated(items: "items")
@readonly
operation ListCities {
input: ListCitiesInput
output: ListCitiesOutput
}
@input
structure ListCitiesInput {
nextToken: String
pageSize: Integer
}
@output
structure ListCitiesOutput {
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
}
The ListCities
operation is paginated, meaning
the results of invoking the operation can be truncated, requiring subsequent
calls to retrieve the entire list of results. It's usually a good idea to add
pagination to an API that lists resources because it can help prevent
operational issues in the future if the list grows to an unpredicted size.
The CitySummary
structure defines a reference
to a City
resource. This gives tooling a better understanding of the
relationships in your service.
The above example refers to prelude shapes like
String
that are automatically available in all Smithy models.
Review
- The
list
lifecycle operation is used to list resources. list
operations should be paginated trait.- The references trait links a structure to a resource.
- Prelude shapes can help DRY up models.
Non-Lifecycle Operations#
Smithy supports operations that don't fit into the typical create, read,
update, delete, and list lifecycles. Operations can be added to any resource or
service shape with no special lifecycle designation using the operations
property. The following operation gets the current time from the Weather
service.
/// Provides weather forecasts.
@paginated(inputToken: "nextToken", outputToken: "nextToken",
pageSize: "pageSize")
service Weather {
version: "2006-03-01"
resources: [City]
operations: [GetCurrentTime]
}
@readonly
operation GetCurrentTime {
input: GetCurrentTimeInput
output: GetCurrentTimeOutput
}
@input
structure GetCurrentTimeInput {}
@output
structure GetCurrentTimeOutput {
@required
time: Timestamp
}
Building the Model#
Now that you have a model, you'll want to build it and generate things from it.
Building the model creates projections of the model, applies plugins to
generate artifacts, runs validation, and creates a JAR that contains the
filtered projection. The Smithy Gradle Plugin is the best way to get started
building a Smithy model. First, create a smithy-build.json
file:
{
"version": "1.0"
}
Then create a new build.gradle.kts
file:
plugins {
id("software.amazon.smithy").version("0.6.0")
}
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
implementation("software.amazon.smithy:smithy-model:1.42.0")
}
configure<software.amazon.smithy.gradle.SmithyExtension> {
// Uncomment this to use a custom projection when building the JAR.
// projection = "foo"
}
// Uncomment to disable creating a JAR.
//tasks["jar"].enabled = false
Finally, copy the weather model to model/weather.smithy
and
then run gradle build
(make sure you have gradle installed).
Next steps#
That's it! We just created a simple, read-only, Weather
service.
- Try adding a "create" lifecycle operation to
City
. - Try adding a "delete" lifecycle operation to
City
. - Try adding HTTP binding traits to the API.
- Try adding tags to shapes and filtering them out with excludeShapesByTag.
- Follow the Using Code Generation Guide
to generate code for the
Weather
service.
There's plenty more to explore in Smithy. The Smithy specification can teach you everything you need to know about Smithy models. Building Smithy Models can teach you more about the build process, including how to use transformations, projections, plugins, and more. For more sample build configurations, see the examples directory of the Smithy Gradle plugin repository.
Complete example#
If you followed all the steps in this guide, you should have three files, laid out like so:
.
├── build.gradle.kts
├── model
│ └── weather.smithy
└── smithy-build.json
The smithy-build.json
should have the following contents:
{
"version": "1.0"
}
The build.gradle.kts
should have the following contents:
plugins {
id("software.amazon.smithy").version("0.6.0")
}
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
implementation("software.amazon.smithy:smithy-model:1.42.0")
}
configure<software.amazon.smithy.gradle.SmithyExtension> {
// Uncomment this to use a custom projection when building the JAR.
// projection = "foo"
}
// Uncomment to disable creating a JAR.
//tasks["jar"].enabled = false
Finally, the complete weather.smithy
model should look like:
$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 }
read: GetCity
list: ListCities
resources: [Forecast]
}
resource Forecast {
identifiers: { cityId: CityId }
read: GetForecast,
}
// "pattern" is a trait.
@pattern("^[A-Za-z0-9 ]+$")
string CityId
@readonly
operation GetCity {
input: GetCityInput
output: GetCityOutput
errors: [NoSuchResource]
}
@input
structure GetCityInput {
// "cityId" provides the identifier for the resource and
// has to be marked as required.
@required
cityId: CityId
}
@output
structure GetCityOutput {
// "required" is used on output to indicate if the service
// will always provide a value for the member.
@required
name: String
@required
coordinates: CityCoordinates
}
// 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: ListCitiesInput
output: ListCitiesOutput
}
@input
structure ListCitiesInput {
nextToken: String
pageSize: Integer
}
@output
structure ListCitiesOutput {
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 {
input: GetCurrentTimeInput
output: GetCurrentTimeOutput
}
@input
structure GetCurrentTimeInput {}
@output
structure GetCurrentTimeOutput {
@required
time: Timestamp
}
@readonly
operation GetForecast {
input: GetForecastInput
output: GetForecastOutput
}
// "cityId" provides the only identifier for the resource since
// a Forecast doesn't have its own.
@input
structure GetForecastInput {
@required
cityId: CityId
}
@output
structure GetForecastOutput {
chanceOfRain: Float
}