17. Model validation#

Smithy provides a customizable validation system that can be used by API designers and organizations to ensure that their APIs adhere to their own standards, best practices, and constraints.

17.1. Introduction#

APIs require a great deal of care and discipline to ensure that they provide a coherent interface to customers, particularly after an API is released and new features are added. This specification defines metadata that is used to validate a model against configurable validator definitions, ensuring that developers adhere to an organization's API standards.

Tools like Checkstyle and Findbugs help to ensure that developers avoid common bugs and pitfalls when writing code. This is a very powerful concept, particularly for developers that are new to a programming language. This concept is even more powerful when teams use the configurability of these tools to communicate the coding standards of an organization and automate their enforcement. This validation standard allows the same level of conformity and rigor to be applied to Smithy models and API definitions.

17.2. Validators#

The validators metadata property contains an array of validator objects that are used to constrain a model. Each object in the validators array supports the following properties:

Property Type Description
name string Required. The name of the validator to apply. This name is used in implementations to find and configure the appropriate validator implementation. Validators only take effect if a Smithy processor implements the validator.
id string

Defines a custom identifier for the validator.

Multiple instances of a single validator can be configured for a model. Providing an id allows suppressions to suppress a specific instance of a validator.

If id is not specified, it will default to the name property of the validator definition.

IDs that contain dots (.) are hierarchical. For example, the ID "Foo.Bar" contains the ID "Foo". Event ID hierarchies can be leveraged to group validation events and allow more granular suppressions.

message string Provides a custom message to use when emitting validation events. The special {super} string can be added to a custom message to inject the original error message of the validation event into the custom message.
severity string

Provides a custom severity level to use when a validation event occurs. If no severity is provided, then the default severity of the validator is used.

Note

The severity of user-defined validators cannot be set to ERROR.

namespaces [ string ] Provides a list of the namespaces that are targeted by the validator. The validator will ignore any validation events encountered that are not specific to the given namespaces.
selector string A valid selector that causes the validator to only validate shapes that match the selector. The validator will ignore any validation events encountered that do not satisfy the selector.
configuration object Object that provides validator configuration. The available properties are defined by each validator. Validators MAY require that specific configuration properties are provided.

The following Smithy document applies a custom validator named "SomeValidator":

$version: "2"
metadata validators = [
    {
        // The name of the validator.
        name: "SomeValidator"
        // Uses a custom event ID for each validation event emitted.
        id: "CustomEventId"
        // Uses a custom message that also includes the default message.
        message: "My custom message name. {super}"
        // Applies the rule only to the following namespaces.
        namespaces: ["foo.baz", "bar.qux"]
        // The following properties are specific to the validator.
        configuration: {
          "someProperty": "foo"
        }
    }
]

namespace smithy.example

// shapes are defined here...

17.2.1. Missing validators#

The actual implementation of a validator is defined in code and is not defined in the Smithy model itself. If a Smithy implementation does not have an implementation for a specific validator by name, the Smithy implementation MUST emit a WARNING validation event with an event ID that is the concatenation of UnknownValidator_ and the name property of the validator that could not be found. For example, given a custom validator that could not be found named Foo, the implementation MUST emit a validation event with an event ID of UnknownValidator_Foo and a severity of WARNING.

17.3. Severity#

When a model is in violation of a validator, a validation event is emitted. This validation event contains metadata about the violation, including the optional shape that was in violation, the validator ID, and the severity of the violation. Severity is used to define the importance or impact of a violation.

ERROR

Indicates that something is structurally wrong with the model and cannot be suppressed.

Validation events with a severity of ERROR are reserved for enforcing that models adhere to the Smithy specification. Validators cannot emit a validation event with a severity of ERROR.

DANGER
Indicates that something is very likely wrong with the model. Unsuppressed DANGER validation events indicate that a model is invalid.
WARNING
Indicates that something might be wrong with the model.
NOTE
Informational message that does not imply anything is wrong with the model.

17.4. Suppressions#

Suppressions are used to suppress specific validation events. Suppressions are created using the suppress trait and suppressions metadata.

17.4.1. suppress trait#

Summary
The suppress trait is used to suppress validation events(s) for a specific shape. Each value in the suppress trait is a validation event ID to suppress for the shape.
Trait selector
*
Value type
[string]

