3. Aggregate types#
Aggregate types contain configurable member references to others shapes.
3.1. List#
The list type represents an ordered homogeneous collection of values.
A list shape requires a single member named member
. Lists are defined
in the IDL using a list_statement.
The following example defines a list with a string member from the
prelude:
$version: "2"
namespace smithy.example
list MyList {
member: String
}
3.1.1. List member optionality#
Lists are considered dense by default, meaning they cannot contain null
values. A list MAY be made sparse by applying the sparse trait. The
following example defines a sparse list:
@sparse
list SparseList {
member: String
}
3.1.2. List member shape ID#
The shape ID of the member of a list is the list shape ID followed by
$member
. For example, the shape ID of the list member in the above
example is smithy.example#MyList$member
.
3.2. Map#
The map type represents a map data structure that maps string
keys to homogeneous values. A map requires a member named key
that MUST target a string
shape and a member named value
.
Maps are defined in the IDL using a map_statement.
The following example defines a map of strings to integers:
$version: "2"
namespace smithy.example
map IntegerMap {
key: String
value: Integer
}
3.2.1. Map member optionality#
3.2.1.1. Map keys are never optional#
Map keys are not permitted to be null
. Not all protocol serialization
formats have a way to define null
map keys, and map implementations
across programming languages often do not allow null
keys in maps.
3.2.1.2. Map values are always present by default#
Maps values are considered dense by default, meaning they cannot contain
null
values. A map MAY be made sparse by applying the
sparse trait. The following example defines a sparse map:
@sparse
map SparseMap {
key: String
value: String
}
3.2.2. Map member shape IDs#
The shape ID of the key
member of a map is the map shape ID followed by
$key
, and the shape ID of the value
member is the map shape ID
followed by $value
. For example, the shape ID of the key
member in
the above map is smithy.example#IntegerMap$key
, and the value
member is smithy.example#IntegerMap$value
.
3.3. Structure#
The structure type represents a fixed set of named, unordered, heterogeneous values. A structure shape contains a set of named members, and each member name maps to exactly one member definition. Structures are defined in the IDL using a structure_statement.
The following example defines a structure with three members, one of which is marked with the required trait, and one that is marked with the default trait using IDL syntactic sugar.
$version: "2"
namespace smithy.example
structure MyStructure {
foo: String
@required
baz: Integer
greeting: String = "Hello"
}
See also
- Applying traits for a description of how to apply traits.
- Mixins to reduce structure duplication
- Target Elision to define members that inherit target from resources or mixins.
3.3.1. Adding new structure members#
Members MAY be added to structures. New members MUST NOT be marked with the required trait. New members SHOULD be added to the end of the structure. This ensures that programming languages that require a specific data structure layout or alignment for code generated from Smithy models are able to maintain backward compatibility.
3.3.2. Structure member shape IDs#
The shape ID of a member of a structure is the structure shape ID, followed
by $
, followed by the member name. For example, the shape ID of the foo
member in the above example is smithy.example#MyStructure$foo
.
3.3.3. Structure member optionality#
Whether a structure member is optional is determined by evaluating the
required trait, default trait, clientOptional trait,
input trait, and addedDefault trait. Authoritative model
consumers like servers MAY choose to determine optionality using more
restrictive rules by ignoring the @input
and @clientOptional
traits.
Trait | Authoritative | Non-Authoritative |
---|---|---|
@clientOptional | Ignored | Optional regardless of the @required or @default trait |
@input | Ignored | All members are optional regardless of the @required or @default trait |
@required | Present | Present unless also @clientOptional or part of an @input structure |
@default | Present | Present unless also @clientOptional or part of an @input structure |
(Other members) | Optional | Optional |
3.3.3.1. Required members#
The required trait indicates that a value MUST always be present for a member in order to create a valid structure. Code generators SHOULD generate accessors for these members that always return a value.
structure TimeSpan {
// years must always be present to make a TimeSpan
@required
years: Integer
}
3.3.3.1.1. Client error correction#
If a mis-configured server fails to serialize a value for a required member, to avoid downtime, clients MAY attempt to fill in an appropriate default value for the member:
- boolean: false
- numbers: 0
- timestamp: 0 seconds since the Unix epoch
- string: ""
- blob: empty bytes
- document: null
- list: []
- map: {}
- enum, intEnum, union: The unknown variant. These types SHOULD define an unknown variant to account for receiving unknown members.
- union: The unknown variant. Code generators for unions SHOULD define an unknown variant to account for newly added members.
- structure: {} if possible, otherwise a deserialization error.
3.3.3.2. Default values#
The default trait gives a structure member a default value. The
following example uses syntactic sugar in the Smithy IDL allows to assign
a default value to the days
member.
structure TimeSpan {
@required
years: Integer
days: Integer = 0
}
3.3.3.3. Evolving requirements and members#
Requirements change; what is required today might not be required tomorrow. Smithy provides several ways to make it so that required members no longer need to be provided without breaking previously generated code.
3.3.3.3.1. Migrating @required
to @default
#
If a required
member no longer needs to be be required, the required
trait MAY be removed and replaced with the default trait. Alternatively,
a default
trait MAY be added to a member marked as required
to
provide a default value for the member but require that it is serialized.
Either way, the member is still considered always present to tools like code
generators, but instead of requiring the value to be provided by an end-user,
a default value is automatically provided if missing. For example, the previous
TimeSpan
model can be backward compatibly changed to:
structure TimeSpan {
// @required is replaced with @default and @addedDefault
@addedDefault
years: Integer = 0
days: Integer = 0
}
The addedDefault trait trait SHOULD be used any time a default
trait is
added to a previously published member. Some tooling does not treat the
required
trait as non-nullable but does treat the default
trait as
non-nullable.
3.3.3.3.2. Requiring members to be optional#
The clientOptional trait is used to indicate that a member that is
currently required by authoritative model consumers like servers MAY become
completely optional in the future. Non-authoritative model consumers like
client code generators MUST treat the member as if it is not required and
has no default value. Authoritative model consumers MAY choose to ignore
the clientOptional
trait.
For example, the following structure:
structure UserData {
@required
@clientOptional
summary: String
}
Can be backward-compatibly updated to remove the required
trait:
structure UserData {
summary: String
}
Replacing both the required
and clientOptional
trait with the default
trait is not a backward compatible change because model consumers would
transition from assuming the value is optional to assuming that it is always
present due to a default value.
3.3.3.3.3. Model evolution and the @input
trait#
The input trait specializes a structure as the input of a single
operation. Transitioning top-level members from required
to optional is
allowed for such structures because it is loosening an input constraint.
Non-authoritative model consumers like clients MUST treat each member as
nullable regardless of the required
or default
trait. This means that
it is a backward compatible change to remove the required
trait from a
member of a structure marked with the input
trait, and the default
trait does not need to be added in its place.
The special ":=" syntax for the operation input property automatically applies
the input
trait:
operation PutTimeSpan {
input := {
@required
years: String
}
}
Because of the input
trait, the operation can be updated to remove the
required
trait without breaking things like previously generated clients:
operation PutTimeSpan {
input := {
years: String
}
}
3.4. Union#
The union type represents a tagged union data structure that can take on several different, but fixed, types. Unions function similarly to structures except that only one member can be used at any one time. Each member in the union is a variant of the tagged union, where member names are the tags of each variant, and the shapes targeted by members are the values of each variant.
Unions are defined in the IDL using a union_statement. A union shape MUST contain one or more named members. The following example defines a union shape with several members:
$version: "2"
namespace smithy.example
union MyUnion {
i32: Integer
@length(min: 1, max: 100)
string: String,
time: Timestamp,
}
3.4.1. Unit types in unions#
Some union members might not need any meaningful information beyond the
tag itself. For these cases, union members MAY target Smithy's built-in
unit type, smithy.api#Unit
.
The following example defines a union for actions a player can take in a game.
union PlayerAction {
/// Quit the game.
quit: Unit,
/// Move in a specific direction.
move: DirectedAction,
/// Jump in a specific direction.
jump: DirectedAction
}
structure DirectedAction {
@required
direction: Integer
}
The quit
action has no meaningful data associated with it, while move
and jump
both reference DirectedAction
.
3.4.2. Union member presence#
Exactly one member of a union MUST be set. The serialization of a union is
defined by a protocol, but for example
purposes, if unions were to be represented in a hypothetical JSON
serialization, the following value would be valid for the PlayerAction
union because a single member is present:
{
"move": {
"direction": 1
}
}
The following value is invalid because multiple members are present:
{
"quit": {},
"move": {
"direction": 1
}
}
The following value is invalid because no members are present:
{}
3.4.3. Adding new union members#
New members added to existing unions SHOULD be added to the end of the union. This ensures that programming languages that require a specific data structure layout or alignment for code generated from Smithy models are able to maintain backward compatibility.
3.4.4. Union member shape IDs#
The shape ID of a member of a union is the union shape ID, followed
by $
, followed by the member name. For example, the shape ID of the i32
member in the above example is smithy.example#MyUnion$i32
.
3.5. Recursive shape definitions#
Smithy allows recursive shape definitions with the following limitations:
- The member of a list or map cannot directly or transitively target
its containing shape unless one or more members in the path from the
container back to itself targets a structure or union shape. This ensures
that shapes that are typically impossible to define in various programming
languages are not defined in Smithy models (for example, you can't define
a recursive list in Java
List<List<List....
). - To ensure a value can be provided for a structure, recursive member relationship from a structure back to itself MUST NOT be made up of all required structure members.
- To ensure a value can be provided for a union, recursive unions MUST contain at least one path through its members that is not recursive or steps through a list, map, or optional structure member.
The following recursive shape definition is valid:
$version: "2"
namespace smithy.example
list ValidList {
member: IntermediateStructure
}
structure IntermediateStructure {
foo: ValidList
}
The following recursive shape definition is invalid:
$version: "2"
namespace smithy.example
list RecursiveList {
member: RecursiveList
}
The following recursive shape definition is invalid due to mutual recursion and the required trait.
$version: "2"
namespace smithy.example
structure RecursiveShape1 {
@required
recursiveMember: RecursiveShape2
}
structure RecursiveShape2 {
@required
recursiveMember: RecursiveShape1
}