Repository: smithy-lang/smithy
Pull request #1858 (2023-07-14 19:36:28)

Define timestamp with more specificity
*Description of changes:*

Timestamps have so far been under-defined by omitting the temporal resolution and allowable date range.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.



Patch:
diff --git a/build.gradle b/build.gradle
index 7c2f8d87e..dd79739ce 100644
--- a/build.gradle
+++ b/build.gradle
@@ -69,7 +69,7 @@ subprojects {
     task sourcesJar(type: Jar) {
         metaInf.with(licenseSpec)
         from {
-            sourceSets.main.allJava
+            sourceSets.main.allSource
         }
         archiveClassifier = "sources"
     }
diff --git a/config/spotbugs/filter.xml b/config/spotbugs/filter.xml
index 5c40defe0..3e4ed00a8 100644
--- a/config/spotbugs/filter.xml
+++ b/config/spotbugs/filter.xml
@@ -149,6 +149,12 @@
         <Bug pattern="NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE"/>
     </Match>
 
+    <!-- This is a false positive. The value is guarded behind an Objects.requireNonNull -->
+    <Match>
+        <Class name="software.amazon.smithy.model.validation.testrunner.SmithyTestCase"/>
+        <Bug pattern="NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE"/>
+    </Match>
+
     <!-- We don't care about the return value of putIfAbsent in this cache -->
     <Match>
         <Class name="software.amazon.smithy.model.shapes.ShapeId$ShapeIdFactory"/>
diff --git a/designs/defining-timestamps.md b/designs/defining-timestamps.md
new file mode 100644
index 000000000..b63c37e43
--- /dev/null
+++ b/designs/defining-timestamps.md
@@ -0,0 +1,165 @@
+# Defining Smithy Timestamps
+
+* **Author**: Michael Dowling
+* **Created**: 2023-07-14
+
+# Abstract
+
+This document defines the missing details of Smithy timestamp shapes that will
+help ensure consistent implementations across programming languages, including
+the temporal resolution and range of allowable dates.
+
+## Motivation
+
+The timestamp shape is a protocol agnostic instant in time; its data type in
+code does not change based on the protocol bindings or serialized format of the
+timestamp. Smithy's timestamp shape does not currently define critical details
+that implementations need to consistently implement Smithy across programming
+languages, including the temporal resolution of a timestamp or the supported
+date range. This lack of specification risks incompatibilities across
+implementations.
+
+## Proposal
+
+A timestamp represents an instant in time in the proleptic Gregorian calendar,
+independent of local times or timezones. Timestamps support an allowable date
+range between midnight January 1, 0001 CE to 23:59:59.999 on
+December 31, 9999 CE, with a temporal resolution of 1 millisecond. This
+resolution and range ensures broad support across programming languages and
+guarantees compatibility with [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html).
+
+### Rationale
+
+Determining the temporal resolution and allowable range of a Smithy timestamp is
+difficult because we want to ensure broad compatibility across programming
+languages. Smithy protocols today support an undefined fractional precision for
+many timestamp formats, which in practice when calling AWS APIs is millisecond
+precision. This implies that Smithy timestamps should at least support
+millisecond precision, but it does not imply a minimum and maximum date. To
+determine an appropriate range, we observe the following programming language
+date/time types:
+
+* .NET
+  - [DateTime](https://learn.microsoft.com/en-us/dotnet/api/system.datetime?view=net-7.0):
+    Nanosecond precision (every 100 nanosecond "ticks"), with a range of
+    January 1, 0001 through December 31, 9999.
+* C++: [chrono time_point](https://en.cppreference.com/w/cpp/chrono/time_point):
+  Seems to allow any resolution, calendar, or date range.
+* Go: [Time](https://pkg.go.dev/time#Time): Nanosecond precision, using a
+  signed int to represent years.
+* Java:
+    * [Instant](https://docs.oracle.com/javase/8/docs/api/java/time/Instant.html):
+      Nanosecond precision, ranging from -1000000000-01-01 to 1000000000-12-31.
+    * [Date](https://docs.oracle.com/javase/8/docs/api/java/util/Date.html):
+      Millisecond precision with a range defined by a Long, allowing for dates
+      in the distant past and future.
+* JavaScript: [Date](https://262.ecma-international.org/5.1/#sec-15.9.1.1):
+  Millisecond precision. Can store ±8,640,000,000,000,000 milliseconds, which is
+  based on and slightly less than the maximum integral number that can be safely
+  represented by a JavaScript number. This provides a range from April 20,
+  271821 BCE to September 13, 275760 CE.
+* Kotlin: Java's Instant or [kotlinx.Instant](https://kotlinlang.org/api/kotlinx-datetime/kotlinx-datetime/kotlinx.datetime/-instant/):
+  Nanosecond precision, with an undocumented range.
+* PHP: [DateTime](https://www.php.net/manual/en/intro.datetime.php):
+  Microsecond precision, "The range is from about 292 billion years in the past
+  to the same in the future".
+* Python: [datetime](https://docs.python.org/3/library/datetime.html):
+  Microsecond precision, ranging from 0001-01-01T00:00:00Z to 9999-12-31T23:59:
+  59.999999Z.
+* Ruby: [DateTime](https://ruby-doc.org/stdlib-2.6.1/libdoc/date/rdoc/DateTime.html):
+  Nanosecond precision, with an undocumented range.
+* Swift: [Date](https://developer.apple.com/documentation/foundation/date)
+  / [NSDate](https://developer.apple.com/documentation/foundation/nsdate):
+  "sub-millisecond" precision, with the underlying representation using a Double
+  to contain sections and fractions of a second.
+
+Millisecond precision is the most broadly supported. Setting the default
+precision of timestamps to nanoseconds rather than milliseconds would
+immediately render the AWS SDK for PHP, JavaScript, Botocore, and many Amazon
+services incompatible with the requirement. Languages without nanosecond
+support would need to create custom data structures to represent timestamps
+that are incompatible with the rest of their ecosystem.
+
+Python and .NET's date ranges are the most limited. However, this range likely
+captures most of the use cases developers would expect from Smithy timestamps
+(i.e., typically representing nearby instants in time), and also ensures
+timestamp values are compatible with
+[RFC 3339 dates](https://www.ietf.org/rfc/rfc3339.txt).
+
+### Protocol limitations
+
+While Smithy timestamps can store timestamps with millisecond precision, not all
+protocols support serializing timestamps with this precision. For example,
+the `http-date` timestamp format does not support anything beyond second
+precision, which can result in a loss of precision when transmitting a
+timestamp. Modelers should be aware of the limitations of timestamp formats when
+defining Smithy models.
+
+This proposal will update the specification of Smithy's
+existing [timestamp formats](https://smithy.io/2.0/spec/protocol-traits.html#timestampformat-trait)
+to document the representable precision and behavior when a timestamp is too
+granular.
+
+* `http-date`: Second precision with no fractional seconds. A deserializer that
+  encounters an `http-date` timestamp with fractional precision SHOULD fail to
+  deserialize the value (for example, an HTTP server SHOULD return a 400 status
+  code).
+* `date-time`: Millisecond precision. Values that are more granular than
+  millisecond precision SHOULD be truncated to fit millisecond precision.
+* `epoch-seconds`: Millisecond precision. Values that are more granular than 
+  millisecond precision SHOULD be truncated to fit millisecond precision.
+
+#### Protocol limitations rationale
+
+Because the resolution of a Smithy timestamp was previously undefined,
+truncating timestamps that are too granular rather than failing to deserialize
+them ensures that existing and new implementations can interoperate. Truncating
+overly precise times also keeps the door open in case support for nanosecond
+precision is needed in the future (e.g., using a hypothetical
+`@resolution("nanos")` trait).
+
+### Timestamp representation in code
+
+Implementations MUST NOT directly expose the serialized value of a timestamp.
+Timestamps are an abstraction to represent an instant in time, and the
+serialization format of a timestamp can vary depending on the protocol
+or `@timestampFormat` trait of the shape. Because Smithy is protocol agnostic,
+changing the serialization format of a shape MUST NOT have any impact on the
+representation of that shape in code.
+
+When generating code, implementations MUST use a type for Smithy timestamp
+shapes that can hold *at least* the minimum and maximum supported values using
+millisecond precision. Implementations MAY use a type that represents dates with
+a wider range or temporal resolution than Smithy's timestamp shape.
+Implementations SHOULD use an idiomatic date/time data structure for the
+target environment, though they may need to use an alternative data structure
+to represent Smithy timestamps if they do not have access to a date/time type
+with at least millisecond precision spanning the supported range.
+
+Some programming language types offer more granularity than millisecond
+precision (e.g., Java's `Instant`). Using such data types to represent a Smithy
+timestamp is fine as long as the timestamp is serialized according to any
+limitations defined in the protocol and any `@timestampFormat` trait applied to
+the timestamp. Implementations SHOULD NOT refuse to serialize a timestamp due to
+an overly granular value, and instead they should serialize the timestamp
+according to the limitations of the protocol and model.
+
+## Alternatives and tradeoffs
+
+### Use nanosecond resolution
+
+Standardizing on nanoseconds would make it more difficult for JavaScript (ms),
+PHP (us), Python (us), and to an extent .NET to losslessly represent timestamps.
+Smithy implementations for these programming languages would need to choose
+whether they should use a lossy but idiomatic data type when working with
+timestamps, or if they want to create their own data structure.
+
+Languages without nanosecond support could provide a light-weight data structure
+that easily converts to idiomatic date/time types. This kind of abstraction
+SHOULD NOT be created for languages that already provide compatible date/time
+types. A drawback of such a type is that a Smithy timestamp would be
+incompatible with the rest of the language's date/time types (e.g, intervals).
+Any date/time-based logic would have to be handled manually by end-users.
+
+Finally, nanosecond precision is not currently supported by most of AWS, which
+supports millisecond precision.
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 7786b761b..998c28219 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,5 +1,5 @@
 sphinx>=4.2.0,<4.3.0
-pygments==2.12.0
+pygments==2.15.0
 furo==2022.6.21
 
 # Used by new docs.
diff --git a/docs/setup.py b/docs/setup.py
index be7efef78..517c9c956 100644
--- a/docs/setup.py
+++ b/docs/setup.py
@@ -4,7 +4,7 @@ from setuptools import setup, find_packages
 
 requires = [
     "sphinx==4.2.0",
-    "pygments==2.12.0",
+    "pygments==2.15.0",
     "sphinx_copybutton==0.5.0",
     # Used by new docs.
     "sphinx-inline-tabs==2022.1.2b11",
diff --git a/docs/source-1.0/spec/aws/aws-core.rst b/docs/source-1.0/spec/aws/aws-core.rst
index 81f657826..c2e84b376 100644
--- a/docs/source-1.0/spec/aws/aws-core.rst
+++ b/docs/source-1.0/spec/aws/aws-core.rst
@@ -26,10 +26,12 @@ Value type
     * :ref:`service-cloudformation-name`
     * :ref:`service-arn-namespace`
     * :ref:`service-cloudtrail-event-source`
+    * :ref:`service-doc-id`
     * :ref:`service-endpoint-prefix`
 
 The following example defines an AWS service that uses the default values of
-``cloudFormationService``, ``arnNamespace``, and ``cloudTrailEventSource``:
+``cloudFormationService``, ``arnNamespace``, ``cloudTrailEventSource`` and
+``docId``:
 
 .. tabs::
 
@@ -78,6 +80,7 @@ The following example provides explicit values for all properties:
             cloudFormationName: "FooBaz",
             arnNamespace: "myservice",
             cloudTrailEventSource: "myservice.amazon.aws",
+            docId: "some-value-2018-03-17",
             endpointPrefix: "my-endpoint"
         )
         service FooBaz {
@@ -97,6 +100,7 @@ The following example provides explicit values for all properties:
                             "sdkId": "Some Value",
                             "cloudFormationName": "FooBaz",
                             "arnNamespace": "myservice",
+                            "docId": "some-value-2018-03-17",
                             "cloudTrailEventSource": "myservice.amazon.aws"
                         }
                     }
@@ -233,6 +237,31 @@ Amazon CloudWatch is ``monitoring.amazonaws.com``. Such services will
 need to explicitly configure the ``cloudTrailEventSource`` setting.
 
 
+.. _service-doc-id:
+
+``docId``
+=========
+
+The ``docId`` property is a ``string`` value that is used to implement linking
+between service and SDK documentation for AWS services.
+
+This will default to the ``sdkId`` value in lower case followed by the service
+``version`` property, separated by dashes. For the example below, the value
+for this property would default to ``some-value-2018-03-17``.
+
+.. code-block:: smithy
+
+    $version: "1.0"
+    namespace aws.fooBaz
+
+    use aws.api#service
+
+    @service(sdkId: "Some Value")
+    service FooBaz {
+        version: "2018-03-17",
+    }
+
+
 .. _service-endpoint-prefix:
 
 ``endpointPrefix``
diff --git a/docs/source-1.0/spec/aws/aws-restjson1-protocol.rst b/docs/source-1.0/spec/aws/aws-restjson1-protocol.rst
index eb65d62de..0c47eb3ba 100644
--- a/docs/source-1.0/spec/aws/aws-restjson1-protocol.rst
+++ b/docs/source-1.0/spec/aws/aws-restjson1-protocol.rst
@@ -154,9 +154,8 @@ that affect serialization:
       - Binds a top-level operation input structure member to a label in
         the hostPrefix of an endpoint trait.
     * - :ref:`http <http-trait>`
-      - Configures the HTTP bindings of an operation. An operation that
-        does not define the ``http`` trait is ineligible for use with
-        this protocol.
+      - Configures the HTTP bindings of an operation. An operation bound to a
+        service with this protocol applied MUST have the ``http`` trait applied.
     * - :ref:`httpError <httpError-trait>`
       - A ``client`` error has a default status code of ``400``, and a
         ``server`` error has a default status code of ``500``. The
diff --git a/docs/source-1.0/spec/aws/aws-restxml-protocol.rst b/docs/source-1.0/spec/aws/aws-restxml-protocol.rst
index ab971cad3..ecf98f6d8 100644
--- a/docs/source-1.0/spec/aws/aws-restxml-protocol.rst
+++ b/docs/source-1.0/spec/aws/aws-restxml-protocol.rst
@@ -150,9 +150,8 @@ that affect serialization:
       - Binds a top-level operation input structure member to a label in
         the hostPrefix of an endpoint trait.
     * - :ref:`http <http-trait>`
-      - Configures the HTTP bindings of an operation. An operation that
-        does not define the ``http`` trait is ineligible for use with
-        this protocol.
+      - Configures the HTTP bindings of an operation. An operation bound to a
+        service with this protocol applied MUST have the ``http`` trait applied.
     * - :ref:`httpError <httpError-trait>`
       - A ``client`` error has a default status code of ``400``, and a
         ``server`` error has a default status code of ``500``. The
diff --git a/docs/source-1.0/spec/core/behavior-traits.rst b/docs/source-1.0/spec/core/behavior-traits.rst
index 9aa6966d1..c1b0d1e45 100644
--- a/docs/source-1.0/spec/core/behavior-traits.rst
+++ b/docs/source-1.0/spec/core/behavior-traits.rst
@@ -214,7 +214,8 @@ The ``paginated`` trait is a structure that contains the following members:
       - ``string``
       - The name of an operation input member that limits the maximum number
         of results to include in the operation output. This input member
-        SHOULD NOT be required and MUST target an integer shape.
+        SHOULD NOT be required and SHOULD target an integer shape. It can, but
+        SHOULD NOT target a byte, short, or long shape.
 
         .. warning::
 
diff --git a/docs/source-1.0/spec/core/http-traits.rst b/docs/source-1.0/spec/core/http-traits.rst
index b6ae367a1..61d2368e9 100644
--- a/docs/source-1.0/spec/core/http-traits.rst
+++ b/docs/source-1.0/spec/core/http-traits.rst
@@ -707,13 +707,9 @@ the body of the response.
 Summary
     Binds a single structure member to the body of an HTTP message.
 Trait selector
-    .. code-block:: none
-
-        structure > :test(member > :test(string, blob, structure, union, document, list, set, map))
+    ``structure > member``
 
-    The ``httpPayload`` trait can be applied to ``structure`` members that
-    target a ``string``, ``blob``, ``structure``, ``union``, ``document``,
-    ``set``, ``map``, or ``list``.
+    *Any structure member*
 Value type
     Annotation trait.
 Conflicts with
@@ -777,10 +773,9 @@ or :ref:`httpPrefixHeaders-trait`.
 
 #. When a string or blob member is referenced, the raw value is serialized
    as the body of the message.
-#. When a :ref:`structure <structure>`, :ref:`union <union>`, :ref:`list <list>`,
-   :ref:`set <set>`, :ref:`map <map>`, or document type is targeted,
-   the shape value is serialized as a :ref:`protocol-specific <protocolDefinition-trait>`
-   document that is sent as the body of the message.
+#. When any other type of member is referenced, the shape value is serialized
+   as a :ref:`protocol-specific <protocolDefinition-trait>` value that is sent
+   as the body of the message.
 
 
 .. smithy-trait:: smithy.api#httpPrefixHeaders
diff --git a/docs/source-1.0/spec/core/model.rst b/docs/source-1.0/spec/core/model.rst
index 2cf4c88ea..bd5c4e6ae 100644
--- a/docs/source-1.0/spec/core/model.rst
+++ b/docs/source-1.0/spec/core/model.rst
@@ -334,11 +334,12 @@ Simple shapes
     * - bigDecimal
       - Arbitrary precision signed decimal number
     * - timestamp
-      - Represents an instant in time with no UTC offset or timezone. The
-        serialization of a timestamp is an implementation detail that is
-        determined by a :ref:`protocol <protocolDefinition-trait>` and
-        MUST NOT have any effect on the types exposed by tooling to
-        represent a timestamp value.
+      - A timestamp represents an instant in time in the proleptic Gregorian
+        calendar, independent of local times or timezones. Timestamps support
+        an allowable date range between midnight January 1, 0001 CE to
+        23:59:59.999 on December 31, 9999 CE, with a temporal resolution of 1
+        millisecond. This resolution and range ensures broad support across
+        programming languages and guarantees compatibility with :rfc:`3339`.
     * - document
       - Represents protocol-agnostic open content that functions as a kind of
         "any" type. Document types are represented by a JSON-like data model
diff --git a/docs/source-1.0/spec/core/protocol-traits.rst b/docs/source-1.0/spec/core/protocol-traits.rst
index 449e48a8d..e420498b0 100644
--- a/docs/source-1.0/spec/core/protocol-traits.rst
+++ b/docs/source-1.0/spec/core/protocol-traits.rst
@@ -302,19 +302,24 @@ Smithy defines the following built-in timestamp formats:
       - Description
     * - date-time
       - Date time as defined by the ``date-time`` production in
-        `RFC3339 section 5.6 <https://www.rfc-editor.org/rfc/rfc3339#section-5.6>`_
-        with optional fractional precision but no UTC offset (for example,
-        ``1985-04-12T23:20:50.52Z``).
-        *However*, offsets are parsed gracefully, but the datetime is normalized
-        to an offset of zero by converting to UTC.
+        :rfc:`3339#section-5.6` with optional millisecond precision but no
+        UTC offset (for example, ``1985-04-12T23:20:50.520Z``). Values that
+        are more granular than millisecond precision SHOULD be truncated to
+        fit millisecond precision. Deserializers SHOULD parse ``date-time``
+        values that contain offsets gracefully by normalizing them to UTC.
     * - http-date
       - An HTTP date as defined by the ``IMF-fixdate`` production in
         :rfc:`7231#section-7.1.1.1` (for example,
-        ``Tue, 29 Apr 2014 18:30:38 GMT``).
+        ``Tue, 29 Apr 2014 18:30:38 GMT``). A deserializer that encounters an
+        ``http-date`` timestamp with fractional precision SHOULD fail to
+        deserialize the value (for example, an HTTP server SHOULD return a 400
+        status code).
     * - epoch-seconds
       - Also known as Unix time, the number of seconds that have elapsed since
         00:00:00 Coordinated Universal Time (UTC), Thursday, 1 January 1970,
-        with optional fractional precision (for example, ``1515531081.1234``).
+        with optional millisecond precision (for example, ``1515531081.123``).
+        Values that are more granular than millisecond precision SHOULD be
+        truncated to fit millisecond precision.
 
 .. rubric:: Resolving timestamp formats
 
diff --git a/docs/source-2.0/aws/aws-core.rst b/docs/source-2.0/aws/aws-core.rst
index 5e082f881..2458e2f28 100644
--- a/docs/source-2.0/aws/aws-core.rst
+++ b/docs/source-2.0/aws/aws-core.rst
@@ -25,10 +25,12 @@ Value type
     * :ref:`service-cloudformation-name`
     * :ref:`service-arn-namespace`
     * :ref:`service-cloudtrail-event-source`
+    * :ref:`service-doc-id`
     * :ref:`service-endpoint-prefix`
 
 The following example defines an AWS service that uses the default values of
-``cloudFormationService``, ``arnNamespace``, and ``cloudTrailEventSource``:
+``cloudFormationService``, ``arnNamespace``, ``cloudTrailEventSource`` and
+``docId``:
 
 .. code-block:: smithy
 
@@ -58,6 +60,7 @@ The following example provides explicit values for all properties:
         cloudFormationName: "FooBaz"
         arnNamespace: "myservice"
         cloudTrailEventSource: "myservice.amazon.aws"
+        docId: "some-value-2018-03-17"
         endpointPrefix: "my-endpoint"
     )
     service FooBaz {
@@ -194,6 +197,32 @@ Amazon CloudWatch is ``monitoring.amazonaws.com``. Such services will
 need to explicitly configure the ``cloudTrailEventSource`` setting.
 
 
+.. _service-doc-id:
+
+``docId``
+=========
+
+The ``docId`` property is a ``string`` value that is used to implement linking
+between service and SDK documentation for AWS services.
+
+This will default to the ``sdkId`` value in lower case followed by the service
+``version`` property, separated by dashes. For the example below, the value
+for this property would default to ``some-value-2018-03-17``.
+
+.. code-block:: smithy
+
+    $version: "2"
+
+    namespace aws.fooBaz
+
+    use aws.api#service
+
+    @service(sdkId: "Some Value")
+    service FooBaz {
+        version: "2018-03-17"
+    }
+
+
 .. _service-endpoint-prefix:
 
 ``endpointPrefix``
diff --git a/docs/source-2.0/aws/protocols/aws-restjson1-protocol.rst b/docs/source-2.0/aws/protocols/aws-restjson1-protocol.rst
index a383077c1..00ac52cfc 100644
--- a/docs/source-2.0/aws/protocols/aws-restjson1-protocol.rst
+++ b/docs/source-2.0/aws/protocols/aws-restjson1-protocol.rst
@@ -146,9 +146,8 @@ that affect serialization:
       - Binds a top-level operation input structure member to a label in
         the hostPrefix of an endpoint trait.
     * - :ref:`http <http-trait>`
-      - Configures the HTTP bindings of an operation. An operation that
-        does not define the ``http`` trait is ineligible for use with
-        this protocol.
+      - Configures the HTTP bindings of an operation. An operation bound to a
+        service with this protocol applied MUST have the ``http`` trait applied.
     * - :ref:`httpError <httpError-trait>`
       - A ``client`` error has a default status code of ``400``, and a
         ``server`` error has a default status code of ``500``. The
diff --git a/docs/source-2.0/aws/protocols/aws-restxml-protocol.rst b/docs/source-2.0/aws/protocols/aws-restxml-protocol.rst
index cb32bee0a..e266fc653 100644
--- a/docs/source-2.0/aws/protocols/aws-restxml-protocol.rst
+++ b/docs/source-2.0/aws/protocols/aws-restxml-protocol.rst
@@ -141,9 +141,8 @@ that affect serialization:
       - Binds a top-level operation input structure member to a label in
         the hostPrefix of an endpoint trait.
     * - :ref:`http <http-trait>`
-      - Configures the HTTP bindings of an operation. An operation that
-        does not define the ``http`` trait is ineligible for use with
-        this protocol.
+      - Configures the HTTP bindings of an operation. An operation bound to a
+        service with this protocol applied MUST have the ``http`` trait applied.
     * - :ref:`httpError <httpError-trait>`
       - A ``client`` error has a default status code of ``400``, and a
         ``server`` error has a default status code of ``500``. The
diff --git a/docs/source-2.0/spec/behavior-traits.rst b/docs/source-2.0/spec/behavior-traits.rst
index b885845b5..7ada59fea 100644
--- a/docs/source-2.0/spec/behavior-traits.rst
+++ b/docs/source-2.0/spec/behavior-traits.rst
@@ -225,7 +225,8 @@ The ``paginated`` trait is a structure that contains the following members:
       - ``string``
       - The name of an operation input member that limits the maximum number
         of results to include in the operation output. This input member
-        SHOULD NOT be required and MUST target an integer shape.
+        SHOULD NOT be required and SHOULD target an integer shape. It can, but
+        SHOULD NOT target a byte, short, or long shape.
 
         .. warning::
 
diff --git a/docs/source-2.0/spec/http-bindings.rst b/docs/source-2.0/spec/http-bindings.rst
index aa8ce2f2d..2748eb2cd 100644
--- a/docs/source-2.0/spec/http-bindings.rst
+++ b/docs/source-2.0/spec/http-bindings.rst
@@ -709,14 +709,9 @@ the body of the response.
 Summary
     Binds a single structure member to the body of an HTTP message.
 Trait selector
-    .. code-block:: none
-
-        structure > :test(member > :test(string, blob, structure, union, document, list, map))
-
-    The ``httpPayload`` trait can be applied to ``structure`` members that
-    target a ``string``, ``blob``, ``structure``, ``union``, ``document``,
-    ``map``, or ``list``.
+    ``structure > member``
 
+    *Any structure member*
 Value type
     Annotation trait.
 Conflicts with
@@ -781,9 +776,8 @@ or :ref:`httpPrefixHeaders-trait`.
 
 #. When a string or blob member is referenced, the raw value is serialized
    as the body of the message.
-#. When a :ref:`structure <structure>`, :ref:`union <union>`, :ref:`list <list>`,
-   :ref:`map <map>`, or document type is targeted, the shape value is serialized
-   as a :ref:`protocol-specific <protocolDefinition-trait>` document that is sent
+#. When any other type of member is referenced, the shape value is serialized
+   as a :ref:`protocol-specific <protocolDefinition-trait>` value that is sent
    as the body of the message.
 
 
diff --git a/docs/source-2.0/spec/protocol-traits.rst b/docs/source-2.0/spec/protocol-traits.rst
index ebe284fa7..2a8bdbe91 100644
--- a/docs/source-2.0/spec/protocol-traits.rst
+++ b/docs/source-2.0/spec/protocol-traits.rst
@@ -218,19 +218,24 @@ Smithy defines the following built-in timestamp formats:
       - Description
     * - date-time
       - Date time as defined by the ``date-time`` production in
-        :rfc:`3339#section-5.6`
-        with optional fractional precision but no UTC offset (for example,
-        ``1985-04-12T23:20:50.52Z``).
-        *However*, offsets are parsed gracefully, but the datetime is normalized
-        to an offset of zero by converting to UTC.
+        :rfc:`3339#section-5.6` with optional millisecond precision but no
+        UTC offset (for example, ``1985-04-12T23:20:50.520Z``). Values that
+        are more granular than millisecond precision SHOULD be truncated to
+        fit millisecond precision. Deserializers SHOULD parse ``date-time``
+        values that contain offsets gracefully by normalizing them to UTC.
     * - http-date
       - An HTTP date as defined by the ``IMF-fixdate`` production in
         :rfc:`7231#section-7.1.1.1` (for example,
-        ``Tue, 29 Apr 2014 18:30:38 GMT``).
+        ``Tue, 29 Apr 2014 18:30:38 GMT``). A deserializer that encounters an
+        ``http-date`` timestamp with fractional precision SHOULD fail to
+        deserialize the value (for example, an HTTP server SHOULD return a 400
+        status code).
     * - epoch-seconds
       - Also known as Unix time, the number of seconds that have elapsed since
         00:00:00 Coordinated Universal Time (UTC), Thursday, 1 January 1970,
-        with optional fractional precision (for example, ``1515531081.1234``).
+        with optional millisecond precision (for example, ``1515531081.123``).
+        Values that are more granular than millisecond precision SHOULD be
+        truncated to fit millisecond precision.
 
 .. rubric:: Resolving timestamp formats
 
diff --git a/docs/source-2.0/spec/simple-types.rst b/docs/source-2.0/spec/simple-types.rst
index c6bdb1b7e..9396a30d9 100644
--- a/docs/source-2.0/spec/simple-types.rst
+++ b/docs/source-2.0/spec/simple-types.rst
@@ -144,17 +144,37 @@ A bigDecimal is an arbitrary precision signed decimal number.
 timestamp
 =========
 
-A timestamp represents an instant in time with no UTC offset or timezone.
-The serialization of a timestamp is an implementation detail that is
-determined by a :ref:`protocol <protocolDefinition-trait>` and MUST NOT
-have any effect on the types exposed by tooling to represent a timestamp
-value.
+A timestamp represents an instant in time in the proleptic Gregorian calendar,
+independent of local times or timezones. Timestamps support an allowable date
+range between midnight January 1, 0001 CE to 23:59:59.999 on
+December 31, 9999 CE, with a temporal resolution of 1 millisecond. This
+resolution and range ensures broad support across programming languages and
+guarantees compatibility with :rfc:`3339`.
 
 .. code-block:: smithy
 
     timestamp MyTimestamp
 
 
+Timestamp serialization and deserialization
+-------------------------------------------
+
+The serialization format of a timestamp is an implementation detail that is
+determined by a :ref:`protocol <protocolDefinition-trait>` and or
+:ref:`timestampFormat-trait`. The format of a timestamp MUST NOT have any
+effect on the types exposed by tooling to represent a timestamp value.
+
+Protocols and ``timestampFormat`` traits MAY support temporal resolutions
+other than 1 millisecond. For example, the ``http-date`` timestamp format
+supports only seconds and forbids fractional precision. Modelers need to be
+aware of these limitations when defining timestamps to avoid an unintended
+loss of precision.
+
+The use of timestamps outside the allowable range risk not interoperating
+correctly across Smithy implementations; deserializers that encounter
+timestamps outside the allowable range SHOULD fail to deserialize the value.
+
+
 .. _document:
 
 document
diff --git a/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/ServiceTrait.java b/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/ServiceTrait.java
index 705abc16f..7d4c43afe 100644
--- a/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/ServiceTrait.java
+++ b/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/ServiceTrait.java
@@ -20,9 +20,11 @@ import java.util.Objects;
 import java.util.Optional;
 import java.util.logging.Logger;
 import software.amazon.smithy.model.SourceException;
+import software.amazon.smithy.model.node.ExpectationNotMetException;
 import software.amazon.smithy.model.node.Node;
 import software.amazon.smithy.model.node.ObjectNode;
 import software.amazon.smithy.model.node.StringNode;
+import software.amazon.smithy.model.shapes.ServiceShape;
 import software.amazon.smithy.model.shapes.ShapeId;
 import software.amazon.smithy.model.traits.AbstractTrait;
 import software.amazon.smithy.model.traits.AbstractTraitBuilder;
@@ -42,6 +44,7 @@ public final class ServiceTrait extends AbstractTrait implements ToSmithyBuilder
     private final String arnNamespace;
     private final String sdkId;
     private final String cloudTrailEventSource;
+    private final String docId;
     private final String endpointPrefix;
 
     private ServiceTrait(Builder builder) {
@@ -51,6 +54,7 @@ public final class ServiceTrait extends AbstractTrait implements ToSmithyBuilder
         this.cloudFormationName = SmithyBuilder.requiredState("cloudFormationName", builder.cloudFormationName);
         this.cloudTrailEventSource = SmithyBuilder.requiredState(
                 "cloudTrailEventSource", builder.cloudTrailEventSource);
+        this.docId = builder.docId;
         this.endpointPrefix = SmithyBuilder.requiredState("endpointPrefix", builder.endpointPrefix);
     }
 
@@ -77,6 +81,8 @@ public final class ServiceTrait extends AbstractTrait implements ToSmithyBuilder
                     .ifPresent(builder::cloudFormationName);
             objectNode.getStringMember("cloudTrailEventSource").map(StringNode::getValue)
                     .ifPresent(builder::cloudTrailEventSource);
+            objectNode.getStringMember("docId").map(StringNode::getValue)
+                    .ifPresent(builder::docId);
             objectNode.getStringMember("endpointPrefix")
                     .map(StringNode::getValue)
                     .ifPresent(builder::endpointPrefix);
@@ -140,6 +146,34 @@ public final class ServiceTrait extends AbstractTrait implements ToSmithyBuilder
         return cloudTrailEventSource;
     }
 
+    /**
+     * Resolves the doc id value for the service.
+     *
+     * <p> When value on trait is not set, this method defaults to the lower
+     * cased value of the sdkId followed by the service version, separated by
+     * dashes.
+     *
+     * @param serviceShape the shape which this trait targets
+     * @return Returns the documentation identifier value for the service name.
+     * @throws ExpectationNotMetException if the shape is not the target of this trait.
+     */
+    public String resolveDocId(ServiceShape serviceShape) {
+        return getDocId().orElseGet(() -> buildDefaultDocId(serviceShape));
+    }
+
+    protected Optional<String> getDocId() {
+        return Optional.ofNullable(docId);
+    }
+
+    private String buildDefaultDocId(ServiceShape serviceShape) {
+        if (!serviceShape.expectTrait(ServiceTrait.class).equals(this)) {
+            throw new ExpectationNotMetException(String.format(
+                    "Provided service shape `%s` is not the target of this trait.", serviceShape.getId()), this);
+        }
+
+        return sdkId.replace(" ", "-").toLowerCase(Locale.US) + "-" + serviceShape.getVersion();
+    }
+
     /**
      * Returns the endpoint prefix for the service.
      *
@@ -167,6 +201,7 @@ public final class ServiceTrait extends AbstractTrait implements ToSmithyBuilder
                 .cloudFormationName(cloudFormationName)
                 .arnNamespace(arnNamespace)
                 .cloudTrailEventSource(cloudTrailEventSource)
+                .docId(docId)
                 .endpointPrefix(endpointPrefix);
     }
 
@@ -178,6 +213,7 @@ public final class ServiceTrait extends AbstractTrait implements ToSmithyBuilder
                 .withMember("arnNamespace", Node.from(getArnNamespace()))
                 .withMember("cloudFormationName", Node.from(getCloudFormationName()))
                 .withMember("cloudTrailEventSource", Node.from(getCloudTrailEventSource()))
+                .withOptionalMember("docId", getDocId().map(Node::from))
                 .withMember("endpointPrefix", Node.from(getEndpointPrefix()))
                 .build();
     }
@@ -196,6 +232,9 @@ public final class ServiceTrait extends AbstractTrait implements ToSmithyBuilder
                     && arnNamespace.equals(os.arnNamespace)
                     && cloudFormationName.equals(os.cloudFormationName)
                     && cloudTrailEventSource.equals(os.cloudTrailEventSource)
+                    && docId == null
+                        ? os.docId == null
+                        : docId.equals(os.docId)
                     && endpointPrefix.equals(os.endpointPrefix);
         }
     }
@@ -203,7 +242,7 @@ public final class ServiceTrait extends AbstractTrait implements ToSmithyBuilder
     @Override
     public int hashCode() {
         return Objects.hash(toShapeId(), sdkId, arnNamespace, cloudFormationName,
-                            cloudTrailEventSource, endpointPrefix);
+                            cloudTrailEventSource, docId, endpointPrefix);
     }
 
     /** Builder for {@link ServiceTrait}. */
@@ -212,6 +251,7 @@ public final class ServiceTrait extends AbstractTrait implements ToSmithyBuilder
         private String cloudFormationName;
         private String arnNamespace;
         private String cloudTrailEventSource;
+        private String docId;
         private String endpointPrefix;
 
         private Builder() {}
@@ -236,7 +276,7 @@ public final class ServiceTrait extends AbstractTrait implements ToSmithyBuilder
             }
 
             if (cloudTrailEventSource == null) {
-                cloudTrailEventSource = arnNamespace + ".amazonaws.com";
+                cloudTrailEventSource(arnNamespace + ".amazonaws.com");
             }
 
             if (endpointPrefix == null) {
@@ -296,6 +336,17 @@ public final class ServiceTrait extends AbstractTrait implements ToSmithyBuilder
             return this;
         }
 
+        /**
+         * Set the documentation identifier for the service.
+         *
+         * @param docId documentation identifier for the service.
+         * @return Returns the builder.
+         */
+        public Builder docId(String docId) {
+            this.docId = docId;
+            return this;
+        }
+
         /**
          * Set the endpoint prefix used to construct client endpoints.
          *
diff --git a/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.api.smithy b/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.api.smithy
index a48ea0d63..baf9cb499 100644
--- a/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.api.smithy
+++ b/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.api.smithy
@@ -186,6 +186,12 @@ structure service {
     /// to the `arnNamespace` plus `.amazonaws.com`.
     cloudTrailEventSource: String
 
+    /// The `docId` property is a string value that defines the identifier
+    /// used to implemention linking between service and SDK documentation for
+    /// AWS services. If not specified, this value defaults to the `sdkId` in
+    /// lower case plus the service `version` property, separated by dashes.
+    docId: String
+
     /// The `endpointPrefix` property is a string value that identifies which
     /// endpoint in a given region should be used to connect to the service.
     /// For example, most services in the AWS standard partition have endpoints
diff --git a/smithy-build/src/main/java/software/amazon/smithy/build/SmithyBuild.java b/smithy-build/src/main/java/software/amazon/smithy/build/SmithyBuild.java
index 136b03f77..6c6429eca 100644
--- a/smithy-build/src/main/java/software/amazon/smithy/build/SmithyBuild.java
+++ b/smithy-build/src/main/java/software/amazon/smithy/build/SmithyBuild.java
@@ -15,8 +15,6 @@
 
 package software.amazon.smithy.build;
 
-import java.io.IOException;
-import java.io.UncheckedIOException;
 import java.nio.file.FileSystems;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -199,23 +197,10 @@ public final class SmithyBuild {
     //
     // Ignores and logs when an unsupported model file is encountered.
     private void addSource(Path path) {
-        try {
-            if (Files.isDirectory(path)) {
-                // Pre-emptively crawl the given models to filter them up front into a flat, file-only, list.
-                Files.list(path).filter(Files::isRegularFile).forEach(this::addSourceFile);
-            } else {
-                addSourceFile(path);
-            }
-        } catch (IOException e) {
-            throw new UncheckedIOException(e);
-        }
-    }
-
-    private void addSourceFile(Path file) {
-        if (!VALID_MODELS.matches(file.getFileName())) {
-            LOGGER.warning("Omitting unsupported Smithy model file from model sources: " + file);
+        if (Files.isRegularFile(path) && !VALID_MODELS.matches(path.getFileName())) {
+            LOGGER.warning("Omitting unsupported Smithy model file from model sources: " + path);
         } else {
-            sources.add(file.toAbsolutePath());
+            sources.add(path.toAbsolutePath());
         }
     }
 
diff --git a/smithy-build/src/main/java/software/amazon/smithy/build/plugins/SourcesPlugin.java b/smithy-build/src/main/java/software/amazon/smithy/build/plugins/SourcesPlugin.java
index 603aa4c2f..6fe3e46b4 100644
--- a/smithy-build/src/main/java/software/amazon/smithy/build/plugins/SourcesPlugin.java
+++ b/smithy-build/src/main/java/software/amazon/smithy/build/plugins/SourcesPlugin.java
@@ -26,6 +26,7 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.logging.Logger;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import software.amazon.smithy.build.FileManifest;
 import software.amazon.smithy.build.PluginContext;
 import software.amazon.smithy.build.SmithyBuildPlugin;
@@ -108,10 +109,11 @@ public final class SourcesPlugin implements SmithyBuildPlugin {
     private static void copyDirectory(List<String> names, FileManifest manifest, Path root, Path current) {
         try {
             if (Files.isDirectory(current)) {
-                Files.list(current)
-                        .filter(p -> !p.equals(current))
-                        .filter(p -> Files.isDirectory(p) || Files.isRegularFile(p))
-                        .forEach(p -> copyDirectory(names, manifest, root, p));
+                try (Stream<Path> fileList = Files.list(current)) {
+                    fileList.filter(p -> !p.equals(current))
+                            .filter(p -> Files.isDirectory(p) || Files.isRegularFile(p))
+                            .forEach(p -> copyDirectory(names, manifest, root, p));
+                }
             } else if (Files.isRegularFile(current)) {
                 if (current.toString().endsWith(".jar")) {
                     // Account for just a simple file vs recursing into directories.
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/pattern/SmithyPattern.java b/smithy-model/src/main/java/software/amazon/smithy/model/pattern/SmithyPattern.java
index f7ed50f98..82bcb7716 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/pattern/SmithyPattern.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/pattern/SmithyPattern.java
@@ -235,10 +235,14 @@ public class SmithyPattern {
         private final Type segmentType;
 
         public Segment(String content, Type segmentType) {
+            this(content, segmentType, null);
+        }
+
+        public Segment(String content, Type segmentType, Integer offset) {
             this.content = Objects.requireNonNull(content);
             this.segmentType = segmentType;
 
-            checkForInvalidContents();
+            checkForInvalidContents(offset);
 
             if (segmentType == Type.GREEDY_LABEL) {
                 asString = "{" + content + "+}";
@@ -249,19 +253,25 @@ public class SmithyPattern {
             }
         }
 
-        private void checkForInvalidContents() {
+        private void checkForInvalidContents(Integer offset) {
+            String offsetString = "";
+            if (offset != null) {
+                offsetString += " at index " + offset;
+            }
             if (segmentType == Type.LITERAL) {
                 if (content.isEmpty()) {
-                    throw new InvalidPatternException("Segments must not be empty");
+                    throw new InvalidPatternException("Segments must not be empty" + offsetString);
                 } else if (content.contains("{") || content.contains("}")) {
                     throw new InvalidPatternException(
-                            "Literal segments must not contain `{` or `}` characters. Found segment `" + content + "`");
+                            "Literal segments must not contain `{` or `}` characters. Found segment `"
+                                    + content + "`" + offsetString);
                 }
             } else if (content.isEmpty()) {
-                throw new InvalidPatternException("Empty label declaration in pattern.");
+                throw new InvalidPatternException("Empty label declaration in pattern" + offsetString + ".");
             } else if (!ShapeId.isValidIdentifier(content)) {
                 throw new InvalidPatternException(
-                        "Invalid label name in pattern: '" + content + "'. Labels must contain value identifiers.");
+                        "Invalid label name in pattern: '" + content + "'" + offsetString
+                                + ". Labels must contain value identifiers.");
             }
         }
 
@@ -269,7 +279,7 @@ public class SmithyPattern {
          * Parse a segment from the given offset.
          *
          * @param content Content of the segment.
-         * @param offset Character offset where the segment starts.
+         * @param offset Character offset where the segment starts in the containing pattern.
          * @return Returns the created segment.
          * @throws InvalidPatternException if the segment is invalid.
          */
@@ -279,9 +289,9 @@ public class SmithyPattern {
                 content = labelType == Type.GREEDY_LABEL
                           ? content.substring(1, content.length() - 2)
                           : content.substring(1, content.length() - 1);
-                return new Segment(content, labelType);
+                return new Segment(content, labelType, offset);
             } else {
-                return new Segment(content, Type.LITERAL);
+                return new Segment(content, Type.LITERAL, offset);
             }
         }
 
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/pattern/UriPattern.java b/smithy-model/src/main/java/software/amazon/smithy/model/pattern/UriPattern.java
index 26be0d4fa..3df67138b 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/pattern/UriPattern.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/pattern/UriPattern.java
@@ -78,7 +78,8 @@ public final class UriPattern extends SmithyPattern {
         for (int i = 1; i < unparsedSegments.length; i++) {
             String segment = unparsedSegments[i];
             segments.add(Segment.parse(segment, offset));
-            offset += segment.length();
+            // Add one to account for `/`
+            offset += segment.length() + 1;
         }
 
         Map<String, String> queryLiterals = new LinkedHashMap<>();
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java
index 8404fb9de..08ae9d87f 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java
@@ -477,12 +477,17 @@ public final class SmithyIdlModelSerializer {
 
         private void applyIntroducedTraits(Collection<MemberShape> members) {
             for (MemberShape member : members) {
+                Map<ShapeId, Trait> introducedTraits = new LinkedHashMap<>(member.getIntroducedTraits());
+
+                // The @enumValue trait is serialized using the `=` IDL syntax, so remove it here.
+                introducedTraits.remove(EnumValueTrait.ID);
+
                 // Use short form for a single trait, and block form for multiple traits.
-                if (member.getIntroducedTraits().size() == 1) {
+                if (introducedTraits.size() == 1) {
                     codeWriter.writeInline("apply $I ", member.getId());
                     serializeTraits(member.getIntroducedTraits(), TraitFeature.NO_SPECIAL_DOCS_SYNTAX);
                     codeWriter.write("");
-                } else if (!member.getIntroducedTraits().isEmpty()) {
+                } else if (!introducedTraits.isEmpty()) {
                     codeWriter.openBlock("apply $I {", "}", member.getId(), () -> {
                         // Only serialize local traits, and don't use special documentation syntax here.
                         serializeTraits(member.getIntroducedTraits(), TraitFeature.NO_SPECIAL_DOCS_SYNTAX);
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/HttpBindingsMissingValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/HttpBindingsMissingValidator.java
index a51072c05..fabe8dcfc 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/HttpBindingsMissingValidator.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/HttpBindingsMissingValidator.java
@@ -24,12 +24,14 @@ import software.amazon.smithy.model.knowledge.ServiceIndex;
 import software.amazon.smithy.model.knowledge.TopDownIndex;
 import software.amazon.smithy.model.shapes.OperationShape;
 import software.amazon.smithy.model.shapes.ServiceShape;
+import software.amazon.smithy.model.shapes.ShapeId;
 import software.amazon.smithy.model.traits.HttpTrait;
 import software.amazon.smithy.model.traits.ProtocolDefinitionTrait;
 import software.amazon.smithy.model.validation.AbstractValidator;
 import software.amazon.smithy.model.validation.Severity;
 import software.amazon.smithy.model.validation.ValidationEvent;
 import software.amazon.smithy.utils.ListUtils;
+import software.amazon.smithy.utils.Pair;
 
 /**
  * Validates that if any operation in a service uses the http trait,
@@ -50,37 +52,46 @@ public final class HttpBindingsMissingValidator extends AbstractValidator {
 
     private List<ValidationEvent> validateService(TopDownIndex topDownIndex, Model model, ServiceShape service) {
         Set<OperationShape> operations = topDownIndex.getContainedOperations(service);
+
+        ShapeId protocolTraitId = protocolWithBindings(service, model);
+        if (protocolTraitId != null) {
+            // Emit ERROR events if any of the protocols for the service support HTTP traits.
+            String reason = "The `" + protocolTraitId + "` protocol requires all";
+            return validateOperations(service, operations, Severity.ERROR, reason);
+        }
+
         // Stop early if there are no bindings at all in the model for any operation.
         if (operations.stream().noneMatch(this::hasBindings)) {
             return ListUtils.of();
         }
 
-        Severity severity = determineSeverity(service, model);
-
-        return operations.stream()
-                .filter(shape -> !shape.getTrait(HttpTrait.class).isPresent())
-                .map(shape -> createEvent(severity, service, shape))
-                .collect(Collectors.toList());
+        return validateOperations(service, operations, Severity.WARNING, "One or more");
     }
 
-    // Only emit ERROR events if any of the protocols for the service support HTTP traits
-    private Severity determineSeverity(ServiceShape service, Model model) {
-        if (ServiceIndex.of(model).getProtocols(service).values().stream()
+    private ShapeId protocolWithBindings(ServiceShape service, Model model) {
+        return ServiceIndex.of(model).getProtocols(service).values().stream()
                 .map(t -> model.expectShape(t.toShapeId()))
-                .map(s -> s.expectTrait(ProtocolDefinitionTrait.class))
-                .anyMatch(pdt -> pdt.getTraits().contains(HttpTrait.ID))) {
-            return Severity.ERROR;
-        }
-        return Severity.WARNING;
+                .map(s -> Pair.of(s.getId(), s.expectTrait(ProtocolDefinitionTrait.class)))
+                .filter(pair -> pair.getRight().getTraits().contains(HttpTrait.ID))
+                .map(Pair::getLeft)
+                .findFirst().orElse(null);
     }
 
     private boolean hasBindings(OperationShape op) {
         return op.getTrait(HttpTrait.class).isPresent();
     }
 
-    private ValidationEvent createEvent(Severity severity, ServiceShape service, OperationShape operation) {
-        return createEvent(severity, operation, operation.getSourceLocation(), String.format(
-                "One or more operations in the `%s` service define the `http` trait, but this "
-                + "operation is missing the `http` trait.", service.getId()));
+    private List<ValidationEvent> validateOperations(
+            ServiceShape service,
+            Set<OperationShape> operations,
+            Severity severity,
+            String reason
+    ) {
+        return operations.stream()
+                .filter(operation -> !operation.getTrait(HttpTrait.class).isPresent())
+                .map(operation -> createEvent(severity, operation, operation.getSourceLocation(),
+                        String.format("%s operations in the `%s` service define the `http` trait, but this "
+                                + "operation is missing the `http` trait.", reason, service.getId())))
+                .collect(Collectors.toList());
     }
 }
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/HttpMethodSemanticsValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/HttpMethodSemanticsValidator.java
index e34024072..69f38b383 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/HttpMethodSemanticsValidator.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/HttpMethodSemanticsValidator.java
@@ -30,6 +30,7 @@ import software.amazon.smithy.model.traits.ReadonlyTrait;
 import software.amazon.smithy.model.validation.AbstractValidator;
 import software.amazon.smithy.model.validation.ValidationEvent;
 import software.amazon.smithy.model.validation.ValidationUtils;
+import software.amazon.smithy.utils.ListUtils;
 import software.amazon.smithy.utils.MapUtils;
 
 /**
@@ -94,10 +95,19 @@ public final class HttpMethodSemanticsValidator extends AbstractValidator {
             HttpTrait trait
     ) {
         String method = trait.getMethod().toUpperCase(Locale.US);
-        List<ValidationEvent> events = new ArrayList<>();
 
+        // We don't have a standard method, so we can't validate it.
         if (!EXPECTED.containsKey(method)) {
-            return events;
+            return ListUtils.of();
+        }
+
+        List<ValidationEvent> events = new ArrayList<>();
+
+        // Emit a warning if the method is standard, but doesn't match standard casing.
+        if (!EXPECTED.containsKey(trait.getMethod())) {
+            events.add(warning(shape, trait, String.format(
+                    "This operation uses the `%s` method in the `http` trait, but expected `%s`.",
+                    trait.getMethod(), method)));
         }
 
         HttpMethodSemantics semantics = EXPECTED.get(method);
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/PaginatedTraitValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/PaginatedTraitValidator.java
index 08dbdf56e..51f6c0f3c 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/PaginatedTraitValidator.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/PaginatedTraitValidator.java
@@ -45,19 +45,21 @@ import software.amazon.smithy.utils.SetUtils;
  *      operation's properties are optionally merged with a service's.</li>
  *     <li>The items property, if set, must reference a list or map
  *      output member.</li>
- *     <li>The pageSize property, if set, must reference an optional integer
- *      input member.</li>
- *     <li>The inputToken property must reference an optional string input
- *      member.</li>
- *     <li>The outputToken property must reference an optional string output
- *      member.</li>
- *     <li>The pageSize property, if set, must reference an optional input
- *      member that targets an integer.</li>
+ *     <li>The pageSize property, if set, should reference an optional integer
+ *      input member. It may, but should not reference an optional byte, short,
+ *      or long.</li>
+ *     <li>The inputToken property should reference an optional string input
+ *      member. It may, but should not reference an optional map.</li>
+ *     <li>The outputToken property should reference an optional string output
+ *      member. It may, but should not reference an optional map.</li>
  * </ul>
  */
 public final class PaginatedTraitValidator extends AbstractValidator {
     private static final Set<ShapeType> ITEM_SHAPES = SetUtils.of(ShapeType.LIST, ShapeType.MAP);
-    private static final Set<ShapeType> PAGE_SHAPES = SetUtils.of(ShapeType.INTEGER);
+    private static final Set<ShapeType> PAGE_SHAPES = SetUtils.of(ShapeType.BYTE, ShapeType.INTEGER,
+            ShapeType.LONG, ShapeType.SHORT);
+    private static final Set<ShapeType> DANGER_PAGE_SHAPES = SetUtils.of(ShapeType.BYTE, ShapeType.LONG,
+            ShapeType.SHORT);
     private static final Set<ShapeType> TOKEN_SHAPES = SetUtils.of(ShapeType.STRING, ShapeType.MAP);
     private static final Set<ShapeType> DANGER_TOKEN_SHAPES = SetUtils.of(ShapeType.MAP);
     private static final Pattern PATH_PATTERN = Pattern.compile("\\.");
@@ -167,14 +169,14 @@ public final class PaginatedTraitValidator extends AbstractValidator {
                 events.add(error(operation, trait, String.format(
                         "%spaginated trait `%s` member `%s` targets a %s shape, but must target one of "
                                 + "the following: [%s]",
-                        prefix, validator.propertyName(), member.getId().getName(), target.getType(),
+                        prefix, validator.propertyName(), member.getId().getMember().get(), target.getType(),
                         ValidationUtils.tickedList(validator.validTargets()))));
             }
             if (validator.dangerTargets().contains(target.getType())) {
                 Set<ShapeType> preferredTargets = new TreeSet<>(validator.validTargets());
                 preferredTargets.removeAll(validator.dangerTargets());
                 String traitName = validator.propertyName();
-                String memberName = member.getId().getName();
+                String memberName = member.getId().getMember().get();
                 String targetType = target.getType().toString();
                 events.add(danger(operation, trait, String.format(
                             "%spaginated trait `%s` member `%s` targets a %s shape, but this is not recommended. "
@@ -319,6 +321,10 @@ public final class PaginatedTraitValidator extends AbstractValidator {
             return PAGE_SHAPES;
         }
 
+        Set<ShapeType> dangerTargets() {
+            return DANGER_PAGE_SHAPES;
+        }
+
         Optional<String> getMemberPath(OperationIndex opIndex, OperationShape operation, PaginatedTrait trait) {
             return trait.getPageSize();
         }
diff --git a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy
index 17dfc9e46..2fc4b2c42 100644
--- a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy
+++ b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy
@@ -832,7 +832,7 @@ string httpPrefixHeaders
 
 /// Binds a single structure member to the body of an HTTP request.
 @trait(
-    selector: "structure > :test(member > :test(string, blob, structure, union, document, list, map))",
+    selector: "structure > member",
     conflicts: [httpLabel, httpQuery, httpHeader, httpPrefixHeaders, httpResponseCode, httpQueryParams],
     structurallyExclusive: "member",
     breakingChanges: [{change: "presence"}]
diff --git a/smithy-syntax/src/main/java/software/amazon/smithy/syntax/Formatter.java b/smithy-syntax/src/main/java/software/amazon/smithy/syntax/Formatter.java
index caeef6d5b..6a13df5b6 100644
--- a/smithy-syntax/src/main/java/software/amazon/smithy/syntax/Formatter.java
+++ b/smithy-syntax/src/main/java/software/amazon/smithy/syntax/Formatter.java
@@ -17,10 +17,12 @@ package software.amazon.smithy.syntax;
 
 import com.opencastsoftware.prettier4j.Doc;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Optional;
 import java.util.function.Function;
+import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import software.amazon.smithy.model.loader.ModelSyntaxException;
 import software.amazon.smithy.model.shapes.ShapeId;
@@ -178,6 +180,7 @@ public final class Formatter {
                 case APPLY_STATEMENT:
                 case NODE_VALUE:
                 case NODE_KEYWORD:
+                case NODE_STRING_VALUE:
                 case SIMPLE_TYPE_NAME:
                 case ENUM_TYPE_NAME:
                 case AGGREGATE_TYPE_NAME:
@@ -408,12 +411,25 @@ public final class Formatter {
                 }
 
                 case NODE_OBJECT_KVP: {
+                    // Since text blocks span multiple lines, when they are the NODE_VALUE for NODE_OBJECT_KVP,
+                    // they have to be indented. Since we only format valid models, NODE_OBJECT_KVP is guaranteed to
+                    // have a NODE_VALUE child.
+                    TreeCursor nodeValue = cursor.getFirstChild(TreeType.NODE_VALUE);
+                    boolean isTextBlock = Optional.ofNullable(nodeValue.getFirstChild(TreeType.NODE_STRING_VALUE))
+                            .map(nodeString -> nodeString.getFirstChild(TreeType.TEXT_BLOCK))
+                            .isPresent();
+                    Doc nodeValueDoc = visit(nodeValue);
+                    if (isTextBlock) {
+                        nodeValueDoc = nodeValueDoc.indent(4);
+                    }
+
+
                     // Hoist awkward comments in the KVP *before* the KVP rather than between the values and colon.
                     // If there is an awkward comment before the TRAIT value, hoist it above the statement.
                     return skippedComments(cursor, false)
                             .append(visit(cursor.getFirstChild(TreeType.NODE_OBJECT_KEY)))
                             .append(Doc.text(": "))
-                            .append(visit(cursor.getFirstChild(TreeType.NODE_VALUE)));
+                            .append(nodeValueDoc);
                 }
 
                 case NODE_OBJECT_KEY: {
@@ -427,9 +443,17 @@ public final class Formatter {
                            : Doc.text(tree.concatTokens());
                 }
 
+                case TEXT_BLOCK: {
+                    // Dispersing the lines of the text block preserves any indentation applied from formatting parent
+                    // nodes.
+                    List<Doc> lines = Arrays.stream(tree.concatTokens().split(System.lineSeparator()))
+                            .map(String::trim)
+                            .map(Doc::text)
+                            .collect(Collectors.toList());
+                    return Doc.intersperse(Doc.line(), lines);
+                }
+
                 case TOKEN:
-                case TEXT_BLOCK:
-                case NODE_STRING_VALUE:
                 case QUOTED_TEXT:
                 case NUMBER:
                 case SHAPE_ID:
diff --git a/smithy-validation-model/build.gradle b/smithy-validation-model/build.gradle
index 83e6c9e43..6e1337756 100644
--- a/smithy-validation-model/build.gradle
+++ b/smithy-validation-model/build.gradle
@@ -27,3 +27,5 @@ ext {
 dependencies {
     implementation project(path: ":smithy-cli", configuration: "shadow")
 }
+
+tasks["sourcesJar"].dependsOn("smithyBuildJar")


Test patch:
diff --git a/smithy-aws-protocol-tests/build.gradle b/smithy-aws-protocol-tests/build.gradle
index 9992420e0..a4110ceb9 100644
--- a/smithy-aws-protocol-tests/build.gradle
+++ b/smithy-aws-protocol-tests/build.gradle
@@ -30,3 +30,5 @@ dependencies {
     implementation project(":smithy-aws-traits")
     api project(":smithy-validation-model")
 }
+
+tasks["sourcesJar"].dependsOn("smithyBuildJar")
diff --git a/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/ServiceTraitTest.java b/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/ServiceTraitTest.java
index 6009d0b6a..1d2e870c5 100644
--- a/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/ServiceTraitTest.java
+++ b/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/ServiceTraitTest.java
@@ -18,12 +18,14 @@ package software.amazon.smithy.aws.traits;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.instanceOf;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.util.Optional;
 import org.junit.jupiter.api.Test;
 import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.node.ExpectationNotMetException;
 import software.amazon.smithy.model.node.Node;
 import software.amazon.smithy.model.shapes.ServiceShape;
 import software.amazon.smithy.model.shapes.ShapeId;
@@ -47,12 +49,13 @@ public class ServiceTraitTest {
         assertThat(serviceTrait.getCloudTrailEventSource(), equalTo("foo.amazonaws.com"));
         assertThat(serviceTrait.getEndpointPrefix(), equalTo("foo"));
         assertThat(serviceTrait.toBuilder().build(), equalTo(serviceTrait));
+        assertFalse(serviceTrait.getDocId().isPresent());
     }
 
     @Test
     public void loadsTraitWithOptionalValues() {
         Node node = Node.parse("{\"sdkId\": \"Foo\", \"arnNamespace\": \"service\", \"cloudFormationName\": \"Baz\", "
-                        + "\"endpointPrefix\": \"endpoint-prefix\"}");
+                        + "\"endpointPrefix\": \"endpoint-prefix\", \"docId\": \"doc-id\"}");
         TraitFactory provider = TraitFactory.createServiceFactory();
         Optional<Trait> trait = provider.createTrait(ServiceTrait.ID, ShapeId.from("ns.foo#foo"), node);
 
@@ -63,6 +66,8 @@ public class ServiceTraitTest {
         assertThat(serviceTrait.getArnNamespace(), equalTo("service"));
         assertThat(serviceTrait.getCloudFormationName(), equalTo("Baz"));
         assertThat(serviceTrait.getEndpointPrefix(), equalTo("endpoint-prefix"));
+        assertTrue(serviceTrait.getDocId().isPresent());
+        assertThat(serviceTrait.getDocId().get(), equalTo("doc-id"));
     }
 
     @Test
@@ -82,6 +87,27 @@ public class ServiceTraitTest {
         assertThrows(IllegalStateException.class, () -> ServiceTrait.builder().build());
     }
 
+    @Test
+    public void requiresProperServiceShapeToResolveDocId() {
+        ServiceTrait trait = ServiceTrait.builder()
+                .sdkId("Foo SDK")
+                .arnNamespace("foo")
+                .cloudTrailEventSource("cloudTrailEventSource")
+                .cloudFormationName("AWS::Foo")
+                .build();
+        ServiceShape service = ServiceShape.builder()
+                .id("smithy.example#Foo")
+                .version("123")
+                .addTrait(trait)
+                .build();
+        ServiceShape anotherService = ServiceShape.builder()
+                .id("smithy.example#Bar")
+                .build();
+
+        assertThat(trait.resolveDocId(service), equalTo("foo-sdk-123"));
+        assertThrows(ExpectationNotMetException.class, () -> trait.resolveDocId(anotherService));
+    }
+
     @Test
     public void loadsFromModel() {
         Model result = Model.assembler()
@@ -90,13 +116,14 @@ public class ServiceTraitTest {
                 .assemble()
                 .unwrap();
         ServiceShape service = result
-                .expectShape(ShapeId.from("ns.foo#SomeService"))
-                .asServiceShape().get();
-        ServiceTrait trait = service.getTrait(ServiceTrait.class).get();
+                .expectShape(ShapeId.from("ns.foo#SomeService"), ServiceShape.class);
+        ServiceTrait trait = service.expectTrait(ServiceTrait.class);
 
         assertThat(trait.getSdkId(), equalTo("Some Value"));
         assertThat(trait.getCloudFormationName(), equalTo("SomeService"));
         assertThat(trait.getArnNamespace(), equalTo("service"));
         assertThat(trait.getEndpointPrefix(), equalTo("some-service"));
+        assertFalse(trait.getDocId().isPresent());
+        assertThat(trait.resolveDocId(service), equalTo("some-value-2018-03-17"));
     }
 }
diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/protocols/aws-protocols-do-not-support-list-set-map-payloads.errors b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/protocols/aws-protocols-do-not-support-list-set-map-payloads.errors
index 7342a67c1..f581a8f91 100644
--- a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/protocols/aws-protocols-do-not-support-list-set-map-payloads.errors
+++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/protocols/aws-protocols-do-not-support-list-set-map-payloads.errors
@@ -1,3 +1,6 @@
 [ERROR] smithy.example#InvalidBindingOperationInput$listBinding: AWS Protocols only support binding the following shape types to the payload: string, blob, structure, union, and document | ProtocolHttpPayload
 [ERROR] smithy.example#InvalidBindingOperationOutput$mapBinding: AWS Protocols only support binding the following shape types to the payload: string, blob, structure, union, and document | ProtocolHttpPayload
 [ERROR] smithy.example#InvalidBindingError$setBinding: AWS Protocols only support binding the following shape types to the payload: string, blob, structure, union, and document | ProtocolHttpPayload
+[ERROR] smithy.example#InvalidSimpleBindingOperationInput$booleanBinding: AWS Protocols only support binding the following shape types to the payload: string, blob, structure, union, and document | ProtocolHttpPayload
+[ERROR] smithy.example#InvalidSimpleBindingOperationOutput$integerBinding: AWS Protocols only support binding the following shape types to the payload: string, blob, structure, union, and document | ProtocolHttpPayload
+[ERROR] smithy.example#InvalidSimpleBindingError$doubleBinding: AWS Protocols only support binding the following shape types to the payload: string, blob, structure, union, and document | ProtocolHttpPayload
diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/protocols/aws-protocols-do-not-support-list-set-map-payloads.smithy b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/protocols/aws-protocols-do-not-support-list-set-map-payloads.smithy
index bc6dc913b..0659896e2 100644
--- a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/protocols/aws-protocols-do-not-support-list-set-map-payloads.smithy
+++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/protocols/aws-protocols-do-not-support-list-set-map-payloads.smithy
@@ -13,7 +13,10 @@ use smithy.api#httpPayload
 @restJson1
 service InvalidExample {
     version: "2020-12-29",
-    operations: [InvalidBindingOperation],
+    operations: [
+        InvalidBindingOperation,
+        InvalidSimpleBindingOperation
+    ],
 }
 
 @http(method: "POST", uri: "/invalid-payload")
@@ -41,6 +44,31 @@ structure InvalidBindingError {
     setBinding: StringSet
 }
 
+@http(method: "POST", uri: "/invalid-simple-payload")
+operation InvalidSimpleBindingOperation {
+    input: InvalidSimpleBindingOperationInput,
+    output: InvalidSimpleBindingOperationOutput,
+    errors: [InvalidSimpleBindingError],
+}
+
+@input
+structure InvalidSimpleBindingOperationInput {
+    @httpPayload
+    booleanBinding: Boolean,
+}
+
+@output
+structure InvalidSimpleBindingOperationOutput {
+    @httpPayload
+    integerBinding: Integer,
+}
+
+@error("client")
+structure InvalidSimpleBindingError {
+    @httpPayload
+    doubleBinding: Double
+}
+
 list StringList {
     member: String
 }
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/testrunner/SmithyTestCase.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/testrunner/SmithyTestCase.java
index cbf0c1f34..f18e36892 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/testrunner/SmithyTestCase.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/testrunner/SmithyTestCase.java
@@ -15,6 +15,9 @@
 
 package software.amazon.smithy.model.validation.testrunner;
 
+import static java.lang.String.format;
+
+import java.nio.file.Paths;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
@@ -164,16 +167,19 @@ public final class SmithyTestCase {
 
     private static List<ValidationEvent> loadExpectedEvents(String errorsFileLocation) {
         String contents = IoUtils.readUtf8File(errorsFileLocation);
+        String fileName = Objects.requireNonNull(Paths.get(errorsFileLocation).getFileName()).toString();
         return Arrays.stream(contents.split(System.lineSeparator()))
                 .filter(line -> !line.trim().isEmpty())
-                .map(SmithyTestCase::parseValidationEvent)
+                .map(line -> parseValidationEvent(line, fileName))
                 .collect(Collectors.toList());
     }
 
-    static ValidationEvent parseValidationEvent(String event) {
+    static ValidationEvent parseValidationEvent(String event, String fileName) {
         Matcher matcher = EVENT_PATTERN.matcher(event);
         if (!matcher.find()) {
-            throw new IllegalArgumentException("Invalid validation event: " + event);
+            throw new IllegalArgumentException(format("Invalid validation event in file `%s`, the following event did "
+                    + "not match the expected regular expression `%s`: %s",
+                    fileName, EVENT_PATTERN.pattern(), event));
         }
 
         // Construct a dummy source location since we don't validate it.
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/pattern/SmithyPatternTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/pattern/SmithyPatternTest.java
index 3ebbe4a90..eae5c47b7 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/pattern/SmithyPatternTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/pattern/SmithyPatternTest.java
@@ -99,7 +99,7 @@ public class SmithyPatternTest {
                     .build();
         });
 
-        assertThat(thrown.getMessage(), containsString("Segments must not be empty"));
+        assertThat(thrown.getMessage(), containsString("Segments must not be empty at index 1"));
     }
 
     @Test
@@ -158,14 +158,14 @@ public class SmithyPatternTest {
     @Test
     public void noEmptyLabels() {
         Throwable thrown = Assertions.assertThrows(InvalidPatternException.class, () -> {
-            String target = "/{}";
+            String target = "/a/{}";
             SmithyPattern.builder()
                     .segments(parser(target))
                     .pattern(target)
                     .build();
         });
 
-        assertThat(thrown.getMessage(), containsString("Empty label declaration"));
+        assertThat(thrown.getMessage(), containsString("Empty label declaration in pattern at index 2"));
     }
 
     @Test
@@ -179,6 +179,7 @@ public class SmithyPatternTest {
         });
 
         assertThat(thrown.getMessage(), containsString("Invalid label name"));
+        assertThat(thrown.getMessage(), containsString("at index 1"));
     }
 
     @Test
@@ -192,5 +193,6 @@ public class SmithyPatternTest {
         });
 
         assertThat(thrown.getMessage(), containsString("Literal segments must not contain"));
+        assertThat(thrown.getMessage(), containsString("at index 1"));
     }
 }
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializerTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializerTest.java
index 8aa756b55..62dedc3a6 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializerTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializerTest.java
@@ -243,4 +243,20 @@ public class SmithyIdlModelSerializerTest {
 
         assertThat(modelResult, equalTo(IoUtils.readUtf8Url(resource).replace("\r\n", "\n")));
     }
+
+    @Test
+    public void handlesEnumMixins() {
+        URL resource = getClass().getResource("idl-serialization/enum-mixin-input.smithy");
+        Model model = Model.assembler().addImport(resource).assemble().unwrap();
+        Map<Path, String> serialized = SmithyIdlModelSerializer.builder().build().serialize(model);
+
+        if (serialized.size() != 1) {
+            throw new RuntimeException("Exactly one smithy file should be output for generated tests.");
+        }
+
+        String expectedOutput = IoUtils.readUtf8Resource(getClass(), "idl-serialization/enum-mixin-output.smithy")
+                .replaceAll("\\R", "\n");
+        String serializedString = serialized.entrySet().iterator().next().getValue();
+        Assertions.assertEquals(expectedOutput, serializedString);
+    }
 }
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/validation/testrunner/SmithyTestCaseTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/validation/testrunner/SmithyTestCaseTest.java
index 065573d98..f185b0884 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/validation/testrunner/SmithyTestCaseTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/validation/testrunner/SmithyTestCaseTest.java
@@ -18,6 +18,7 @@ package software.amazon.smithy.model.validation.testrunner;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.util.Collections;
 import org.junit.jupiter.api.Assertions;
@@ -32,14 +33,17 @@ import software.amazon.smithy.utils.ListUtils;
 public class SmithyTestCaseTest {
     @Test
     public void validatesThatEventsAreValid() {
-        Assertions.assertThrows(
+        IllegalArgumentException e = Assertions.assertThrows(
                 IllegalArgumentException.class,
-                () -> SmithyTestCase.parseValidationEvent("[ERROR] - m"));
+                () -> SmithyTestCase.parseValidationEvent("[ERROR] - m", "filename"));
+
+        assertTrue(e.getMessage().contains("`filename`"));
+        assertTrue(e.getMessage().contains("SUPPRESSED|NOTE|WARNING|DANGER|ERROR"));
     }
 
     @Test
     public void parsesValidEvents() {
-        SmithyTestCase.parseValidationEvent("[ERROR] -: message | EventId /filename:0:0");
+        SmithyTestCase.parseValidationEvent("[ERROR] -: message | EventId /filename:0:0", "filename");
     }
 
     @Test
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-bindings-missing-validator.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-bindings-missing-validator.errors
index 1375a56da..d63edc77a 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-bindings-missing-validator.errors
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-bindings-missing-validator.errors
@@ -1,2 +1,2 @@
-[ERROR] ns.foo#MissingBindings1: One or more operations in the `ns.foo#MyService` service define the `http` trait, but this operation is missing the `http` trait. | HttpBindingsMissing
-[ERROR] ns.foo#MissingBindings2: One or more operations in the `ns.foo#MyService` service define the `http` trait, but this operation is missing the `http` trait. | HttpBindingsMissing
+[ERROR] ns.foo#MissingBindings1: The `ns.foo#restProtocol` protocol requires all operations in the `ns.foo#MyService` service define the `http` trait, but this operation is missing the `http` trait. | HttpBindingsMissing
+[ERROR] ns.foo#MissingBindings2: The `ns.foo#restProtocol` protocol requires all operations in the `ns.foo#MyService` service define the `http` trait, but this operation is missing the `http` trait. | HttpBindingsMissing
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-method-semantics-validator.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-method-semantics-validator.errors
index e63b8308a..47fe47a07 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-method-semantics-validator.errors
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-method-semantics-validator.errors
@@ -5,3 +5,4 @@
 [WARNING] ns.foo#H: This operation uses the `DELETE` method in the `http` trait, but is not marked with the idempotent trait | HttpMethodSemantics.MissingIdempotentTrait
 [WARNING] ns.foo#I: This operation uses the `GET` method in the `http` trait, but is not marked with the readonly trait | HttpMethodSemantics.MissingReadonlyTrait
 [WARNING] ns.foo#Options: OPTIONS requests are typically used as part of CORS and should not be modeled explicitly. They are an implementation detail that should not appear in generated client or server code. Instead, tooling should use the `cors` trait on a service to automatically configure CORS on clients and servers. It is the responsibility of service frameworks and API gateways to automatically manage OPTIONS requests. For example, OPTIONS requests are automatically created when using Smithy models with Amazon API Gateway. | HttpMethodSemantics.OPTIONS
+[WARNING] ns.foo#A: This operation uses the `Get` method in the `http` trait, but expected `GET`. | HttpMethodSemantics
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-method-semantics-validator.json b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-method-semantics-validator.json
index d55c1449a..b457df117 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-method-semantics-validator.json
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-method-semantics-validator.json
@@ -6,7 +6,7 @@
             "traits": {
                 "smithy.api#readonly": {},
                 "smithy.api#http": {
-                    "method": "GET",
+                    "method": "Get",
                     "uri": "/A"
                 }
             }
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/paginated/paginated-invalid.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/paginated/paginated-invalid.errors
index 90123a12a..48508ebf0 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/paginated/paginated-invalid.errors
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/paginated/paginated-invalid.errors
@@ -1,13 +1,13 @@
 [ERROR] ns.foo#Invalid2: paginated trait `inputToken` targets a member `nextToken` that does not exist | PaginatedTrait
 [ERROR] ns.foo#Invalid2: paginated trait `outputToken` targets a member `nextToken` that does not exist | PaginatedTrait
-[ERROR] ns.foo#Invalid3: paginated trait `inputToken` member `InputNotString` targets a integer shape, but must target one of the following: [`map`, `string`] | PaginatedTrait
+[ERROR] ns.foo#Invalid3: paginated trait `inputToken` member `nextToken` targets a integer shape, but must target one of the following: [`map`, `string`] | PaginatedTrait
 [ERROR] ns.foo#Invalid3: paginated trait `items` targets a member `items` that does not exist | PaginatedTrait
-[ERROR] ns.foo#Invalid3: paginated trait `outputToken` member `OutputNotString` targets a integer shape, but must target one of the following: [`map`, `string`] | PaginatedTrait
+[ERROR] ns.foo#Invalid3: paginated trait `outputToken` member `nextToken` targets a integer shape, but must target one of the following: [`map`, `string`] | PaginatedTrait
 [ERROR] ns.foo#Invalid4: paginated trait `inputToken` member `nextToken` must not be required | PaginatedTrait
 [ERROR] ns.foo#Invalid4: paginated trait `outputToken` member `nextToken` must not be required | PaginatedTrait
 [WARNING] ns.foo#Invalid4: paginated trait `pageSize` member `pageSize` should not be required | PaginatedTrait.ShouldNotBeRequired.pageSize
-[ERROR] ns.foo#Invalid5: paginated trait `pageSize` member `Invalid5Input` targets a string shape, but must target one of the following: [`integer`] | PaginatedTrait
-[ERROR] ns.foo#Invalid6: paginated trait `items` member `Invalid6Output` targets a string shape, but must target one of the following: [`list`, `map`] | PaginatedTrait
+[ERROR] ns.foo#Invalid5: paginated trait `pageSize` member `pageSize` targets a string shape, but must target one of the following: [`byte`, `integer`, `long`, `short`] | PaginatedTrait
+[ERROR] ns.foo#Invalid6: paginated trait `items` member `items` targets a string shape, but must target one of the following: [`list`, `map`] | PaginatedTrait
 [ERROR] ns.foo#Invalid7: paginated trait `items` targets a member `items` that does not exist | PaginatedTrait
 [ERROR] ns.foo#Invalid7: paginated trait `outputToken` targets a member `missing` that does not exist | PaginatedTrait
 [ERROR] ns.foo#Invalid8: When bound within the `ns.foo#Service` service, paginated trait `inputToken` is not configured | PaginatedTrait
@@ -18,3 +18,6 @@
 [ERROR] ns.foo#Invalid1: paginated trait `pageSize` targets a member `pageSize` that does not exist | PaginatedTrait
 [ERROR] ns.foo#InvalidNoOutput: paginated trait `items` targets a member `items` that does not exist | PaginatedTrait
 [ERROR] ns.foo#InvalidNoOutput: paginated trait `outputToken` targets a member `nextToken` that does not exist | PaginatedTrait
+[DANGER] ns.foo#DangerSizeByte: paginated trait `pageSize` member `pageSize` targets a byte shape, but this is not recommended. One of [`integer`] SHOULD be targeted. | PaginatedTrait
+[DANGER] ns.foo#DangerSizeShort: paginated trait `pageSize` member `pageSize` targets a short shape, but this is not recommended. One of [`integer`] SHOULD be targeted. | PaginatedTrait
+[DANGER] ns.foo#DangerSizeLong: paginated trait `pageSize` member `pageSize` targets a long shape, but this is not recommended. One of [`integer`] SHOULD be targeted. | PaginatedTrait
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/paginated/paginated-invalid.json b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/paginated/paginated-invalid.json
index 5080a86cf..79d3e73dd 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/paginated/paginated-invalid.json
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/paginated/paginated-invalid.json
@@ -34,6 +34,15 @@
                 },
                 {
                     "target": "ns.foo#InvalidNestedInput"
+                },
+                {
+                    "target": "ns.foo#DangerSizeByte"
+                },
+                {
+                    "target": "ns.foo#DangerSizeShort"
+                },
+                {
+                    "target": "ns.foo#DangerSizeLong"
                 }
             ]
         },
@@ -303,6 +312,93 @@
                 }
             }
         },
+        "ns.foo#DangerSizeByte": {
+            "type": "operation",
+            "input": {
+                "target": "ns.foo#DangerSizeByteInput"
+            },
+            "output": {
+                "target": "ns.foo#ValidOutput"
+            },
+            "traits": {
+                "smithy.api#readonly": {},
+                "smithy.api#paginated": {
+                    "inputToken": "nextToken",
+                    "outputToken": "nextToken",
+                    "items": "items",
+                    "pageSize": "pageSize"
+                }
+            }
+        },
+        "ns.foo#DangerSizeByteInput": {
+            "type": "structure",
+            "members": {
+                "nextToken": {
+                    "target": "smithy.api#String"
+                },
+                "pageSize": {
+                    "target": "smithy.api#Byte"
+                }
+            }
+        },
+        "ns.foo#DangerSizeShort": {
+            "type": "operation",
+            "input": {
+                "target": "ns.foo#DangerSizeShortInput"
+            },
+            "output": {
+                "target": "ns.foo#ValidOutput"
+            },
+            "traits": {
+                "smithy.api#readonly": {},
+                "smithy.api#paginated": {
+                    "inputToken": "nextToken",
+                    "outputToken": "nextToken",
+                    "items": "items",
+                    "pageSize": "pageSize"
+                }
+            }
+        },
+        "ns.foo#DangerSizeShortInput": {
+            "type": "structure",
+            "members": {
+                "nextToken": {
+                    "target": "smithy.api#String"
+                },
+                "pageSize": {
+                    "target": "smithy.api#Short"
+                }
+            }
+        },
+        "ns.foo#DangerSizeLong": {
+            "type": "operation",
+            "input": {
+                "target": "ns.foo#DangerSizeLongInput"
+            },
+            "output": {
+                "target": "ns.foo#ValidOutput"
+            },
+            "traits": {
+                "smithy.api#readonly": {},
+                "smithy.api#paginated": {
+                    "inputToken": "nextToken",
+                    "outputToken": "nextToken",
+                    "items": "items",
+                    "pageSize": "pageSize"
+                }
+            }
+        },
+        "ns.foo#DangerSizeLongInput": {
+            "type": "structure",
+            "members": {
+                "nextToken": {
+                    "target": "smithy.api#String"
+                },
+                "pageSize": {
+                    "target": "smithy.api#Long"
+                }
+            }
+        },
         "ns.foo#NestedInput": {
             "type": "structure",
             "members": {
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/paginated/paginated-map-tokens.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/paginated/paginated-map-tokens.errors
index 9f6bc968c..30b2459a2 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/paginated/paginated-map-tokens.errors
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/paginated/paginated-map-tokens.errors
@@ -1,2 +1,2 @@
-[DANGER] ns.foo#MapTokens: paginated trait `inputToken` member `MapTokensInput` targets a map shape, but this is not recommended. One of [`string`] SHOULD be targeted. | PaginatedTrait.WrongShapeType.inputToken
-[DANGER] ns.foo#MapTokens: paginated trait `outputToken` member `MapTokensOutput` targets a map shape, but this is not recommended. One of [`string`] SHOULD be targeted. | PaginatedTrait.WrongShapeType.outputToken
+[DANGER] ns.foo#MapTokens: paginated trait `inputToken` member `nextToken` targets a map shape, but this is not recommended. One of [`string`] SHOULD be targeted. | PaginatedTrait.WrongShapeType.inputToken
+[DANGER] ns.foo#MapTokens: paginated trait `outputToken` member `nextToken` targets a map shape, but this is not recommended. One of [`string`] SHOULD be targeted. | PaginatedTrait.WrongShapeType.outputToken
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/enum-mixin-input.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/enum-mixin-input.smithy
new file mode 100644
index 000000000..d09dab55b
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/enum-mixin-input.smithy
@@ -0,0 +1,23 @@
+$version: "2.0"
+
+namespace ns.foo
+
+@mixin
+intEnum IntMixin {
+    FOO = 1
+    BAR = 2
+}
+
+intEnum IntEnum with [IntMixin] {
+    FOO = 1
+    BAR = 2
+}
+
+@mixin
+enum StringMixin {
+    BAZ = "baz"
+}
+
+enum StringEnum with [StringMixin] {
+    BAZ = "baz"
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/enum-mixin-output.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/enum-mixin-output.smithy
new file mode 100644
index 000000000..560cb6b03
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/enum-mixin-output.smithy
@@ -0,0 +1,18 @@
+$version: "2.0"
+
+namespace ns.foo
+
+intEnum IntEnum with [IntMixin] {}
+
+@mixin
+intEnum IntMixin {
+    FOO = 1
+    BAR = 2
+}
+
+enum StringEnum with [StringMixin] {}
+
+@mixin
+enum StringMixin {
+    BAZ = "baz"
+}
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/node-text-blocks.formatted.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/node-text-blocks.formatted.smithy
new file mode 100644
index 000000000..9a39712d8
--- /dev/null
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/node-text-blocks.formatted.smithy
@@ -0,0 +1,21 @@
+$version: "2.0"
+
+metadata validators = [
+    {
+        name: "EmitEachSelector"
+        id: "MissingStringInputLengthValidation"
+        severity: "DANGER"
+        message: "This string is missing required length trait"
+        configuration: {
+            selector: """
+                operation -[input]-> structure > member
+                :test(member > string:not([trait|enum]))
+                :test(member > string:not([trait|length]))
+                :test(member > string:not([trait|aws.api#arnReference]))
+                :test(member > string:not([trait|aws.api#providesPassRole]))
+                """
+        }
+    }
+]
+
+namespace smithy.example
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/node-text-blocks.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/node-text-blocks.smithy
new file mode 100644
index 000000000..45e1b5648
--- /dev/null
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/node-text-blocks.smithy
@@ -0,0 +1,21 @@
+$version: "2.0"
+
+metadata validators = [
+    {
+        name: "EmitEachSelector"
+        id: "MissingStringInputLengthValidation"
+        severity: "DANGER"
+        message: "This string is missing required length trait"
+        configuration: {
+            selector: """
+operation -[input]-> structure > member
+    :test(member > string:not([trait|enum]))
+:test(member > string:not([trait|length]))
+:test(member > string:not([trait|aws.api#arnReference]))
+:test(member > string:not([trait|aws.api#providesPassRole]))
+"""
+        }
+    }
+]
+
+namespace smithy.example
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.formatted.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.formatted.smithy
new file mode 100644
index 000000000..8fa2be8e6
--- /dev/null
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.formatted.smithy
@@ -0,0 +1,26 @@
+$version: "2.0"
+
+namespace smithy.example
+
+@documentation(
+    """
+    This is the documentation for Foo.
+    Lorem ipsum dolor."""
+)
+string Foo
+
+@documentation(
+    """
+    This is the documentation for Bar.
+    Lorem ipsum dolor.
+    """
+)
+string Bar
+
+@documentation(
+    """
+    This is the documentation for Baz.
+    Lorem ipsum dolor.
+    """
+)
+string Baz
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.smithy
index f586af1aa..99efba0d8 100644
--- a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.smithy
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.smithy
@@ -16,3 +16,11 @@ string Foo
     """
 )
 string Bar
+
+@documentation(
+"""
+This is the documentation for Baz.
+Lorem ipsum dolor.
+"""
+)
+string Baz