The following example suppresses the Foo and Bar validation events for the smithy.example#MyString shape:

$version: "2"
namespace smithy.example

@suppress(["Foo", "Bar"])
string MyString

17.4.2. Suppression metadata#

The suppressions metadata property contains an array of suppression objects that are used to suppress validation events for the entire model or for an entire namespace.

Each suppression object in the suppressions array supports the following properties:

Property Type Description
id string Required. The hierarchical validation event ID to suppress.
namespace string Required. The validation event is only suppressed if it matches the supplied namespace. A value of * can be provided to match any namespace. * is useful for suppressing validation events that are not bound to any specific shape.
reason string Provides an optional reason for the suppression.

The following example suppresses all validation events on shapes in the foo.baz namespace with an ID of UnreferencedShape:

$version: "2"
metadata suppressions = [
    {
        id: "UnreferencedShape"
        namespace: "foo.baz"
        reason: "This is a test namespace."
    }
]

The following example suppresses all validation events with an ID of OverlyBroadValidator:

$version: "2"
metadata suppressions = [
    {
        id: "OverlyBroadValidator"
        namespace: "*"
    }
]

17.4.3. Matching suppression event IDs#

Both the suppress trait and suppressions defined in metadata match events hierarchically based on dot (.) segments. For example, given a validation event ID of "Foo.Bar", and a suppression ID of "Foo", the suppression ID matches the event ID because "Foo.Bar" begins with the segment "Foo". However, a suppression ID of "Foo.Bar" does not match an event ID of "Foo" because "Foo.Bar" is more specific. Further, a suppression ID of "ABC" does not match an event ID of "ABC123" because "ABC" is not a segment of the event ID.

Event ID Suppression ID Is match
Foo Foo Yes
Foo.Bar Foo Yes
Foo.Bar.Baz Foo Yes
Foo. Foo. Yes
Foo. Foo Yes
Foo Foo. No
Foosball Foo No
Foo Foo.Bar No
Abc.Foo.Bar Foo.Bar No

17.5. Severity overrides#

The severityOverrides metadata property is used to elevate the severity of non-suppressed validation events. This property contains an array of severity override objects that support the following properties:

Property Type Description
id string Required. The hierarchical validation event ID to elevate.
namespace string Required. The validation event is only elevated if it matches the supplied namespace. A value of * can be provided to match any namespace.
severity string Defines the severity to elevate matching events to. This value can only be set to WARNING or DANGER.

The following example elevates the events of SomeValidator to DANGER in any namespace, and OtherValidator is elevated to WARNING but only for events emitted for shapes in the smithy.example namespace:

$version: "2"

metadata severityOverrides = [
    {
        namespace: "*"
        id: "SomeValidator"
        severity: "DANGER"
    }
    {
        namespace: "smithy.example"
        id: "OtherValidator"
        severity: "WARNING"
    }
]

namespace smithy.example

17.6. Built-in validators#

Smithy provides built-in validators that can be used in any model in the validators metadata property. Implementations MAY support additional validators.

17.6.1. EmitEachSelector#

Emits a validation event for each shape that matches the given selector.

Rationale
Detecting shapes that violate a validation rule using customizable validators allows organizations to create custom Smithy validators without needing to write code.
Default severity
DANGER
Configuration
Property Type Description
selector string Required. A valid selector. A validation event is emitted for each shape in the model that matches the selector.
bindToTrait string An optional string that MUST be a valid shape ID that targets a trait definition. A validation event is only emitted for shapes that have this trait.
messageTemplate string A custom template that is expanded for each matching shape and assigned as the message for the emitted validation event.

The following example detects if a shape is missing documentation with the following constraints:

  • Shapes that have the documentation trait are excluded.
  • Members that target shapes that have the documentation trait are excluded.
  • Simple types are excluded.
  • List and map members are excluded.
$version: "2"
metadata validators = [{
    name: "EmitEachSelector"
    id: "MissingDocumentation"
    message: "This shape is missing documentation"
    configuration: {
        selector: """
            :not([trait|documentation])
            :not(simpleType)
            :not(member :test(< :test(list, map)))
            :not(member > [trait|documentation])"""
    }
}]

