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:

  1. 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....).
  2. 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.
  3. 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
}