The following example emits a validation event for each structure referenced as input/output that has a shape name that does not case-insensitively end with "Input"/"Output":

$version: "2"
metadata validators = [
    {
        name: "EmitEachSelector"
        id: "OperationInputName"
        message: "This shape is referenced as input but the name does not end with 'Input'"
        configuration: {
            selector: "operation -[input]-> :not([id|name$=Input i])"
        }
    }
    {
        name: "EmitEachSelector"
        id: "OperationOutputName"
        message: "This shape is referenced as output but the name does not end with 'Output'"
        configuration: {
            selector: "operation -[output]-> :not([id|name$=Output i])"
        }
    }
]

The following example emits a validation event for each operation referenced as lifecycle 'read' or 'delete' that has a shape name that does not start with "Get" or "Delete":

$version: "2"
metadata validators = [
    {
        name: "EmitEachSelector"
        id: "LifecycleGetName"
        message: "Lifecycle 'read' operation shape names should start with 'Get'"
        configuration: {
            selector: "operation [read]-> :not([id|name^=Get i])"
        }
    }
    {
        name: "EmitEachSelector"
        id: "LifecycleDeleteName"
        message: "Lifecycle 'delete' operation shape names should start with 'Delete'"
        configuration: {
            selector: "operation -[delete]-> :not([id|name^=Delete i])"
        }
    }
]

17.6.1.1. Binding events to traits#

The bindToTrait property contains a shape ID that MUST reference a trait definition shape. When set, this property causes the EmitEachSelector validator to only emit validation events for shapes that have the referenced trait. The contextual location of where the violation occurred in the model SHOULD point to the location where the trait is applied to the matched shape.

Consider the following model:

metadata validators = [
    {
        name: "EmitEachSelector"
        id: "DocumentedString"
        configuration: {
            // matches all shapes
            selector: "*"
            // Only emitted for shapes with the documentation
            // trait, and each event points to where the
            // trait is defined.
            bindToTrait: documentation
        }
    }
]

namespace smithy.example

@documentation("Hello")
string A // <-- Emits an event

string B // <-- Does not emit an event

The DocumentedString validator will only emit an event for smithy.example#A because smithy.example#B does not have the documentation trait.

17.6.1.2. Message templates#

A messageTemplate is used to create more granular error messages. The template consists of literal spans and selector context value templates (for example, @{id}). A selector context value MAY be escaped by placing a @ before a @ character (for example, @@ expands to @). @ characters in the message template that are not escaped MUST form a valid selector_context_value production.

For each shaped matched by the selector of an EmitEachSelector, a selector attribute is created from the shape along with all of the selector variables that were assigned when the shape was matched. Each selector_context_value in the template is then expanded by retrieving nested properties from the shape using a pipe-delimited path (for example, @{id|name} expands to the name of the matching shape's shape ID).

Consider the following model:

metadata validators = [
    {
        name: "EmitEachSelector"
        configuration: {
            selector: "[trait|documentation]"
            messageTemplate: """
                This shape has a name of @{id|name} and a @@documentation \
                trait of "@{trait|documentation}"."""
        }
    }
]

namespace smithy.example

@documentation("Hello")
string A

@documentation("Goodbye")
string B

The above selector will emit two validation events:

Shape ID Expanded message
smithy.example#A This shape has a name of A and a @documentation trait of "Hello".
smithy.example#B This shape has a name of B and a @documentation trait of "Goodbye".

Selector variables can be used in the selector to make message templates more descriptive. Consider the following example:

metadata validators = [
    {
        name: "EmitEachSelector"
        id: "UnstableTrait"
        configuration: {
            selector: """
                  $matches(-[trait]-> [trait|unstable])
                  ${matches}"""
            messageTemplate: "This shape applies traits(s) that are unstable: @{var|matches|id}"
        }
    }
]

namespace smithy.example

@trait
@unstable
structure doNotUseMe {}

@doNotUseMe
string A

The above selector will emit the following validation event:

Shape ID Expanded message
smithy.example#A This shape applies traits(s) that are unstable: [smithy.example#doNotUseMe]

17.6.1.3. Variable message formatting#

Different types of variables expand to different kinds of strings in message templates.

Attribute Expansion
empty values

An empty value expands to nothingness [1]. Empty values are created when a selector context value attempts to access a variable or nested property that does not exist.

Consider the following message template: Hello, @{foo}.. Because foo is not a valid selector attribute, the message expands to:

Hello, .
id Expands to the absolute shape ID of a shape [1].
literal values Literal values are created when descending into nested properties of an id, service, or projection attribute. A literal string is expanded to the contents of the string with no wrapping quotes. A literal integer is expanded to the string representation of the number. [1]
node

A JSON formatted string representation of a trait or nested property of a trait. The JSON is not pretty-printed, meaning there is no indentation or newlines inserted into the JSON output for formatting. For example, a template of @{trait|tags} applied to a shape with a tags trait that contains "a" and "b" would expand to:

["a","b"]
projection

Expands to a list that starts with [ and ends with ]. Each shape in the projection is inserted into the list using variable message formatting. Subsequent shapes are separated from the previous shape by a comma followed by a space. If a variable projection (for example, @{var|foo}) contains two shape IDs, smithy.example#A and smithy.example#B, the attribute expands to:

[smithy.example#A, smithy.example#B]
service Expands to the absolute shape ID of a service shape [1].
trait Expands to nothingness [1].
[1](1, 2, 3, 4, 5) This is the same behavior that is used when the attribute is used in a string comparison.

17.6.2. EmitNoneSelector#

Emits a validation event if no shape in the model matches the given selector.

Rationale
Detecting the omission of a specific trait, pattern, or other requirement can help developers to remember to apply constraint traits, documentation, etc.
Default severity
DANGER
Configuration
Property Type Description
selector string Required. A valid selector. If no shape in the model is returned by the selector, then a validation event is emitted.

The following example detects if the model does not contain any constraint traits.

$version: "2"
metadata validators = [{
    name: "EmitNoneSelector"
    id: "MissingConstraintTraits"
    message: """
        No instances of the enum, pattern, length, or range trait
        could be found. Did you forget to apply these traits?"""
    configuration: {
        selector: ":is([trait|enum], [trait|pattern], [trait|length], [trait|range])"
    }
}]

17.7. traitValidators trait#

It's sometimes necessary to constrain the set of shapes that can be referenced when certain traits are applied to a shape. For example, some protocols don't support event streams or document types. When that kind of protocol trait is applied to a service and the service references such a shape, a validation event should be emitted automatically. This can be achieved without writing code by applying a traitValidators trait to a trait definition.

Summary
A meta-trait used to limit the kinds of shapes that can be referenced by a shape when a trait is applied to the shape.
Trait selector
[trait|trait]
Value type
Map of event ID strings to validator definition objects.

Example

The following example defines a protocol that does not support document types.

$version: "2"

namespace smithy.example

@trait(selector: "service")
@traitValidators(
    "myCustomProtocol.NoDocuments": {
       selector: "~> member :test(> document)"
       message: "myCustomProtocol does not support document types."
    }
)
@protocolDefinition
structure myCustomProtocol {}

If the trait is applied to the following service:

@myCustomProtocol
service MyService {
    operations: [GetFoo]
}

operation GetFoo {
    input := {
        document: Document
    }
}

It will emit the following event:

──  ERROR  ──────────────────────────────────────── myCustomProtocol.NoDocuments
Shape: smithy.example#GetFooInput$document
File:  example.smithy:22:9

21|     input := {
22|         document: Document
  |         ^

Found an incompatible shape when validating the constraints of the
smithy.example#myCustomProtocol trait attached to smithy.example#MyService:
myCustomProtocol does not support document types.

17.7.1. Map key event ID#

The key of the map is the event ID to use with each emitted validation event. The event MUST adhere to the smithy:Namespace syntax.

17.7.2. Validator definition#

Property Type Description
selector string

Required. A Smithy selector that receives only the applied shape. Any shape yielded by the selector is considered incompatible with the trait and causes a validation event.

For example, the following selector would emit an event for any member in the closure of the applied shape that targets a boolean shape:

~> member :test(> boolean)

The following selector would emit an event only if the trait is bound to an operation that defines an input shape with a member named "foo":

-[input]-> structure > member [id|member='foo']
message string Additional context to include in the message of each emitted validation event.
severity string The severity to use when an incompatible shape is found. Defaults to ERROR if not set.