Repository: smithy-lang/smithy
Pull request #2266 (2024-04-29 23:10:06)

Add support for StringArray rules engine Parameters
#### Background
* Add support for `StringArray` Parameters in the rules engine.  The rules engine already supports array types in its internal type system, this PR just allows users to use `StringArray` as a type when specifying parameters in an `EndpointRuleset`.

#### Testing
* New language error file tests

---
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/docs/source-1.0/spec/waiters.rst b/docs/source-1.0/spec/waiters.rst
index 4465a63ae..371eaac32 100644
--- a/docs/source-1.0/spec/waiters.rst
+++ b/docs/source-1.0/spec/waiters.rst
@@ -190,12 +190,12 @@ exponential backoff with jitter through the following algorithm:
     delay = random(minDelay, delay)
 
     if remainingTime - delay <= minDelay:
-        delay = remainingTime - minDelay
+        delay = remainingTime
 
 If the computed ``delay`` subtracted from ``remainingTime`` is less than
-or equal to ``minDelay``, then set ``delay`` to ``remainingTime`` minus
-``minDelay`` and perform one last retry. This prevents a waiter from waiting
-needlessly only to exceed ``maxWaitTime`` before issuing a final request.
+or equal to ``minDelay``, then set ``delay`` to ``remainingTime`` and
+perform one last retry. This prevents a waiter from waiting needlessly
+only to exceed ``maxWaitTime`` before issuing a final request.
 
 Using the default ``minDelay`` of 2, the default ``maxDelay`` of 120, a caller
 provided ``maxWaitTime`` of 300 (5 minutes), and assuming that requests
diff --git a/docs/source-2.0/additional-specs/rules-engine/parameters.rst b/docs/source-2.0/additional-specs/rules-engine/parameters.rst
index 3385dc5f2..13f32750e 100644
--- a/docs/source-2.0/additional-specs/rules-engine/parameters.rst
+++ b/docs/source-2.0/additional-specs/rules-engine/parameters.rst
@@ -23,9 +23,9 @@ This following is the :rfc:`ABNF <5234>` grammar for rule set parameter names:
 .. productionlist:: smithy
     identifier = ALPHA *(ALPHA / DIGIT)
 
-Parameters declare their respective type using the ``type`` key. There are two
-supported rule set parameter types: ``string`` and ``boolean``. The following
-table provides the description of these types, and their Smithy compatible
+Parameters declare their respective type using the ``type`` key. The following
+parameter types are supported: ``string``, ``boolean``, and ``stringArray``.
+The following table provides the description of these types, and their Smithy compatible
 types whose values can be bound to these parameters. Rule set parameters are
 always considered nullable and have no default value associated with them.
 
@@ -42,6 +42,9 @@ always considered nullable and have no default value associated with them.
     * - ``boolean``
       - ``boolean``
       - Boolean value type.
+    * - ``stringArray``
+      - ``list``
+      - A list with ``string`` members.
 
 
 .. _rules-engine-parameters-implementation:
@@ -95,6 +98,7 @@ order of the most specific to least specific value locations:
 
 #. `smithy.rules#staticContextParams trait`_
 #. `smithy.rules#contextParam trait`_
+#. `smithy.rules#operationContextParams trait`_
 #. `smithy.rules#clientContextParams trait`_
 #. Built-in bindings
 #. Built-in binding default values
@@ -218,13 +222,14 @@ The ``staticContextParam`` structure has the following properties:
     * - value
       - ``document``
       - **Required**. The static value to be set for the parameter. The type
-        of the value MUST be either a ``string`` or ``boolean``.
+        of the value MUST be either a ``string``, ``boolean`` or an
+        array of ``string``.
 
 Each parameter is identified using it’s name as specified in the rule set. The
 type of a ``staticContextParam`` MUST be compatible with the parameter type
 specified in the rule set.
 
-The following example specifies two parameters to statically set for an
+The following example specifies three parameters to statically set for an
 operation:
 
 .. code-block:: smithy
@@ -235,11 +240,90 @@ operation:
         }
         previewEndpoint: {
             value: true
+        },
+        supportedPrefixes: {
+            value: ["host", "id", "resourceId"]
         }
     )
     operation GetThing {}
 
 
+.. smithy-trait:: smithy.rules#operationContextParams
+.. _smithy.rules#operationContextParams-trait:
+
+``smithy.rules#operationContextParams`` trait
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Summary
+    Defines one or more rule set parameters that MUST be bound to values
+    specified in the operation input.
+Trait selector
+    ``operation``
+Value type
+    ``map`` of ``string`` containing a rule set parameter name to a
+    ``operationContextParam`` structure.
+
+The ``operationContextParam`` structure has the following properties:
+
+.. list-table::
+    :header-rows: 1
+    :widths: 10 23 67
+
+    * - Property
+      - Type
+      - Description
+    * - path
+      - ``string``
+      - **Required**. A JMESPath expression to select element(s) from the operation input to bind to.
+
+Each parameter is identified using it’s name as specified in the rule set. The
+type of a ``operationContextParam`` MUST be compatible with the parameter type
+specified in the rule set.
+
+The following example specifies a parameter bound to an array of object keys in the
+operation input using a JMESPath expression:
+
+.. code-block:: smithy
+
+    @operationContextParams(
+        ObjectKeys: {
+            path: "Delete.Objects[*].Key"
+        }
+    )
+    operation DeleteObjects {
+        input: DeleteObjectsRequest
+    }
+
+    structure DeleteObjectsRequest {
+        Delete: Delete
+    }
+
+    structure Delete {
+        Objects: ObjectIdentifierList
+    }
+
+    list ObjectIdentifierList {
+        member: ObjectIdentifier
+    }
+
+    structure ObjectIdentifier {
+        Key: String
+    }
+
+`paths` specified in :ref:`OperationContextParams <smithy.rules#operationContextParams-trait>` are limited
+to a subset of JMESPath:
+
+* `Identifiers`_ - the most basic expression and can be used to extract a single element from a JSON document.
+  The return value for an identifier is the value associated with the identifier. If the identifier does not
+  exist in the JSON document, than a null value is returned.
+* `Sub Expressions`_ - a combination of two expressions separated by the ``.`` char.
+  Example:  ``grandparent.parent.child``
+* `Wildcard Expressions`_ - Creates a projection over the values in an array or map.
+  Remaining expressions are evaluated against each returned element.
+* `Keys function`_ - return a list of the keys in a map.  This is the only supported function but is required
+  for binding to key values.
+
+
 .. smithy-trait:: smithy.rules#contextParam
 .. _smithy.rules#contextParam-trait:
 
@@ -343,3 +427,7 @@ The rules engine is highly extensible through
 
 .. _Javadocs: https://smithy.io/javadoc/__smithy_version__/software/amazon/smithy/rulesengine/language/EndpointRuleSetExtension.html
 .. _service providers: https://docs.oracle.com/javase/tutorial/sound/SPI-intro.html
+.. _Identifiers: https://jmespath.org/specification.html#identifiers
+.. _Sub expressions: https://jmespath.org/specification.html#subexpressions
+.. _Wildcard expressions: https://jmespath.org/specification.html#wildcard-expressions
+.. _Keys function: https://jmespath.org/specification.html#keys
\ No newline at end of file
diff --git a/docs/source-2.0/additional-specs/rules-engine/specification.rst b/docs/source-2.0/additional-specs/rules-engine/specification.rst
index 148170f66..bd934d9d3 100644
--- a/docs/source-2.0/additional-specs/rules-engine/specification.rst
+++ b/docs/source-2.0/additional-specs/rules-engine/specification.rst
@@ -91,13 +91,13 @@ A parameter object contains the following properties:
       - Description
     * - type
       - ``string``
-      - **Required**. MUST be one of ``string`` or ``boolean``.
+      - **Required**. MUST be one of ``string``, ``boolean``, or ``stringArray``.
     * - builtIn
       - ``string``
       - Specifies a named built-in value that is sourced and provided to the
         endpoint provider by a caller.
     * - default
-      - ``string`` or ``boolean``
+      - ``string``, ``boolean`` or an array of ``string``.
       - Specifies the default value for the parameter if not set. Parameters
         with defaults MUST also be marked as ``required``. The type of the
         provided default MUST match ``type``.
diff --git a/docs/source-2.0/additional-specs/waiters.rst b/docs/source-2.0/additional-specs/waiters.rst
index 45bbb5e73..70ab82c83 100644
--- a/docs/source-2.0/additional-specs/waiters.rst
+++ b/docs/source-2.0/additional-specs/waiters.rst
@@ -191,12 +191,12 @@ exponential backoff with jitter through the following algorithm:
     delay = random(minDelay, delay)
 
     if remainingTime - delay <= minDelay:
-        delay = remainingTime - minDelay
+        delay = remainingTime
 
 If the computed ``delay`` subtracted from ``remainingTime`` is less than
-or equal to ``minDelay``, then set ``delay`` to ``remainingTime`` minus
-``minDelay`` and perform one last retry. This prevents a waiter from waiting
-needlessly only to exceed ``maxWaitTime`` before issuing a final request.
+or equal to ``minDelay``, then set ``delay`` to ``remainingTime`` and
+perform one last retry. This prevents a waiter from waiting needlessly
+only to exceed ``maxWaitTime`` before issuing a final request.
 
 Using the default ``minDelay`` of 2, the default ``maxDelay`` of 120, a caller
 provided ``maxWaitTime`` of 300 (5 minutes), and assuming that requests
@@ -259,8 +259,8 @@ follows:
       - 296
       - 4
     * - 13 (last attempt)
-      - 2
-      - 298
+      - 4
+      - 300
       - N/A
 
 .. note::
diff --git a/docs/source-2.0/spec/model.rst b/docs/source-2.0/spec/model.rst
index 47f79c8a8..d07630146 100644
--- a/docs/source-2.0/spec/model.rst
+++ b/docs/source-2.0/spec/model.rst
@@ -423,13 +423,13 @@ Shape types are grouped into three categories:
 
         * - Type
           - Description
-        * - :ref:`list`
+        * - :ref:`list <list>`
           - Ordered collection of homogeneous values
-        * - :ref:`map`
+        * - :ref:`map <map>`
           - Map data structure that maps string keys to homogeneous values
-        * - :ref:`structure`
+        * - :ref:`structure <structure>`
           - Fixed set of named heterogeneous members
-        * - :ref:`union`
+        * - :ref:`union <union>`
           - Tagged union data structure that can take on one of several
             different, but fixed, types
 :ref:`Service types <service-types>`
diff --git a/smithy-rules-engine/build.gradle b/smithy-rules-engine/build.gradle
index ea5067f64..2b8df453d 100644
--- a/smithy-rules-engine/build.gradle
+++ b/smithy-rules-engine/build.gradle
@@ -13,4 +13,5 @@ ext {
 dependencies {
     api project(":smithy-model")
     api project(":smithy-utils")
+    api project(":smithy-jmespath")
 }
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/Type.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/Type.java
index 244a1c9ab..d4a709ec3 100644
--- a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/Type.java
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/Type.java
@@ -45,6 +45,9 @@ public interface Type {
         if (parameterType == ParameterType.BOOLEAN) {
             return booleanType();
         }
+        if (parameterType == ParameterType.STRING_ARRAY) {
+            return arrayType(stringType());
+        }
         throw new IllegalArgumentException("Unexpected parameter type: " + parameterType);
     }
 
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/value/ArrayValue.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/value/ArrayValue.java
index 7f736179b..2a76ac8de 100644
--- a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/value/ArrayValue.java
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/value/ArrayValue.java
@@ -55,7 +55,7 @@ public final class ArrayValue extends Value {
         } else {
             Type first = values.get(0).getType();
             for (Value value : values) {
-                if (value.getType() != first) {
+                if (!value.getType().isA(first)) {
                     throw new SourceException("An array cannot contain different types", this);
                 }
             }
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/syntax/expressions/functions/GetAttr.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/syntax/expressions/functions/GetAttr.java
index 6dd674ba3..97e1d83ea 100644
--- a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/syntax/expressions/functions/GetAttr.java
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/syntax/expressions/functions/GetAttr.java
@@ -105,7 +105,9 @@ public final class GetAttr extends LibraryFunction {
                         throw new InvalidRulesException("Invalid path component: slice index must be >= 0",
                                 sourceLocation);
                     }
-                    result.add(Part.Key.of(component.substring(0, slicePartIndex)));
+                    if (slicePartIndex > 0) {
+                        result.add(Part.Key.of(component.substring(0, slicePartIndex)));
+                    }
                     result.add(new Part.Index(slice));
                 } catch (NumberFormatException ex) {
                     throw new InvalidRulesException(String.format("%s could not be parsed as a number", slicePart),
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/syntax/parameters/ParameterType.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/syntax/parameters/ParameterType.java
index 14ddcdc1d..6882d02b8 100644
--- a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/syntax/parameters/ParameterType.java
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/syntax/parameters/ParameterType.java
@@ -10,6 +10,7 @@ import software.amazon.smithy.model.node.Node;
 import software.amazon.smithy.model.node.StringNode;
 import software.amazon.smithy.model.shapes.ShapeType;
 import software.amazon.smithy.rulesengine.language.error.RuleError;
+import software.amazon.smithy.rulesengine.language.evaluation.type.ArrayType;
 import software.amazon.smithy.rulesengine.language.evaluation.type.BooleanType;
 import software.amazon.smithy.rulesengine.language.evaluation.type.StringType;
 import software.amazon.smithy.rulesengine.language.evaluation.type.Type;
@@ -28,7 +29,12 @@ public enum ParameterType {
     /**
      * A "boolean" parameter type.
      */
-    BOOLEAN;
+    BOOLEAN,
+
+    /**
+     * An array (list) of strings parameter type.
+     */
+    STRING_ARRAY;
 
     /**
      * Creates a {@link ParameterType} of a specific type from the given Node information.
@@ -39,14 +45,18 @@ public enum ParameterType {
      */
     public static ParameterType fromNode(StringNode node) throws RuleError {
         String value = node.getValue();
-        if (value.equalsIgnoreCase("String")) {
+        if (value.equalsIgnoreCase("string")) {
             return STRING;
         }
-        if (value.equalsIgnoreCase("Boolean")) {
+        if (value.equalsIgnoreCase("boolean")) {
             return BOOLEAN;
         }
+        if (value.equals("stringArray")) {
+            return STRING_ARRAY;
+        }
         throw new RuleError(new SourceException(
-                String.format("Unexpected parameter type `%s`. Expected `String` or `Boolean`.", value), node));
+                String.format("Unexpected parameter type `%s`. Expected `string`, `boolean`, or `stringArray`.",
+                        value), node));
     }
 
     /**
@@ -63,6 +73,17 @@ public enum ParameterType {
         if (node.isBooleanNode()) {
             return BOOLEAN;
         }
+        if (node.isArrayNode()) {
+            // confirm all elements are Strings
+            node.expectArrayNode().getElements().forEach(memberNode -> {
+                if (!memberNode.isStringNode()) {
+                    throw new RuleError(new SourceException(
+                            String.format("Unexpected array member parameter type `%s`. Expected a string.",
+                                    memberNode.getType()), memberNode));
+                }
+            });
+            return STRING_ARRAY;
+        }
         throw new RuleError(new SourceException(
                 String.format("Unexpected parameter type `%s`. Expected a string or boolean.", node.getType()), node));
     }
@@ -81,8 +102,14 @@ public enum ParameterType {
         if (type instanceof BooleanType) {
             return BOOLEAN;
         }
+        if (type instanceof ArrayType) {
+            ArrayType arrayType = (ArrayType) type;
+            if (arrayType.getMember().isA(Type.stringType()) || arrayType.getMember().isA(Type.emptyType())) {
+                return STRING_ARRAY;
+            }
+        }
         throw new RuntimeException(
-                String.format("Unexpected parameter type `%s`. Expected a string or boolean.", type));
+                String.format("Unexpected parameter type `%s`. Expected a string, boolean, or array<string>.", type));
     }
 
     /**
@@ -105,6 +132,16 @@ public enum ParameterType {
 
     @Override
     public String toString() {
-        return this == STRING ? "String" : "Boolean";
+        // Inconsistent casing on string/boolean to preserve backwards compatibility in serialization
+        switch (this) {
+            case STRING:
+                return "String";
+            case BOOLEAN:
+                return "Boolean";
+            case STRING_ARRAY:
+                return "stringArray";
+            default:
+                return "Unknown Type";
+        }
     }
 }
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/traits/ContextIndex.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/traits/ContextIndex.java
index 4ddddcdc3..9f55985fc 100644
--- a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/traits/ContextIndex.java
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/traits/ContextIndex.java
@@ -56,6 +56,17 @@ public final class ContextIndex implements KnowledgeIndex {
         return operation.getTrait(StaticContextParamsTrait.class);
     }
 
+    /**
+     * Gets the operation context parameter names and their {@link OperationContextParamDefinition} for the given
+     * operation.
+     *
+     * @param operation The operation shape.
+     * @return The mapping of context parameter names to the JMESPath expression to bind to input elements.
+     */
+    public Optional<OperationContextParamsTrait> getOperationContextParams(Shape operation) {
+        return operation.getTrait(OperationContextParamsTrait.class);
+    }
+
     /**
      * Gets the mapping of {@link MemberShape} to {@link ContextParamTrait} for the operation.
      *
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/traits/OperationContextParamDefinition.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/traits/OperationContextParamDefinition.java
new file mode 100644
index 000000000..8c99ed599
--- /dev/null
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/traits/OperationContextParamDefinition.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.rulesengine.traits;
+
+import java.util.Objects;
+import software.amazon.smithy.utils.SmithyBuilder;
+import software.amazon.smithy.utils.SmithyUnstableApi;
+import software.amazon.smithy.utils.ToSmithyBuilder;
+
+/**
+ * An operation context parameter definition.
+ */
+@SmithyUnstableApi
+public final class OperationContextParamDefinition implements ToSmithyBuilder<OperationContextParamDefinition> {
+    private final String path;
+
+    private OperationContextParamDefinition(Builder builder) {
+        this.path = SmithyBuilder.requiredState("path", builder.path);
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    public String getPath() {
+        return path;
+    }
+
+    @Override
+    public Builder toBuilder() {
+        return builder().path(path);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getPath());
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        OperationContextParamDefinition that = (OperationContextParamDefinition) o;
+        return getPath().equals(that.getPath());
+    }
+
+    public static final class Builder implements SmithyBuilder<OperationContextParamDefinition> {
+        private String path;
+
+        private Builder() {
+        }
+
+        public Builder path(String path) {
+            this.path = path;
+            return this;
+        }
+
+        public OperationContextParamDefinition build() {
+            return new OperationContextParamDefinition(this);
+        }
+    }
+}
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/traits/OperationContextParamsTrait.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/traits/OperationContextParamsTrait.java
new file mode 100644
index 000000000..5bf6d68a5
--- /dev/null
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/traits/OperationContextParamsTrait.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.rulesengine.traits;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import software.amazon.smithy.model.node.Node;
+import software.amazon.smithy.model.node.NodeMapper;
+import software.amazon.smithy.model.shapes.ShapeId;
+import software.amazon.smithy.model.traits.AbstractTrait;
+import software.amazon.smithy.model.traits.AbstractTraitBuilder;
+import software.amazon.smithy.model.traits.Trait;
+import software.amazon.smithy.utils.BuilderRef;
+import software.amazon.smithy.utils.SmithyUnstableApi;
+import software.amazon.smithy.utils.ToSmithyBuilder;
+
+/**
+ * Binds elements of a target operation's input to rule-set parameters.
+ */
+@SmithyUnstableApi
+public final class OperationContextParamsTrait extends AbstractTrait
+        implements ToSmithyBuilder<OperationContextParamsTrait> {
+    public static final ShapeId ID = ShapeId.from("smithy.rules#operationContextParams");
+
+    private final Map<String, OperationContextParamDefinition> parameters;
+
+    private OperationContextParamsTrait(Builder builder) {
+        super(ID, builder.getSourceLocation());
+        this.parameters = builder.parameters.copy();
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    public Map<String, OperationContextParamDefinition> getParameters() {
+        return parameters;
+    }
+
+    @Override
+    protected Node createNode() {
+        NodeMapper mapper = new NodeMapper();
+        mapper.setOmitEmptyValues(true);
+        return mapper.serialize(getParameters()).expectObjectNode();
+    }
+
+    @Override
+    public Builder toBuilder() {
+        return new Builder()
+                .sourceLocation(getSourceLocation())
+                .parameters(parameters);
+    }
+
+    public static final class Provider extends AbstractTrait.Provider {
+        public Provider() {
+            super(ID);
+        }
+
+        @Override
+        public Trait createTrait(ShapeId target, Node value) {
+            NodeMapper mapper = new NodeMapper();
+            Map<String, OperationContextParamDefinition> parameters = new LinkedHashMap<>();
+            value.expectObjectNode().getMembers().forEach((stringNode, node) -> {
+                parameters.put(stringNode.getValue(), mapper.deserialize(node, OperationContextParamDefinition.class));
+            });
+            OperationContextParamsTrait trait = builder()
+                    .sourceLocation(value)
+                    .parameters(parameters)
+                    .build();
+            trait.setNodeCache(value);
+            return trait;
+        }
+    }
+
+    public static final class Builder extends AbstractTraitBuilder<OperationContextParamsTrait, Builder> {
+        private final BuilderRef<Map<String, OperationContextParamDefinition>> parameters = BuilderRef.forOrderedMap();
+
+        private Builder() {
+        }
+
+        public Builder parameters(Map<String, OperationContextParamDefinition> parameters) {
+            this.parameters.clear();
+            this.parameters.get().putAll(parameters);
+            return this;
+        }
+
+        public Builder putParameter(String name, OperationContextParamDefinition definition) {
+            this.parameters.get().put(name, definition);
+            return this;
+        }
+
+        public Builder removeParameter(String name) {
+            this.parameters.get().remove(name);
+            return this;
+        }
+
+        public Builder clearParameters() {
+            this.parameters.clear();
+            return this;
+        }
+
+        @Override
+        public OperationContextParamsTrait build() {
+            return new OperationContextParamsTrait(this);
+        }
+    }
+}
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/traits/OperationContextParamsTraitValidator.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/traits/OperationContextParamsTraitValidator.java
new file mode 100644
index 000000000..c652fc1c1
--- /dev/null
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/traits/OperationContextParamsTraitValidator.java
@@ -0,0 +1,459 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.rulesengine.traits;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import software.amazon.smithy.jmespath.ExpressionVisitor;
+import software.amazon.smithy.jmespath.JmespathException;
+import software.amazon.smithy.jmespath.JmespathExpression;
+import software.amazon.smithy.jmespath.LinterResult;
+import software.amazon.smithy.jmespath.ast.AndExpression;
+import software.amazon.smithy.jmespath.ast.ComparatorExpression;
+import software.amazon.smithy.jmespath.ast.CurrentExpression;
+import software.amazon.smithy.jmespath.ast.ExpressionTypeExpression;
+import software.amazon.smithy.jmespath.ast.FieldExpression;
+import software.amazon.smithy.jmespath.ast.FilterProjectionExpression;
+import software.amazon.smithy.jmespath.ast.FlattenExpression;
+import software.amazon.smithy.jmespath.ast.FunctionExpression;
+import software.amazon.smithy.jmespath.ast.IndexExpression;
+import software.amazon.smithy.jmespath.ast.LiteralExpression;
+import software.amazon.smithy.jmespath.ast.MultiSelectHashExpression;
+import software.amazon.smithy.jmespath.ast.MultiSelectListExpression;
+import software.amazon.smithy.jmespath.ast.NotExpression;
+import software.amazon.smithy.jmespath.ast.ObjectProjectionExpression;
+import software.amazon.smithy.jmespath.ast.OrExpression;
+import software.amazon.smithy.jmespath.ast.ProjectionExpression;
+import software.amazon.smithy.jmespath.ast.SliceExpression;
+import software.amazon.smithy.jmespath.ast.Subexpression;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.knowledge.OperationIndex;
+import software.amazon.smithy.model.shapes.BigDecimalShape;
+import software.amazon.smithy.model.shapes.BigIntegerShape;
+import software.amazon.smithy.model.shapes.BlobShape;
+import software.amazon.smithy.model.shapes.BooleanShape;
+import software.amazon.smithy.model.shapes.ByteShape;
+import software.amazon.smithy.model.shapes.DocumentShape;
+import software.amazon.smithy.model.shapes.DoubleShape;
+import software.amazon.smithy.model.shapes.FloatShape;
+import software.amazon.smithy.model.shapes.IntegerShape;
+import software.amazon.smithy.model.shapes.ListShape;
+import software.amazon.smithy.model.shapes.LongShape;
+import software.amazon.smithy.model.shapes.MapShape;
+import software.amazon.smithy.model.shapes.MemberShape;
+import software.amazon.smithy.model.shapes.OperationShape;
+import software.amazon.smithy.model.shapes.ResourceShape;
+import software.amazon.smithy.model.shapes.ServiceShape;
+import software.amazon.smithy.model.shapes.Shape;
+import software.amazon.smithy.model.shapes.ShapeVisitor;
+import software.amazon.smithy.model.shapes.ShortShape;
+import software.amazon.smithy.model.shapes.StringShape;
+import software.amazon.smithy.model.shapes.StructureShape;
+import software.amazon.smithy.model.shapes.TimestampShape;
+import software.amazon.smithy.model.shapes.UnionShape;
+import software.amazon.smithy.model.traits.LengthTrait;
+import software.amazon.smithy.model.traits.RangeTrait;
+import software.amazon.smithy.model.validation.AbstractValidator;
+import software.amazon.smithy.model.validation.ValidationEvent;
+import software.amazon.smithy.utils.ListUtils;
+import software.amazon.smithy.utils.SmithyUnstableApi;
+
+/**
+ * Validates {@link OperationContextParamsTrait} traits.
+ */
+@SmithyUnstableApi
+public final class OperationContextParamsTraitValidator extends AbstractValidator {
+    @Override
+    public List<ValidationEvent> validate(Model model) {
+        ContextIndex index = ContextIndex.of(model);
+        List<ValidationEvent> events = new ArrayList<>();
+        for (OperationShape operationShape : model.getOperationShapes()) {
+            Map<String, OperationContextParamDefinition> definitionMap = index.getOperationContextParams(operationShape)
+                    .map(OperationContextParamsTrait::getParameters)
+                    .orElse(Collections.emptyMap());
+            for (Map.Entry<String, OperationContextParamDefinition> entry : definitionMap.entrySet()) {
+
+                try {
+                    JmespathExpression path = JmespathExpression.parse(entry.getValue().getPath());
+                    StructureShape input = OperationIndex.of(model).expectInputShape(operationShape);
+                    LinterResult linterResult = path.lint(createCurrentNodeFromShape(input, model));
+
+                    if (!linterResult.getProblems().isEmpty()) {
+                        events.add(error(operationShape,
+                                String.format("The operation `%s` is marked with `%s` which contains a "
+                                                + "path `%s` with an invalid JMESPath path `%s`: %s.",
+                                        operationShape.getId(),
+                                        OperationContextParamsTrait.ID.toString(),
+                                        entry.getKey(),
+                                        entry.getValue().getPath(),
+                                        linterResult.getProblems().stream()
+                                                .map(p -> "'" + p.message + "'")
+                                                .collect(Collectors.joining(", "))
+                                )));
+                    }
+
+                    List<String> unsupportedExpressions = path.accept(new UnsupportedJmesPathVisitor());
+                    if (!unsupportedExpressions.isEmpty()) {
+                        events.add(error(operationShape,
+                                String.format("The operation `%s` is marked with `%s` which contains a "
+                                                + "key `%s` with a JMESPath path `%s` with "
+                                                + "unsupported expressions: %s.",
+                                        operationShape.getId(),
+                                        OperationContextParamsTrait.ID.toString(),
+                                        entry.getKey(),
+                                        entry.getValue().getPath(),
+                                        unsupportedExpressions.stream()
+                                                .map(e -> "'" + e + "'")
+                                                .collect(Collectors.joining(", "))
+                                )));
+                    }
+                } catch (JmespathException e) {
+                    events.add(error(operationShape,
+                            String.format("The operation `%s` is marked with `%s` which contains a "
+                                            + "key `%s` with an unparseable JMESPath path `%s`: %s.",
+                                    operationShape.getId(),
+                                    OperationContextParamsTrait.ID.toString(),
+                                    entry.getKey(),
+                                    entry.getValue().getPath(),
+                                    e.getMessage()
+                            )));
+                }
+            }
+        }
+        return events;
+    }
+
+    private LiteralExpression createCurrentNodeFromShape(Shape shape, Model model) {
+        return shape == null
+                ? LiteralExpression.ANY
+                : new LiteralExpression(shape.accept(new ModelRuntimeTypeGenerator(model)));
+    }
+
+    private static final class UnsupportedJmesPathVisitor implements ExpressionVisitor<List<String>> {
+
+        @Override
+        public List<String> visitComparator(ComparatorExpression expression) {
+            return ListUtils.of("comparator");
+        }
+
+        @Override
+        public List<String> visitCurrentNode(CurrentExpression expression) {
+            return ListUtils.of("current node");
+        }
+
+        @Override
+        public List<String> visitExpressionType(ExpressionTypeExpression expression) {
+            return expression.getExpression().accept(this);
+        }
+
+        @Override
+        public List<String> visitFlatten(FlattenExpression expression) {
+            return ListUtils.of("flatten");
+        }
+
+        @Override
+        public List<String> visitFunction(FunctionExpression expression) {
+            if (expression.getName().equals("keys")) {
+                return expression.getArguments().stream()
+                        .map(e -> e.accept(this))
+                        .flatMap(List::stream)
+                        .collect(Collectors.toList());
+            } else {
+                return ListUtils.of("`" + expression.getName() + "` function");
+            }
+        }
+
+        @Override
+        public List<String> visitField(FieldExpression expression) {
+            return Collections.emptyList();
+        }
+
+        @Override
+        public List<String> visitIndex(IndexExpression expression) {
+            return ListUtils.of("index");
+        }
+
+        @Override
+        public List<String> visitLiteral(LiteralExpression expression) {
+            return ListUtils.of("literal");
+        }
+
+        @Override
+        public List<String> visitMultiSelectList(MultiSelectListExpression expression) {
+            return ListUtils.of("multiselect list");
+        }
+
+        @Override
+        public List<String> visitMultiSelectHash(MultiSelectHashExpression expression) {
+            return ListUtils.of("multiselect hash");
+        }
+
+        @Override
+        public List<String> visitAnd(AndExpression expression) {
+            return ListUtils.of("and");
+        }
+
+        @Override
+        public List<String> visitOr(OrExpression expression) {
+            return ListUtils.of("or");
+        }
+
+        @Override
+        public List<String> visitNot(NotExpression expression) {
+            return ListUtils.of("not");
+        }
+
+        @Override
+        public List<String> visitProjection(ProjectionExpression expression) {
+            return Collections.emptyList();
+        }
+
+        @Override
+        public List<String> visitFilterProjection(FilterProjectionExpression expression) {
+            return ListUtils.of("filter projection");
+        }
+
+        @Override
+        public List<String> visitObjectProjection(ObjectProjectionExpression expression) {
+            List<String> unsupported = new ArrayList<>();
+            unsupported.addAll(expression.getLeft().accept(this));
+            unsupported.addAll(expression.getRight().accept(this));
+            return Collections.unmodifiableList(unsupported);
+        }
+
+        @Override
+        public List<String> visitSlice(SliceExpression expression) {
+            return ListUtils.of("slice");
+        }
+
+        @Override
+        public List<String> visitSubexpression(Subexpression expression) {
+            List<String> unsupported = new ArrayList<>();
+            unsupported.addAll(expression.getLeft().accept(this));
+            unsupported.addAll(expression.getRight().accept(this));
+            return Collections.unmodifiableList(unsupported);
+        }
+    }
+
+    /**
+     * This class is duplicated from
+     * smithy-waiters/src/main/java/software/amazon/smithy/waiters/ModelRuntimeTypeGenerator.java
+     *
+     * It is duplicated here to avoid taking a dependency on smithy-waiters.
+     */
+    private static final class ModelRuntimeTypeGenerator implements ShapeVisitor<Object> {
+
+        private final Model model;
+        private Set<MemberShape> visited = new HashSet<>();
+
+        ModelRuntimeTypeGenerator(Model model) {
+            this.model = model;
+        }
+
+        @Override
+        public Object blobShape(BlobShape shape) {
+            return "blob";
+        }
+
+        @Override
+        public Object booleanShape(BooleanShape shape) {
+            return true;
+        }
+
+        @Override
+        public Object byteShape(ByteShape shape) {
+            return computeRange(shape);
+        }
+
+        @Override
+        public Object shortShape(ShortShape shape) {
+            return computeRange(shape);
+        }
+
+        @Override
+        public Object integerShape(IntegerShape shape) {
+            return computeRange(shape);
+        }
+
+        @Override
+        public Object longShape(LongShape shape) {
+            return computeRange(shape);
+        }
+
+        @Override
+        public Object floatShape(FloatShape shape) {
+            return computeRange(shape);
+        }
+
+        @Override
+        public Object doubleShape(DoubleShape shape) {
+            return computeRange(shape);
+        }
+
+        @Override
+        public Object bigIntegerShape(BigIntegerShape shape) {
+            return computeRange(shape);
+        }
+
+        @Override
+        public Object bigDecimalShape(BigDecimalShape shape) {
+            return computeRange(shape);
+        }
+
+        @Override
+        public Object documentShape(DocumentShape shape) {
+            return LiteralExpression.ANY;
+        }
+
+        @Override
+        public Object stringShape(StringShape shape) {
+            // Create a random string that does not exceed or go under the length trait.
+            int chars = computeLength(shape);
+
+            // Fill a string with "a"'s up to chars.
+            return new String(new char[chars]).replace("\0", "a");
+        }
+
+        @Override
+        public Object listShape(ListShape shape) {
+            return withCopiedVisitors(() -> {
+                int size = computeLength(shape);
+                List<Object> result = new ArrayList<>(size);
+                Object memberValue = shape.getMember().accept(this);
+                if (memberValue != null) {
+                    for (int i = 0; i < size; i++) {
+                        result.add(memberValue);
+                    }
+                }
+                return result;
+            });
+        }
+
+        // Visits members and mutates a copy of the current set of visited
+        // shapes rather than a shared set. This allows a shape to be used
+        // multiple times in the closure of a single shape without causing the
+        // reuse of the shape to always be assumed to be a recursive type.
+        private Object withCopiedVisitors(Supplier<Object> supplier) {
+            // Account for recursive shapes at the current
+            Set<MemberShape> visitedCopy = new HashSet<>(visited);
+            Object result = supplier.get();
+            visited = visitedCopy;
+            return result;
+        }
+
+        @Override
+        public Object mapShape(MapShape shape) {
+            return withCopiedVisitors(() -> {
+                int size = computeLength(shape);
+                Map<String, Object> result = new HashMap<>();
+                String key = (String) shape.getKey().accept(this);
+                Object memberValue = shape.getValue().accept(this);
+                for (int i = 0; i < size; i++) {
+                    result.put(key + i, memberValue);
+                }
+                return result;
+            });
+        }
+
+        @Override
+        public Object structureShape(StructureShape shape) {
+            return structureOrUnion(shape);
+        }
+
+        @Override
+        public Object unionShape(UnionShape shape) {
+            return structureOrUnion(shape);
+        }
+
+        private Object structureOrUnion(Shape shape) {
+            return withCopiedVisitors(() -> {
+                Map<String, Object> result = new LinkedHashMap<>();
+                for (MemberShape member : shape.members()) {
+                    Object memberValue = member.accept(this);
+                    result.put(member.getMemberName(), memberValue);
+                }
+                return result;
+            });
+        }
+
+        @Override
+        public Object memberShape(MemberShape shape) {
+            // Account for recursive shapes.
+            // A false return value means it was in the set.
+            if (!visited.add(shape)) {
+                return LiteralExpression.ANY;
+            }
+
+            return model.getShape(shape.getTarget())
+                    .map(target -> target.accept(this))
+                    // Rather than fail on broken models during waiter validation,
+                    // return an ANY to get *some* validation.
+                    .orElse(LiteralExpression.ANY);
+        }
+
+        @Override
+        public Object timestampShape(TimestampShape shape) {
+            return LiteralExpression.NUMBER;
+        }
+
+        @Override
+        public Object operationShape(OperationShape shape) {
+            throw new UnsupportedOperationException(shape.toString());
+        }
+
+        @Override
+        public Object resourceShape(ResourceShape shape) {
+            throw new UnsupportedOperationException(shape.toString());
+        }
+
+        @Override
+        public Object serviceShape(ServiceShape shape) {
+            throw new UnsupportedOperationException(shape.toString());
+        }
+
+        private int computeLength(Shape shape) {
+            // Create a random string that does not exceed or go under the length trait.
+            int chars = 2;
+
+            if (shape.hasTrait(LengthTrait.class)) {
+                LengthTrait trait = shape.expectTrait(LengthTrait.class);
+                if (trait.getMin().isPresent()) {
+                    chars = Math.max(chars, trait.getMin().get().intValue());
+                }
+                if (trait.getMax().isPresent()) {
+                    chars = Math.min(chars, trait.getMax().get().intValue());
+                }
+            }
+
+            return chars;
+        }
+
+        private double computeRange(Shape shape) {
+            // Create a random string that does not exceed or go under the range trait.
+            double i = 8;
+
+            if (shape.hasTrait(RangeTrait.class)) {
+                RangeTrait trait = shape.expectTrait(RangeTrait.class);
+                if (trait.getMin().isPresent()) {
+                    i = Math.max(i, trait.getMin().get().doubleValue());
+                }
+                if (trait.getMax().isPresent()) {
+                    i = Math.min(i, trait.getMax().get().doubleValue());
+                }
+            }
+
+            return i;
+        }
+    }
+}
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/traits/StaticContextParamsTraitValidator.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/traits/StaticContextParamsTraitValidator.java
index b5aa8bfe2..6197aa3fa 100644
--- a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/traits/StaticContextParamsTraitValidator.java
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/traits/StaticContextParamsTraitValidator.java
@@ -31,7 +31,7 @@ public final class StaticContextParamsTraitValidator extends AbstractValidator {
                     .orElse(Collections.emptyMap());
             for (Map.Entry<String, StaticContextParamDefinition> entry : definitionMap.entrySet()) {
                 Node node = entry.getValue().getValue();
-                if (node.isStringNode() || node.isBooleanNode()) {
+                if (supportedType(node)) {
                     continue;
                 }
                 events.add(error(operationShape,
@@ -45,4 +45,17 @@ public final class StaticContextParamsTraitValidator extends AbstractValidator {
         }
         return events;
     }
+
+    private static boolean supportedType(Node node) {
+        if (node.isStringNode() || node.isBooleanNode()) {
+            return true;
+        }
+
+        if (node.isArrayNode()) {
+            // all elements must be strings
+            return node.expectArrayNode().getElements().stream().allMatch(e -> e.isStringNode());
+        }
+
+        return false;
+    }
 }
diff --git a/smithy-rules-engine/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService b/smithy-rules-engine/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService
index ab79eaaa5..353f3b95e 100644
--- a/smithy-rules-engine/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService
+++ b/smithy-rules-engine/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService
@@ -1,5 +1,6 @@
 software.amazon.smithy.rulesengine.traits.ClientContextParamsTrait$Provider
 software.amazon.smithy.rulesengine.traits.ContextParamTrait$Provider
 software.amazon.smithy.rulesengine.traits.StaticContextParamsTrait$Provider
+software.amazon.smithy.rulesengine.traits.OperationContextParamsTrait$Provider
 software.amazon.smithy.rulesengine.traits.EndpointRuleSetTrait$Provider
 software.amazon.smithy.rulesengine.traits.EndpointTestsTrait$Provider
diff --git a/smithy-rules-engine/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator b/smithy-rules-engine/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
index af9a50287..2f24c9415 100644
--- a/smithy-rules-engine/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
+++ b/smithy-rules-engine/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
@@ -1,5 +1,6 @@
 software.amazon.smithy.rulesengine.traits.EndpointTestsTraitValidator
 software.amazon.smithy.rulesengine.traits.StaticContextParamsTraitValidator
+software.amazon.smithy.rulesengine.traits.OperationContextParamsTraitValidator
 software.amazon.smithy.rulesengine.validators.RuleSetAuthSchemesValidator
 software.amazon.smithy.rulesengine.validators.RuleSetBuiltInValidator
 software.amazon.smithy.rulesengine.validators.RuleSetUriValidator
diff --git a/smithy-rules-engine/src/main/resources/META-INF/smithy/smithy.rules.smithy b/smithy-rules-engine/src/main/resources/META-INF/smithy/smithy.rules.smithy
index de26a37bb..9555434d3 100644
--- a/smithy-rules-engine/src/main/resources/META-INF/smithy/smithy.rules.smithy
+++ b/smithy-rules-engine/src/main/resources/META-INF/smithy/smithy.rules.smithy
@@ -41,6 +41,18 @@ map staticContextParams {
     value: StaticContextParamDefinition
 }
 
+/// Binds one or more named rule-set parameters to elements contained in the operation's input structure.
+/// The type of the shapes targeted by the trait MUST match the parameter types defined in the rule-set.
+@unstable
+@trait(selector: "operation")
+map operationContextParams {
+    /// The rule-set parameter name.
+    key: String,
+
+    /// The static parameter definition.
+    value: OperationContextParamDefinition
+}
+
 /// Defines one or more named rule-set parameters to be generated as configurable client parameters.
 /// The type specified for the client parameter MUST match the parameter type defined in the rule-set.
 @unstable
@@ -62,6 +74,15 @@ structure StaticContextParamDefinition {
     value: Document
 }
 
+/// An operation context parameter definition.
+@unstable
+@private
+structure OperationContextParamDefinition {
+    /// a JMESPath expression to select element(s) from the operation input to bind to.
+    @required
+    path: String
+}
+
 /// A client context parameter definition.
 @unstable
 @private
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/TraitCodegenUtils.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/TraitCodegenUtils.java
index 2880b2bfa..3f9ad732b 100644
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/TraitCodegenUtils.java
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/TraitCodegenUtils.java
@@ -10,6 +10,7 @@ import software.amazon.smithy.codegen.core.ReservedWords;
 import software.amazon.smithy.codegen.core.ReservedWordsBuilder;
 import software.amazon.smithy.codegen.core.Symbol;
 import software.amazon.smithy.codegen.core.SymbolProvider;
+import software.amazon.smithy.model.shapes.MemberShape;
 import software.amazon.smithy.model.shapes.Shape;
 import software.amazon.smithy.model.traits.UniqueItemsTrait;
 import software.amazon.smithy.utils.CaseUtils;
@@ -134,4 +135,17 @@ public final class TraitCodegenUtils {
         }
         return shapeNamespace.replace(rootSmithyNamespace, packageNamespace);
     }
+
+    /**
+     * Determines if a given member represents a nullable type.
+     *
+     * @see <a href="https://smithy.io/2.0/spec/aggregate-types.html#structure-member-optionality">structure member optionality</a>
+     *
+     * @param shape member to check for nullability
+     *
+     * @return if the shape is a nullable type
+     */
+    public static boolean isNullableMember(MemberShape shape) {
+        return !shape.isRequired() && !shape.hasNonNullDefault();
+    }
 }
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/BuilderGenerator.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/BuilderGenerator.java
index 53423c4b7..6529039b0 100644
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/BuilderGenerator.java
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/BuilderGenerator.java
@@ -5,24 +5,44 @@
 
 package software.amazon.smithy.traitcodegen.generators;
 
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
 import java.util.Iterator;
 import java.util.Optional;
 import software.amazon.smithy.codegen.core.Symbol;
 import software.amazon.smithy.codegen.core.SymbolProvider;
 import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.node.Node;
+import software.amazon.smithy.model.shapes.BigDecimalShape;
+import software.amazon.smithy.model.shapes.BigIntegerShape;
+import software.amazon.smithy.model.shapes.BlobShape;
+import software.amazon.smithy.model.shapes.BooleanShape;
+import software.amazon.smithy.model.shapes.ByteShape;
+import software.amazon.smithy.model.shapes.DocumentShape;
+import software.amazon.smithy.model.shapes.DoubleShape;
+import software.amazon.smithy.model.shapes.FloatShape;
+import software.amazon.smithy.model.shapes.IntegerShape;
 import software.amazon.smithy.model.shapes.ListShape;
+import software.amazon.smithy.model.shapes.LongShape;
 import software.amazon.smithy.model.shapes.MapShape;
 import software.amazon.smithy.model.shapes.MemberShape;
 import software.amazon.smithy.model.shapes.Shape;
 import software.amazon.smithy.model.shapes.ShapeType;
 import software.amazon.smithy.model.shapes.ShapeVisitor;
+import software.amazon.smithy.model.shapes.ShortShape;
+import software.amazon.smithy.model.shapes.StringShape;
 import software.amazon.smithy.model.shapes.StructureShape;
+import software.amazon.smithy.model.shapes.TimestampShape;
+import software.amazon.smithy.model.shapes.UnionShape;
 import software.amazon.smithy.model.traits.AbstractTraitBuilder;
+import software.amazon.smithy.model.traits.DefaultTrait;
 import software.amazon.smithy.model.traits.StringListTrait;
+import software.amazon.smithy.model.traits.TimestampFormatTrait;
 import software.amazon.smithy.model.traits.TraitDefinition;
 import software.amazon.smithy.traitcodegen.SymbolProperties;
 import software.amazon.smithy.traitcodegen.TraitCodegenUtils;
-import software.amazon.smithy.traitcodegen.sections.BuilderClassSection;
 import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
 import software.amazon.smithy.utils.BuilderRef;
 import software.amazon.smithy.utils.SmithyBuilder;
@@ -61,7 +81,7 @@ final class BuilderGenerator implements Runnable {
     }
 
     private void writeBuilderClass() {
-        writer.pushState(new BuilderClassSection(symbol));
+        writer.writeDocString(writer.format("Builder for {@link $T}.", symbol));
         writer.writeInline("public static final class Builder $C", (Runnable) this::writeBuilderInterface);
         writer.indent();
         baseShape.accept(new BuilderPropertyGenerator());
@@ -72,7 +92,6 @@ final class BuilderGenerator implements Runnable {
         writer.openBlock("public $T build() {", "}", symbol,
                 () -> writer.write("return new $C;", (Runnable) this::writeBuilderReturn));
         writer.dedent().write("}");
-        writer.popState();
         writer.newLine();
     }
 
@@ -99,10 +118,7 @@ final class BuilderGenerator implements Runnable {
 
 
     private void writeToBuilderMethod() {
-        writer.openDocstring();
-        writer.writeDocStringContents("Creates a builder used to build a {@link $T}.", symbol);
-        writer.closeDocstring();
-        writer.override();
+        writer.writeDocString(writer.format("Creates a builder used to build a {@link $T}.", symbol));
         writer.openBlock("public $T<$T> toBuilder() {", "}",
                 SmithyBuilder.class, symbol, () -> {
             writer.writeInlineWithNoFormatting("return builder()");
@@ -177,6 +193,13 @@ final class BuilderGenerator implements Runnable {
                         symbolProvider.toSymbol(shape),
                         symbolProvider.toMemberName(shape),
                         builderRefOptional.orElseThrow(RuntimeException::new));
+                return;
+            }
+
+            if (shape.hasNonNullDefault()) {
+                writer.write("private $B $L = $C;", symbolProvider.toSymbol(shape),
+                        symbolProvider.toMemberName(shape),
+                        new DefaultInitializerGenerator(writer, model, symbolProvider, shape));
             } else {
                 writer.write("private $B $L;", symbolProvider.toSymbol(shape),
                         symbolProvider.toMemberName(shape));
@@ -310,4 +333,161 @@ final class BuilderGenerator implements Runnable {
             return model.expectShape(shape.getTarget()).accept(this);
         }
     }
+
+    /**
+     * Adds default values to builder properties.
+     */
+    private static final class DefaultInitializerGenerator extends ShapeVisitor.DataShapeVisitor<Void> implements
+            Runnable {
+        private final TraitCodegenWriter writer;
+        private final Model model;
+        private final SymbolProvider symbolProvider;
+        private final MemberShape member;
+        private Node defaultValue;
+
+        DefaultInitializerGenerator(
+                TraitCodegenWriter writer,
+                Model model,
+                SymbolProvider symbolProvider, MemberShape member
+        ) {
+            this.writer = writer;
+            this.model = model;
+            this.symbolProvider = symbolProvider;
+            this.member = member;
+        }
+
+        @Override
+        public void run() {
+            if (member.hasNonNullDefault()) {
+                this.defaultValue = member.expectTrait(DefaultTrait.class).toNode();
+                member.accept(this);
+            }
+        }
+
+        @Override
+        public Void blobShape(BlobShape blobShape) {
+            throw new UnsupportedOperationException("Blob default value cannot be set.");
+        }
+
+        @Override
+        public Void booleanShape(BooleanShape booleanShape) {
+            writer.write("$L", defaultValue.expectBooleanNode().getValue());
+            return null;
+        }
+
+        @Override
+        public Void listShape(ListShape listShape) {
+            throw new UnsupportedOperationException("List default values are not set with DefaultGenerator.");
+        }
+
+        @Override
+        public Void mapShape(MapShape mapShape) {
+            throw new UnsupportedOperationException("Map default values are not set with DefaultGenerator.");
+        }
+
+        @Override
+        public Void byteShape(ByteShape byteShape) {
+            // Bytes duplicate the integer toString method
+            writer.write("$L", defaultValue.expectNumberNode().getValue().intValue());
+            return null;
+        }
+
+        @Override
+        public Void shortShape(ShortShape shortShape) {
+            // Shorts duplicate the int toString method
+            writer.write("$L", defaultValue.expectNumberNode().getValue().intValue());
+            return null;
+        }
+
+        @Override
+        public Void integerShape(IntegerShape integerShape) {
+            writer.write("$L", defaultValue.expectNumberNode().getValue().intValue());
+            return null;
+        }
+
+        @Override
+        public Void longShape(LongShape longShape) {
+            writer.write("$LL", defaultValue.expectNumberNode().getValue().longValue());
+            return null;
+        }
+
+        @Override
+        public Void floatShape(FloatShape floatShape) {
+            writer.write("$Lf", defaultValue.expectNumberNode().getValue().floatValue());
+            return null;
+        }
+
+        @Override
+        public Void documentShape(DocumentShape documentShape) {
+            throw new UnsupportedOperationException("Document shape defaults cannot be set.");
+        }
+
+        @Override
+        public Void doubleShape(DoubleShape doubleShape) {
+            writer.write("$L", defaultValue.expectNumberNode().getValue().doubleValue());
+            return null;
+        }
+
+        @Override
+        public Void bigIntegerShape(BigIntegerShape bigIntegerShape) {
+            writer.write("$T.valueOf($L)", BigInteger.class, defaultValue.expectNumberNode().getValue().intValue());
+            return null;
+        }
+
+        @Override
+        public Void bigDecimalShape(BigDecimalShape bigDecimalShape) {
+            writer.write("$T.valueOf($L)", BigDecimal.class, defaultValue.expectNumberNode().getValue().doubleValue());
+            return null;
+        }
+
+        @Override
+        public Void stringShape(StringShape stringShape) {
+            writer.write("$S", defaultValue.expectStringNode().getValue());
+            return null;
+        }
+
+        @Override
+        public Void structureShape(StructureShape structureShape) {
+            throw new UnsupportedOperationException("Structure shape defaults cannot be set.");
+        }
+
+        @Override
+        public Void unionShape(UnionShape unionShape) {
+            throw new UnsupportedOperationException("Union shape defaults cannot be set.");
+
+        }
+
+        @Override
+        public Void memberShape(MemberShape memberShape) {
+            return model.expectShape(memberShape.getTarget()).accept(this);
+        }
+
+        @Override
+        public Void timestampShape(TimestampShape timestampShape) {
+            if (member.hasTrait(TimestampFormatTrait.class)) {
+                switch (member.expectTrait(TimestampFormatTrait.class).getFormat()) {
+                    case EPOCH_SECONDS:
+                        writer.writeInline(
+                                "$T.ofEpochSecond($LL)",
+                                Instant.class,
+                                defaultValue.expectNumberNode().getValue().longValue()
+                        );
+                        return null;
+                    case HTTP_DATE:
+                        writer.writeInline(
+                                "$T.from($T.RFC_1123_DATE_TIME.parse($S))",
+                                Instant.class,
+                                DateTimeFormatter.class,
+                                defaultValue.expectStringNode().getValue()
+                        );
+                        return null;
+                    default:
+                        // Fall through on default
+                        break;
+                }
+            }
+            writer.write("$T.parse($S)", Instant.class, defaultValue.expectStringNode().getValue());
+            return null;
+        }
+    }
 }
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/ConstructorGenerator.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/ConstructorGenerator.java
index 91fa81f06..46e873f52 100644
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/ConstructorGenerator.java
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/ConstructorGenerator.java
@@ -269,11 +269,11 @@ final class ConstructorGenerator extends TraitVisitor<Void> implements Runnable
         @Override
         public Void structureShape(StructureShape shape) {
             for (MemberShape member : shape.members()) {
-                if (member.isRequired()) {
+                if (TraitCodegenUtils.isNullableMember(member)) {
+                    writer.write("this.$L = $L;", symbolProvider.toMemberName(member), getBuilderValue(member));
+                } else {
                     writer.write("this.$1L = $2T.requiredState($1S, $3L);",
                             symbolProvider.toMemberName(member), SmithyBuilder.class, getBuilderValue(member));
-                } else {
-                    writer.write("this.$L = $L;", symbolProvider.toMemberName(member), getBuilderValue(member));
                 }
             }
             return null;
@@ -300,13 +300,17 @@ final class ConstructorGenerator extends TraitVisitor<Void> implements Runnable
         }
 
         private String getBuilderValue(MemberShape member) {
+            String memberName = symbolProvider.toMemberName(member);
+
             // If the member requires a builderRef we need to copy that builder ref value rather than use it directly.
             if (symbolProvider.toSymbol(member).getProperty(SymbolProperties.BUILDER_REF_INITIALIZER).isPresent()) {
-                return writer.format("builder.$1L.hasValue() ? builder.$1L.copy() : null",
-                        symbolProvider.toMemberName(member));
-            } else {
-                return writer.format("builder.$L", symbolProvider.toMemberName(member));
+                if (TraitCodegenUtils.isNullableMember(member)) {
+                    return writer.format("builder.$1L.hasValue() ? builder.$1L.copy() : null", memberName);
+                } else {
+                    return writer.format("builder.$1L.copy()", memberName);
+                }
             }
+            return writer.format("builder.$L", memberName);
         }
 
         private void writeValuesInitializer() {
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/EnumShapeGenerator.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/EnumShapeGenerator.java
index 58a44e265..41d9153ab 100644
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/EnumShapeGenerator.java
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/EnumShapeGenerator.java
@@ -129,14 +129,10 @@ abstract class EnumShapeGenerator implements Consumer<GenerateTraitDirective> {
     }
 
     private void writeFromMethod(Symbol enumSymbol, TraitCodegenWriter writer) {
-        writer.openDocstring();
-        writer.writeDocStringContents("Create a {@code $B} from a value in a model.", enumSymbol);
-        writer.writeDocStringContents("");
-        writer.writeDocStringContents("<p> Any unknown value is returned as {@code UNKNOWN}.");
-        writer.writeDocStringContents("");
-        writer.writeDocStringContents("@param value Value to create enum from.");
-        writer.writeDocStringContents("@return Returns the {@link $B} enum value.", enumSymbol);
-        writer.closeDocstring();
+        writer.writeDocString(writer.format("Create a {@code $1B} from a value in a model.\n\n"
+                + "<p>Any unknown value is returned as {@code UNKNOWN}.\n"
+                + "@param value Value to create enum from.\n"
+                + "@return Returns the {@link $1B} enum value.", enumSymbol));
         writer.openBlock("public static $B from($T value) {", "}",
                 enumSymbol, getValueType(), () -> {
                     writer.write("$T.requireNonNull(value, \"Enum value should not be null.\");", Objects.class);
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/FromNodeGenerator.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/FromNodeGenerator.java
index 8d71f05fd..5920c7ba1 100644
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/FromNodeGenerator.java
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/FromNodeGenerator.java
@@ -172,15 +172,11 @@ final class FromNodeGenerator extends TraitVisitor<Void> implements Runnable {
     }
 
     private void writeFromNodeJavaDoc() {
-        // Add docstring for method
-        writer.openDocstring();
-        writer.writeDocStringContents("Creates a {@link $T} from a {@link Node}.", symbol);
-        writer.writeDocStringContents("");
-        writer.writeDocStringContents("@param node Node to create the $T from.", symbol);
-        writer.writeDocStringContents("@return Returns the created $T.", symbol);
-        writer.writeDocStringContents("@throws $T if the given Node is invalid.",
-                ExpectationNotMetException.class);
-        writer.closeDocstring();
+        writer.writeDocString(writer.format("Creates a {@link $1T} from a {@link Node}.\n\n"
+                    + "@param node Node to create the $1T from.\n"
+                    + "@return Returns the created $1T.\n"
+                    + "@throws $2T if the given Node is invalid.\n",
+                symbol, ExpectationNotMetException.class));
     }
 
     /**
@@ -194,7 +190,7 @@ final class FromNodeGenerator extends TraitVisitor<Void> implements Runnable {
         private MemberGenerator(MemberShape member) {
             this.fieldName = member.getMemberName();
             this.memberName = symbolProvider.toMemberName(member);
-            this.memberPrefix = member.isRequired() ? ".expect" : ".get";
+            this.memberPrefix = (member.isRequired() && !member.hasNonNullDefault()) ? ".expect" : ".get";
         }
 
         @Override
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/GetterGenerator.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/GetterGenerator.java
index 946816e67..3da55b4d0 100644
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/GetterGenerator.java
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/GetterGenerator.java
@@ -116,14 +116,7 @@ final class GetterGenerator implements Runnable {
                 // If the member is required or the type does not require an optional wrapper (such as a list or map)
                 // then do not wrap return in an Optional
                 writer.pushState(new GetterSection(member));
-                if (member.isRequired()) {
-                    writer.openBlock("public $T get$U() {", "}",
-                            symbolProvider.toSymbol(member),
-                            symbolProvider.toMemberName(member),
-                            () -> writer.write("return $L;", symbolProvider.toMemberName(member)));
-                    writer.popState();
-                    writer.newLine();
-                } else {
+                if (TraitCodegenUtils.isNullableMember(member)) {
                     writer.openBlock("public $T<$T> get$U() {", "}",
                             Optional.class, symbolProvider.toSymbol(member), symbolProvider.toMemberName(member),
                             () -> writer.write("return $T.ofNullable($L);",
@@ -140,6 +133,12 @@ final class GetterGenerator implements Runnable {
                                 symbolProvider.toMemberName(member),
                                 () -> writer.write("return $L;", symbolProvider.toMemberName(member)));
                     }
+                } else {
+                    writer.openBlock("public $T get$U() {", "}",
+                            symbolProvider.toSymbol(member),
+                            symbolProvider.toMemberName(member),
+                            () -> writer.write("return $L;", symbolProvider.toMemberName(member)));
+                    writer.popState();
                 }
                 writer.newLine();
             }
@@ -159,11 +158,9 @@ final class GetterGenerator implements Runnable {
         }
 
         private void generateEnumValueGetterDocstring(Symbol symbol) {
-            writer.openDocstring();
-            writer.writeDocStringContents("Gets the {@code $1T} value as a {@code $1B} enum.", symbol);
-            writer.writeDocStringContents("");
-            writer.writeDocStringContents("@return Returns the {@code $B} enum.", symbol);
-            writer.closeDocstring();
+            writer.writeDocString(writer.format("Gets the {@code $1T} value as a {@code $1B} enum.\n\n"
+                    + "@return Returns the {@code $1B} enum.",
+                    symbol));
         }
 
         private void generateValuesGetter(Shape shape) {
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/ToNodeGenerator.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/ToNodeGenerator.java
index f5a0eff40..fb91bcf41 100644
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/ToNodeGenerator.java
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/ToNodeGenerator.java
@@ -139,14 +139,14 @@ final class ToNodeGenerator implements Runnable {
                 writer.writeInline(".sourceLocation(getSourceLocation())");
             }
             for (MemberShape mem : shape.members()) {
-                if (mem.isRequired()) {
-                    writer.write(".withMember($S, $C)",
-                            mem.getMemberName(),
-                            (Runnable) () -> mem.accept(new ToNodeMapperVisitor(symbolProvider.toMemberName(mem))));
-                } else {
+                if (TraitCodegenUtils.isNullableMember(mem)) {
                     writer.write(".withOptionalMember($S, get$U().map(m -> $C))",
                             mem.getMemberName(), symbolProvider.toMemberName(mem),
                             (Runnable) () -> mem.accept(new ToNodeMapperVisitor("m")));
+                } else {
+                    writer.write(".withMember($S, $C)",
+                            mem.getMemberName(),
+                            (Runnable) () -> mem.accept(new ToNodeMapperVisitor(symbolProvider.toMemberName(mem))));
                 }
             }
             writer.writeWithNoFormatting(".build();");
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/annotations/AnnotationIntegration.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/annotations/AnnotationIntegration.java
new file mode 100644
index 000000000..5225a82b1
--- /dev/null
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/annotations/AnnotationIntegration.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.traitcodegen.integrations.annotations;
+
+import java.util.List;
+import software.amazon.smithy.traitcodegen.TraitCodegenContext;
+import software.amazon.smithy.traitcodegen.integrations.TraitCodegenIntegration;
+import software.amazon.smithy.traitcodegen.integrations.javadoc.JavaDocIntegration;
+import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
+import software.amazon.smithy.utils.CodeInterceptor;
+import software.amazon.smithy.utils.CodeSection;
+import software.amazon.smithy.utils.ListUtils;
+
+/**
+ * Adds Java annotations to generated Java classes.
+ */
+public class AnnotationIntegration implements TraitCodegenIntegration {
+    @Override
+    public String name() {
+        return "annotations";
+    }
+
+    @Override
+    public List<String> runBefore() {
+        return ListUtils.of(JavaDocIntegration.NAME);
+    }
+
+    @Override
+    public List<? extends CodeInterceptor<? extends CodeSection, TraitCodegenWriter>> interceptors(
+            TraitCodegenContext codegenContext) {
+        return ListUtils.of(
+            new SmithyGeneratedAnnotationInterceptor(),
+            new DeprecatedAnnotationInterceptor(),
+            new UnstableAnnotationInterceptor()
+        );
+    }
+}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/annotations/DeprecatedAnnotationInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/annotations/DeprecatedAnnotationInterceptor.java
new file mode 100644
index 000000000..28d087f44
--- /dev/null
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/annotations/DeprecatedAnnotationInterceptor.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.traitcodegen.integrations.annotations;
+
+import software.amazon.smithy.model.traits.DeprecatedTrait;
+import software.amazon.smithy.traitcodegen.sections.ClassSection;
+import software.amazon.smithy.traitcodegen.sections.EnumVariantSection;
+import software.amazon.smithy.traitcodegen.sections.GetterSection;
+import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
+import software.amazon.smithy.utils.CodeInterceptor;
+import software.amazon.smithy.utils.CodeSection;
+
+/**
+ * Adds the {@link Deprecated} annotation to generated code for shapes with the {@link DeprecatedTrait}.
+ */
+final class DeprecatedAnnotationInterceptor implements CodeInterceptor.Prepender<CodeSection, TraitCodegenWriter> {
+
+    @Override
+    public Class<CodeSection> sectionType() {
+        return CodeSection.class;
+    }
+
+    @Override
+    public boolean isIntercepted(CodeSection section) {
+        if (section instanceof ClassSection) {
+            return ((ClassSection) section).shape().hasTrait(DeprecatedTrait.class);
+        } else if (section instanceof GetterSection) {
+            return ((GetterSection) section).shape().hasTrait(DeprecatedTrait.class);
+        } else if (section instanceof EnumVariantSection) {
+            return  ((EnumVariantSection) section).memberShape().hasTrait(DeprecatedTrait.class);
+        }
+        return false;
+    }
+
+    @Override
+    public void prepend(TraitCodegenWriter writer, CodeSection section) {
+        writer.write("@$T", Deprecated.class);
+    }
+}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/annotations/SmithyGeneratedAnnotationInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/annotations/SmithyGeneratedAnnotationInterceptor.java
new file mode 100644
index 000000000..3ada672c6
--- /dev/null
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/annotations/SmithyGeneratedAnnotationInterceptor.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.traitcodegen.integrations.annotations;
+
+import software.amazon.smithy.traitcodegen.sections.ClassSection;
+import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
+import software.amazon.smithy.utils.CodeInterceptor;
+import software.amazon.smithy.utils.SmithyGenerated;
+
+/**
+ * Adds the {@link software.amazon.smithy.utils.SmithyGenerated} annotation to all generated classes.
+ */
+final class SmithyGeneratedAnnotationInterceptor
+        implements CodeInterceptor.Prepender<ClassSection, TraitCodegenWriter> {
+    @Override
+    public Class<ClassSection> sectionType() {
+        return ClassSection.class;
+    }
+
+    @Override
+    public void prepend(TraitCodegenWriter writer, ClassSection section) {
+        writer.write("@$T", SmithyGenerated.class);
+    }
+}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/annotations/UnstableAnnotationInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/annotations/UnstableAnnotationInterceptor.java
new file mode 100644
index 000000000..5f8ba199d
--- /dev/null
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/annotations/UnstableAnnotationInterceptor.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.traitcodegen.integrations.annotations;
+
+import software.amazon.smithy.model.traits.UnstableTrait;
+import software.amazon.smithy.traitcodegen.sections.ClassSection;
+import software.amazon.smithy.traitcodegen.sections.EnumVariantSection;
+import software.amazon.smithy.traitcodegen.sections.GetterSection;
+import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
+import software.amazon.smithy.utils.CodeInterceptor;
+import software.amazon.smithy.utils.CodeSection;
+import software.amazon.smithy.utils.SmithyUnstableApi;
+
+/**
+ * Adds the {@link software.amazon.smithy.utils.SmithyUnstableApi} annotation to generated code for
+ * shapes with the {@link UnstableTrait}.
+ */
+final class UnstableAnnotationInterceptor implements CodeInterceptor.Prepender<CodeSection, TraitCodegenWriter> {
+    @Override
+    public Class<CodeSection> sectionType() {
+        return CodeSection.class;
+    }
+
+    @Override
+    public boolean isIntercepted(CodeSection section) {
+        if (section instanceof ClassSection) {
+            return ((ClassSection) section).shape().hasTrait(UnstableTrait.class);
+        } else if (section instanceof GetterSection) {
+            return ((GetterSection) section).shape().hasTrait(UnstableTrait.class);
+        } else if (section instanceof EnumVariantSection) {
+            return  ((EnumVariantSection) section).memberShape().hasTrait(UnstableTrait.class);
+        }
+        return false;
+    }
+
+    @Override
+    public void prepend(TraitCodegenWriter writer, CodeSection section) {
+        writer.write("@$T", SmithyUnstableApi.class);
+    }
+}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/core/CoreIntegration.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/core/CoreIntegration.java
index bd590dbec..5795638d7 100644
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/core/CoreIntegration.java
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/core/CoreIntegration.java
@@ -5,6 +5,7 @@
 
 package software.amazon.smithy.traitcodegen.integrations.core;
 
+import java.util.List;
 import software.amazon.smithy.codegen.core.Symbol;
 import software.amazon.smithy.codegen.core.SymbolProvider;
 import software.amazon.smithy.model.Model;
@@ -12,9 +13,13 @@ import software.amazon.smithy.model.shapes.MemberShape;
 import software.amazon.smithy.model.shapes.Shape;
 import software.amazon.smithy.model.traits.TraitDefinition;
 import software.amazon.smithy.traitcodegen.SymbolProperties;
+import software.amazon.smithy.traitcodegen.TraitCodegenContext;
 import software.amazon.smithy.traitcodegen.TraitCodegenSettings;
 import software.amazon.smithy.traitcodegen.TraitCodegenUtils;
 import software.amazon.smithy.traitcodegen.integrations.TraitCodegenIntegration;
+import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
+import software.amazon.smithy.utils.CodeInterceptor;
+import software.amazon.smithy.utils.CodeSection;
 import software.amazon.smithy.utils.ListUtils;
 import software.amazon.smithy.utils.SmithyInternalApi;
 
@@ -66,6 +71,13 @@ public final class CoreIntegration implements TraitCodegenIntegration {
         };
     }
 
+    @Override
+    public List<? extends CodeInterceptor<? extends CodeSection, TraitCodegenWriter>> interceptors(
+            TraitCodegenContext codegenContext
+    ) {
+        return ListUtils.of(new JavadocFormatterInterceptor());
+    }
+
     private Symbol getTraitSymbol(TraitCodegenSettings settings, Shape shape, Symbol baseSymbol) {
         String relativeNamespace = TraitCodegenUtils.mapNamespace(settings.smithyNamespace(),
                 shape.getId().getNamespace(), settings.packageName());
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/core/JavadocFormatterInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/core/JavadocFormatterInterceptor.java
new file mode 100644
index 000000000..eedc4bf2a
--- /dev/null
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/core/JavadocFormatterInterceptor.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.traitcodegen.integrations.core;
+
+import software.amazon.smithy.traitcodegen.sections.JavaDocSection;
+import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
+import software.amazon.smithy.utils.CodeInterceptor;
+
+/**
+ * Formats any populated Javadoc comment sections as documentation comments.
+ *
+ * <p>This interceptor will run after all other Javadoc interceptors to ensure it picks up all content
+ * added to Jav doc sections. Javadoc sections with no content are discarded.
+ */
+public class JavadocFormatterInterceptor implements CodeInterceptor<JavaDocSection, TraitCodegenWriter> {
+
+    @Override
+    public Class<JavaDocSection> sectionType() {
+        return JavaDocSection.class;
+    }
+
+    @Override
+    public void write(TraitCodegenWriter writer, String previousText, JavaDocSection section) {
+        if (!previousText.isEmpty()) {
+            writer.writeDocString(previousText);
+        }
+    }
+}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/BuilderClassSectionDocsInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/BuilderClassSectionDocsInterceptor.java
deleted file mode 100644
index a18bc95ca..000000000
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/BuilderClassSectionDocsInterceptor.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package software.amazon.smithy.traitcodegen.integrations.javadoc;
-
-
-import software.amazon.smithy.traitcodegen.sections.BuilderClassSection;
-import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
-import software.amazon.smithy.utils.CodeInterceptor;
-
-/**
- * Adds JavaDocs to static builder classes and is triggered by the {@link BuilderClassSection}.
- */
-final class BuilderClassSectionDocsInterceptor implements CodeInterceptor.Prepender<BuilderClassSection,
-        TraitCodegenWriter> {
-    @Override
-    public void prepend(TraitCodegenWriter writer, BuilderClassSection section) {
-        writer.openDocstring();
-        writer.writeDocStringContents("Builder for {@link $T}.", section.symbol());
-        writer.closeDocstring();
-    }
-
-    @Override
-    public Class<BuilderClassSection> sectionType() {
-        return BuilderClassSection.class;
-    }
-}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/ClassJavaDocInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/ClassJavaDocInterceptor.java
deleted file mode 100644
index 6586ea309..000000000
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/ClassJavaDocInterceptor.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package software.amazon.smithy.traitcodegen.integrations.javadoc;
-
-import software.amazon.smithy.model.traits.DocumentationTrait;
-import software.amazon.smithy.traitcodegen.sections.ClassSection;
-import software.amazon.smithy.traitcodegen.sections.JavaDocSection;
-import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
-import software.amazon.smithy.utils.CodeInterceptor;
-import software.amazon.smithy.utils.SmithyGenerated;
-
-/**
- * Adds basic JavaDocs for generated java classes.
- */
-final class ClassJavaDocInterceptor implements CodeInterceptor.Prepender<ClassSection, TraitCodegenWriter> {
-    @Override
-    public void prepend(TraitCodegenWriter writer, ClassSection section) {
-        writer.openDocstring();
-        writer.pushState(new JavaDocSection(section.shape()));
-        writer.writeDocStringContents(section.shape().expectTrait(DocumentationTrait.class).getValue());
-        writer.popState();
-        writer.closeDocstring();
-        // Adds smithy generated annotation to indicate class was generated
-        // by a code generator
-        writer.write("@$T", SmithyGenerated.class);
-    }
-
-    @Override
-    public Class<ClassSection> sectionType() {
-        return ClassSection.class;
-    }
-
-    @Override
-    public boolean isIntercepted(ClassSection section) {
-        return section.shape().hasTrait(DocumentationTrait.class);
-    }
-}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DeprecatedAnnotationClassInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DeprecatedAnnotationClassInterceptor.java
deleted file mode 100644
index 4990a0f8e..000000000
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DeprecatedAnnotationClassInterceptor.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package software.amazon.smithy.traitcodegen.integrations.javadoc;
-
-import software.amazon.smithy.model.traits.DeprecatedTrait;
-import software.amazon.smithy.traitcodegen.sections.ClassSection;
-import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
-import software.amazon.smithy.utils.CodeInterceptor;
-
-/**
- * Adds the {@code @Deprecated} annotation to generated classes if the smithy shape the class corresponds to
- * has the {@link DeprecatedTrait} trait applied.
- */
-final class DeprecatedAnnotationClassInterceptor implements CodeInterceptor.Prepender<ClassSection,
-        TraitCodegenWriter> {
-    @Override
-    public void prepend(TraitCodegenWriter writer, ClassSection section) {
-        writer.writeWithNoFormatting("@Deprecated");
-    }
-
-    @Override
-    public Class<ClassSection> sectionType() {
-        return ClassSection.class;
-    }
-
-    @Override
-    public boolean isIntercepted(ClassSection section) {
-        return section.shape().hasTrait(DeprecatedTrait.class);
-    }
-}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DeprecatedNoteInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DeprecatedInterceptor.java
similarity index 66%
rename from smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DeprecatedNoteInterceptor.java
rename to smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DeprecatedInterceptor.java
index 733b293c1..67f285a2d 100644
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DeprecatedNoteInterceptor.java
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DeprecatedInterceptor.java
@@ -16,15 +16,18 @@ import software.amazon.smithy.utils.CodeInterceptor;
  *
  * <p>If the {@code DeprecatedTrait} contains a {@code since} field, then "As of " note will be added
  * to the generated tag.
+ * <p>Note: This interceptor should be run after {@link SinceInterceptor} and {@link ExternalDocumentationInterceptor}
+ * to ensure proper tag ordering.
  */
-final class DeprecatedNoteInterceptor implements CodeInterceptor.Appender<JavaDocSection, TraitCodegenWriter> {
+final class DeprecatedInterceptor implements CodeInterceptor.Appender<JavaDocSection, TraitCodegenWriter> {
     @Override
     public void append(TraitCodegenWriter writer, JavaDocSection section) {
         DeprecatedTrait trait = section.shape().expectTrait(DeprecatedTrait.class);
-        writer.putContext("since", trait.getSince());
-        // Add spacing
-        writer.writeDocStringContents("");
-        writer.writeDocStringContents("@deprecated ${?since}As of ${since:L}. ${/since}$L", trait.getMessage());
+        // Only add the deprecated java doc tag if message or since message exist.
+        if (trait.getSince().isPresent() || trait.getMessage().isPresent()) {
+            writer.putContext("since", trait.getSince());
+            writer.write("@deprecated ${?since}As of ${since:L}. ${/since}$L", trait.getMessage());
+        }
     }
 
     @Override
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DocumentationTraitInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DocumentationTraitInterceptor.java
new file mode 100644
index 000000000..d15fde7ef
--- /dev/null
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DocumentationTraitInterceptor.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.traitcodegen.integrations.javadoc;
+
+import software.amazon.smithy.model.traits.DocumentationTrait;
+import software.amazon.smithy.traitcodegen.sections.JavaDocSection;
+import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
+import software.amazon.smithy.utils.CodeInterceptor;
+
+/**
+ * Adds Javadoc documentation for the {@link DocumentationTrait}.
+ *
+ * <p>The documentation trait contents are added as the contents of the Javadoc section.
+ */
+final class DocumentationTraitInterceptor implements CodeInterceptor<JavaDocSection, TraitCodegenWriter> {
+
+    @Override
+    public void write(TraitCodegenWriter writer, String previousText, JavaDocSection section) {
+        writer.write(section.shape().expectTrait(DocumentationTrait.class).getValue());
+
+        if (!previousText.isEmpty()) {
+            // Add spacing if tags have been added to the javadoc
+            writer.newLine();
+            writer.writeInlineWithNoFormatting(previousText);
+        }
+    }
+
+    @Override
+    public Class<JavaDocSection> sectionType() {
+        return JavaDocSection.class;
+    }
+
+    @Override
+    public boolean isIntercepted(JavaDocSection section) {
+        return section.shape().hasTrait(DocumentationTrait.class);
+    }
+}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/EnumVariantJavaDocInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/EnumVariantJavaDocInterceptor.java
deleted file mode 100644
index b3e75dfb3..000000000
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/EnumVariantJavaDocInterceptor.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package software.amazon.smithy.traitcodegen.integrations.javadoc;
-
-import software.amazon.smithy.model.traits.DocumentationTrait;
-import software.amazon.smithy.traitcodegen.sections.EnumVariantSection;
-import software.amazon.smithy.traitcodegen.sections.JavaDocSection;
-import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
-import software.amazon.smithy.utils.CodeInterceptor;
-
-/**
- * Adds a docstring to each java enum variant if the corresponding enum member has a doc comment.
- */
-final class EnumVariantJavaDocInterceptor implements CodeInterceptor.Prepender<EnumVariantSection,
-        TraitCodegenWriter> {
-    @Override
-    public void prepend(TraitCodegenWriter writer, EnumVariantSection section) {
-        DocumentationTrait trait = section.memberShape().expectTrait(DocumentationTrait.class);
-        writer.newLine();
-        writer.openDocstring();
-        writer.pushState(new JavaDocSection(section.memberShape()));
-        writer.writeDocStringContents(trait.getValue());
-        writer.popState();
-        writer.closeDocstring();
-    }
-
-    @Override
-    public Class<EnumVariantSection> sectionType() {
-        return EnumVariantSection.class;
-    }
-
-    @Override
-    public boolean isIntercepted(EnumVariantSection section) {
-        return section.memberShape().hasTrait(DocumentationTrait.class);
-    }
-}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/ExternalDocsInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/ExternalDocumentationInterceptor.java
similarity index 76%
rename from smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/ExternalDocsInterceptor.java
rename to smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/ExternalDocumentationInterceptor.java
index 6fb08ddac..519421da7 100644
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/ExternalDocsInterceptor.java
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/ExternalDocumentationInterceptor.java
@@ -14,16 +14,17 @@ import software.amazon.smithy.utils.CodeInterceptor;
 /**
  * Adds the javadoc {@code @see} tag to the generated javadocs if the corresponding smithy shape
  * has the {@link ExternalDocumentationTrait} trait applied.
+ *
+ * <p>Note: This interceptor should run before the {@link DeprecatedInterceptor} and {@link SinceInterceptor}
+ * to ensure proper ordering of Javadoc tags.
  */
-final class ExternalDocsInterceptor implements CodeInterceptor.Appender<JavaDocSection, TraitCodegenWriter> {
+final class ExternalDocumentationInterceptor implements CodeInterceptor.Appender<JavaDocSection, TraitCodegenWriter> {
 
     @Override
     public void append(TraitCodegenWriter writer, JavaDocSection section) {
         ExternalDocumentationTrait trait = section.shape().expectTrait(ExternalDocumentationTrait.class);
-        // Add a space to make it easier to read
-        writer.writeDocStringContents("");
         for (Map.Entry<String, String> entry : trait.getUrls().entrySet()) {
-            writer.writeDocStringContents("@see <a href=$S>$L</a>", entry.getKey(), entry.getValue());
+            writer.write("@see <a href=$S>$L</a>", entry.getValue(), entry.getKey());
         }
     }
 
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/GetterJavaDocInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/GetterJavaDocInterceptor.java
deleted file mode 100644
index c06cb2caa..000000000
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/GetterJavaDocInterceptor.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package software.amazon.smithy.traitcodegen.integrations.javadoc;
-
-import software.amazon.smithy.model.traits.DocumentationTrait;
-import software.amazon.smithy.traitcodegen.sections.GetterSection;
-import software.amazon.smithy.traitcodegen.sections.JavaDocSection;
-import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
-import software.amazon.smithy.utils.CodeInterceptor;
-
-/**
- * Adds Javadocs to the getter for a member if the corresponding member has a {@link DocumentationTrait}
- * trait applied. The Javadoc contents will just be the contents of the {@code DocumentationTrait}.
- */
-final class GetterJavaDocInterceptor implements CodeInterceptor.Prepender<GetterSection, TraitCodegenWriter> {
-    @Override
-    public void prepend(TraitCodegenWriter writer, GetterSection section) {
-        DocumentationTrait trait = section.shape().expectTrait(DocumentationTrait.class);
-        writer.newLine();
-        writer.openDocstring();
-        writer.pushState(new JavaDocSection(section.shape()));
-        writer.writeDocStringContents(trait.getValue());
-        writer.popState();
-        writer.closeDocstring();
-    }
-
-    @Override
-    public Class<GetterSection> sectionType() {
-        return GetterSection.class;
-    }
-
-    @Override
-    public boolean isIntercepted(GetterSection section) {
-        return section.shape().hasTrait(DocumentationTrait.class)
-                && section.shape().isMemberShape();
-    }
-}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/JavaDocInjectorInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/JavaDocInjectorInterceptor.java
new file mode 100644
index 000000000..6a089e5b5
--- /dev/null
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/JavaDocInjectorInterceptor.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.traitcodegen.integrations.javadoc;
+
+import software.amazon.smithy.model.shapes.Shape;
+import software.amazon.smithy.traitcodegen.sections.ClassSection;
+import software.amazon.smithy.traitcodegen.sections.EnumVariantSection;
+import software.amazon.smithy.traitcodegen.sections.GetterSection;
+import software.amazon.smithy.traitcodegen.sections.JavaDocSection;
+import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
+import software.amazon.smithy.utils.CodeInterceptor;
+import software.amazon.smithy.utils.CodeSection;
+
+/**
+ * Adds a javadoc section to classes, getters, and enum variants.
+ *
+ * <p>The injected javadoc section does not contain any javadoc contents when it is first injected. This
+ * section is instead populated by other {@code CodeInterceptors} that trigger off of specific traits.
+ * Note: This interceptor will also add any relevant documentation annotations to classes, getters, or enum variants.
+ */
+final class JavaDocInjectorInterceptor implements CodeInterceptor.Prepender<CodeSection, TraitCodegenWriter> {
+    @Override
+    public Class<CodeSection> sectionType() {
+        return CodeSection.class;
+    }
+
+    @Override
+    public boolean isIntercepted(CodeSection section) {
+        return section instanceof ClassSection
+                || section instanceof GetterSection
+                || section instanceof EnumVariantSection;
+    }
+
+    @Override
+    public void prepend(TraitCodegenWriter writer, CodeSection section) {
+        Shape shape;
+        if (section instanceof ClassSection) {
+            shape = ((ClassSection) section).shape();
+        } else if (section instanceof GetterSection) {
+            shape = ((GetterSection) section).shape();
+        } else if (section instanceof EnumVariantSection) {
+            shape = ((EnumVariantSection) section).memberShape();
+        } else {
+            throw new IllegalArgumentException("Javadocs cannot be injected for section: " + section);
+        }
+
+        writer.injectSection(new JavaDocSection(shape));
+    }
+}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/JavaDocIntegration.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/JavaDocIntegration.java
index 9d3724088..02a8b4c91 100644
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/JavaDocIntegration.java
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/JavaDocIntegration.java
@@ -18,29 +18,26 @@ import software.amazon.smithy.utils.SmithyInternalApi;
  * Adds all built-in Javadoc-generating interceptors.
  *
  * <p>This integration adds all the required documentation interceptors that ensure
- * that methods, classes, and properties all have JavaDocs added. This integration also
- * adds Annotations such as {@code @Deprecated} that serve as documentation.
+ * that methods, classes, and properties all have JavaDocs added.
  */
 @SmithyInternalApi
 public final class JavaDocIntegration implements TraitCodegenIntegration  {
+    public static final String NAME = "javadoc";
 
     @Override
     public String name() {
-        return "javadoc";
+        return NAME;
     }
 
     @Override
     public List<? extends CodeInterceptor<? extends CodeSection, TraitCodegenWriter>> interceptors(
             TraitCodegenContext codegenContext) {
         return ListUtils.of(
-                new DeprecatedAnnotationClassInterceptor(),
-                new DeprecatedNoteInterceptor(),
-                new UnstableAnnotationClassInterceptor(),
-                new ClassJavaDocInterceptor(),
-                new ExternalDocsInterceptor(),
-                new BuilderClassSectionDocsInterceptor(),
-                new GetterJavaDocInterceptor(),
-                new EnumVariantJavaDocInterceptor()
+                new JavaDocInjectorInterceptor(),
+                new ExternalDocumentationInterceptor(),
+                new SinceInterceptor(),
+                new DeprecatedInterceptor(),
+                new DocumentationTraitInterceptor()
         );
     }
 }
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/SinceInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/SinceInterceptor.java
new file mode 100644
index 000000000..a1c413b8f
--- /dev/null
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/SinceInterceptor.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.traitcodegen.integrations.javadoc;
+
+import software.amazon.smithy.model.traits.SinceTrait;
+import software.amazon.smithy.traitcodegen.sections.JavaDocSection;
+import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
+import software.amazon.smithy.utils.CodeInterceptor;
+
+/**
+ * Adds the {@code @since} javadoc tag to generated Javadoc documentation for a class
+ * if the smithy shape the class corresponds to has the {@link SinceTrait} trait applied.
+ *
+ * <p>Note: This interceptor should be run before {@link DeprecatedInterceptor} and after
+ * {@link ExternalDocumentationInterceptor} to ensure proper tag ordering.
+ */
+final class SinceInterceptor implements CodeInterceptor.Appender<JavaDocSection, TraitCodegenWriter> {
+    @Override
+    public void append(TraitCodegenWriter writer, JavaDocSection section) {
+        SinceTrait trait = section.shape().expectTrait(SinceTrait.class);
+        writer.write("@since $L", trait.getValue());
+    }
+
+    @Override
+    public Class<JavaDocSection> sectionType() {
+        return JavaDocSection.class;
+    }
+
+    @Override
+    public boolean isIntercepted(JavaDocSection section) {
+        return section.shape().hasTrait(SinceTrait.class);
+    }
+}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/UnstableAnnotationClassInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/UnstableAnnotationClassInterceptor.java
deleted file mode 100644
index bdaaef545..000000000
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/UnstableAnnotationClassInterceptor.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package software.amazon.smithy.traitcodegen.integrations.javadoc;
-
-import software.amazon.smithy.model.traits.UnstableTrait;
-import software.amazon.smithy.traitcodegen.sections.ClassSection;
-import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
-import software.amazon.smithy.utils.CodeInterceptor;
-import software.amazon.smithy.utils.SmithyUnstableApi;
-
-/**
- * Adds the {@code @SmithyUnstableApi} annotation to generated classes if the smithy shape the class corresponds to
- * has the {@link UnstableTrait} trait applied.
- */
-final class UnstableAnnotationClassInterceptor implements CodeInterceptor.Prepender<ClassSection, TraitCodegenWriter> {
-
-    @Override
-    public void prepend(TraitCodegenWriter writer, ClassSection section) {
-        writer.write("@$T", SmithyUnstableApi.class);
-    }
-
-    @Override
-    public Class<ClassSection> sectionType() {
-        return ClassSection.class;
-    }
-
-    @Override
-    public boolean isIntercepted(ClassSection section) {
-        return section.shape().hasTrait(UnstableTrait.class);
-    }
-}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/sections/BuilderClassSection.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/sections/BuilderClassSection.java
deleted file mode 100644
index de3fc05b9..000000000
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/sections/BuilderClassSection.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package software.amazon.smithy.traitcodegen.sections;
-
-import software.amazon.smithy.codegen.core.Symbol;
-import software.amazon.smithy.utils.CodeSection;
-
-/**
- * Contains the static builder class for a shape.
- */
-public final class BuilderClassSection implements CodeSection {
-    private final Symbol symbol;
-
-    public BuilderClassSection(Symbol symbol) {
-        this.symbol = symbol;
-    }
-
-    /**
-     * {@link Symbol} representing the enclosing class for this builder.
-     */
-    public Symbol symbol() {
-        return symbol;
-    }
-}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/writer/TraitCodegenWriter.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/writer/TraitCodegenWriter.java
index 98fa58946..cd350891d 100644
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/writer/TraitCodegenWriter.java
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/writer/TraitCodegenWriter.java
@@ -10,6 +10,7 @@ import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Scanner;
 import java.util.Set;
 import java.util.function.BiFunction;
 import java.util.regex.Matcher;
@@ -74,11 +75,14 @@ public class TraitCodegenWriter extends SymbolWriter<TraitCodegenWriter, TraitCo
         addImport(symbol, symbol.getName());
     }
 
-    public void openDocstring() {
-        pushState().writeWithNoFormatting("/**");
-    }
+    /**
+     * Writes the provided text in the format of a Java doc string.
+     *
+     * @param contents text to format as a doc string.
+     */
+    public void writeDocString(String contents) {
+        writeWithNoFormatting("/**");
 
-    public void writeDocStringContents(String contents) {
         // Split out any HTML-tag wrapped sections as we do not want to wrap
         // any customer documentation with tags
         Matcher matcher = PATTERN.matcher(contents);
@@ -86,28 +90,26 @@ public class TraitCodegenWriter extends SymbolWriter<TraitCodegenWriter, TraitCo
         writeInlineWithNoFormatting(" * ");
         while (matcher.find()) {
             // write all contents up to the match.
-            writeInlineWithNoFormatting(StringUtils.wrap(contents.substring(lastMatchPos, matcher.start())
-                            .replace("\n", "\n * "), MAX_LINE_LENGTH - 8,
-                    getNewline() + " * ", false));
+            writeDocStringLine(contents.substring(lastMatchPos, matcher.start()));
+
             // write match contents
             writeInlineWithNoFormatting(contents.substring(matcher.start(), matcher.end()).replace("\n", "\n * "));
             lastMatchPos = matcher.end();
         }
-        // Write out all remaining contents
-        writeWithNoFormatting(StringUtils.wrap(contents.substring(lastMatchPos).replace("\n", "\n * "),
-                MAX_LINE_LENGTH - 8,
-                getNewline() + " * ",
-                false));
-    }
 
-    public void writeDocStringContents(String contents, Object... args) {
-        writeInlineWithNoFormatting(" * ");
-        write(StringUtils.wrap(contents.replace("\n", "\n * "), MAX_LINE_LENGTH - 8,
-                getNewline() + " * ", false), args);
+        // Write out all remaining contents
+        writeDocStringLine(contents.substring(lastMatchPos));
+        writeWithNoFormatting("\n */");
     }
 
-    public void closeDocstring() {
-        writeWithNoFormatting(" */").popState();
+    private void writeDocStringLine(String string) {
+        for (Scanner it = new Scanner(string); it.hasNextLine();) {
+            String s = it.nextLine();
+            writeInlineWithNoFormatting(StringUtils.wrap(s, MAX_LINE_LENGTH, getNewline() + " * ", false));
+            if (it.hasNextLine()) {
+                writeInlineWithNoFormatting(getNewline() + " * ");
+            }
+        }
     }
 
     @Override
diff --git a/smithy-trait-codegen/src/main/resources/META-INF/services/software.amazon.smithy.traitcodegen.integrations.TraitCodegenIntegration b/smithy-trait-codegen/src/main/resources/META-INF/services/software.amazon.smithy.traitcodegen.integrations.TraitCodegenIntegration
index 86df7dfb4..05f33b559 100644
--- a/smithy-trait-codegen/src/main/resources/META-INF/services/software.amazon.smithy.traitcodegen.integrations.TraitCodegenIntegration
+++ b/smithy-trait-codegen/src/main/resources/META-INF/services/software.amazon.smithy.traitcodegen.integrations.TraitCodegenIntegration
@@ -1,3 +1,4 @@
 software.amazon.smithy.traitcodegen.integrations.core.CoreIntegration
 software.amazon.smithy.traitcodegen.integrations.javadoc.JavaDocIntegration
 software.amazon.smithy.traitcodegen.integrations.idref.IdRefDecoratorIntegration
+software.amazon.smithy.traitcodegen.integrations.annotations.AnnotationIntegration


Test patch:
diff --git a/smithy-aws-protocol-tests/model/ec2Query/input-lists.smithy b/smithy-aws-protocol-tests/model/ec2Query/input-lists.smithy
index 07eeb8ee2..12425358e 100644
--- a/smithy-aws-protocol-tests/model/ec2Query/input-lists.smithy
+++ b/smithy-aws-protocol-tests/model/ec2Query/input-lists.smithy
@@ -42,14 +42,16 @@ apply QueryLists @httpRequestTests([
     },
     {
         id: "Ec2EmptyQueryLists",
-        documentation: "Serializes empty query lists",
+        // Empty lists are NOT serialized for the EC2 query protocol, which differs from the AWS query protocol.
+        // See "EmptyQueryLists" for the AWS query protocol equivalent test case.
+        documentation: "Does not serialize empty query lists.",
         protocol: ec2Query,
         method: "POST",
         uri: "/",
         headers: {
             "Content-Type": "application/x-www-form-urlencoded"
         },
-        body: "Action=QueryLists&Version=2020-01-08&ListArg=",
+        body: "Action=QueryLists&Version=2020-01-08",
         bodyMediaType: "application/x-www-form-urlencoded",
         params: {
             ListArg: []
diff --git a/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/invalid-operation-context-params.errors b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/invalid-operation-context-params.errors
new file mode 100644
index 000000000..f499df1f5
--- /dev/null
+++ b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/invalid-operation-context-params.errors
@@ -0,0 +1,9 @@
+[WARNING] example#Bar: This shape applies a trait that is unstable: smithy.rules#operationContextParams | UnstableTrait.smithy.rules#operationContextParams
+[ERROR] example#Bar: The operation `example#Bar` is marked with `smithy.rules#operationContextParams` which contains a key `invalidJmesPath` with an unparseable JMESPath path `...`: Syntax error at line 1 column 1: Expected ['@', 'A-Z|a-z|_', '`', '*', '{', '[', '[]', '&', '!', '[?', '('], but found '.'. | OperationContextParamsTrait
+[ERROR] example#Bar: The operation `example#Bar` is marked with `smithy.rules#operationContextParams` which contains a path `arrayAsObject` with an invalid JMESPath path `listOfObjects.notAKey`: 'Object field 'notAKey' extraction performed on array'. | OperationContextParamsTrait
+[ERROR] example#Bar: The operation `example#Bar` is marked with `smithy.rules#operationContextParams` which contains a path `incorrectKey` with an invalid JMESPath path `nested.bar`: 'Object field 'bar' does not exist in object with properties [foo]'. | OperationContextParamsTrait
+[ERROR] example#Bar: The operation `example#Bar` is marked with `smithy.rules#operationContextParams` which contains a path `projectionOnScalar` with an invalid JMESPath path `nested.foo[*]`: 'Array projection performed on string'. | OperationContextParamsTrait
+[ERROR] example#Bar: The operation `example#Bar` is marked with `smithy.rules#operationContextParams` which contains a key `unsupportedFunction` with a JMESPath path `length(listOfObjects)` with unsupported expressions: '`length` function'. | OperationContextParamsTrait
+[ERROR] example#Bar: The operation `example#Bar` is marked with `smithy.rules#operationContextParams` which contains a key `unsupportedExpression` with a JMESPath path `listOfObjects[1]` with unsupported expressions: 'index'. | OperationContextParamsTrait
+
+
diff --git a/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/invalid-operation-context-params.smithy b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/invalid-operation-context-params.smithy
new file mode 100644
index 000000000..687e1148f
--- /dev/null
+++ b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/invalid-operation-context-params.smithy
@@ -0,0 +1,40 @@
+$version: "2.0"
+
+namespace example
+
+use smithy.rules#operationContextParams
+
+service FizzBuzz {
+    operations: [Bar]
+}
+
+@operationContextParams(
+    invalidJmesPath: {path: "..." },
+    arrayAsObject: {path: "listOfObjects.notAKey"},
+    incorrectKey: {path: "nested.bar"},
+    projectionOnScalar: {path: "nested.foo[*]"},
+    unsupportedFunction: {path: "length(listOfObjects)"},
+    unsupportedExpression: {path: "listOfObjects[1]"}
+)
+operation Bar {
+    input: BarInput
+}
+
+structure BarInput {
+    nested: Nested,
+    listOfObjects: ListOfObjects
+}
+
+list ListOfObjects {
+    member: ObjectMember
+}
+
+structure ObjectMember {
+    key: Key,
+}
+
+structure Nested {
+    foo: String,
+}
+
+string Key
diff --git a/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/valid/default-values.errors b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/valid/default-values.errors
index be1d71040..d40f70809 100644
--- a/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/valid/default-values.errors
+++ b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/valid/default-values.errors
@@ -1,3 +1,4 @@
 [WARNING] example#FizzBuzz: This shape applies a trait that is unstable: smithy.rules#clientContextParams | UnstableTrait
 [WARNING] example#FizzBuzz: This shape applies a trait that is unstable: smithy.rules#endpointRuleSet | UnstableTrait
 [WARNING] example#FizzBuzz: This shape applies a trait that is unstable: smithy.rules#endpointTests | UnstableTrait
+[WARNING] example#GetThing: This shape applies a trait that is unstable: smithy.rules#staticContextParams | UnstableTrait.smithy.rules#staticContextParams
diff --git a/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/valid/default-values.smithy b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/valid/default-values.smithy
index d63c041fe..f137a1872 100644
--- a/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/valid/default-values.smithy
+++ b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/valid/default-values.smithy
@@ -5,6 +5,7 @@ namespace example
 use smithy.rules#clientContextParams
 use smithy.rules#endpointRuleSet
 use smithy.rules#endpointTests
+use smithy.rules#staticContextParams
 
 @clientContextParams(
     bar: {type: "string", documentation: "a client string parameter"}
@@ -30,10 +31,16 @@ use smithy.rules#endpointTests
             default: "asdf"
             documentation: "docs"
         },
+        stringArrayParam: {
+            type: "stringArray",
+            required: true,
+            default: ["a", "b", "c"],
+            documentation: "docs"
+        }
     },
     rules: [
         {
-            "documentation": "Template the region into the URI when FIPS is enabled",
+            "documentation": "Template baz into URI when bar is set",
             "conditions": [
                 {
                     "fn": "isSet",
@@ -49,6 +56,25 @@ use smithy.rules#endpointTests
             },
             "type": "endpoint"
         },
+        {
+            "documentation": "Template first array value into URI",
+            "conditions": [
+                {
+                    "fn": "getAttr",
+                    "argv": [
+                        {
+                            "ref": "stringArrayParam"
+                        },
+                        "[0]"
+                    ],
+                    "assign": "arrayValue"
+                }
+            ],
+            "endpoint": {
+                "url": "https://example.com/{arrayValue}"
+            },
+            "type": "endpoint"
+        },
         {
             "conditions": [],
             "documentation": "error fallthrough",
@@ -94,6 +120,19 @@ use smithy.rules#endpointTests
             }
         },
         {
+            "documentation": "Default array values used"
+            "params": {
+            }
+            "expect": {
+                "endpoint": {
+                    "url": "https://example.com/a"
+                }
+            }
+        },
+        {
+            "params": {
+                "stringArrayParam": []
+            }
             "documentation": "a documentation string",
             "expect": {
                 "error": "endpoint error"
@@ -106,6 +145,9 @@ service FizzBuzz {
     operations: [GetThing]
 }
 
+@staticContextParams(
+    "stringArrayParam": {value: []}
+)
 operation GetThing {
     input := {}
 }
diff --git a/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/valid/operation-context-params.errors b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/valid/operation-context-params.errors
new file mode 100644
index 000000000..e5619e6a7
--- /dev/null
+++ b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/valid/operation-context-params.errors
@@ -0,0 +1,1 @@
+[WARNING] example#Bar: This shape applies a trait that is unstable: smithy.rules#operationContextParams | UnstableTrait.smithy.rules#operationContextParams
diff --git a/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/valid/operation-context-params.smithy b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/valid/operation-context-params.smithy
new file mode 100644
index 000000000..1a257d9e5
--- /dev/null
+++ b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/valid/operation-context-params.smithy
@@ -0,0 +1,51 @@
+$version: "2.0"
+
+namespace example
+
+use smithy.rules#operationContextParams
+
+service FizzBuzz {
+    operations: [Bar]
+}
+
+@operationContextParams(
+    toplLevelMember: {path: "resourceId" },
+    projection: {path: "listOfObjects[*].key"},
+    subExpression: {path: "nested.nested.bar"},
+    recursive: {path: "nested.nested.recursiveMember.foo"}
+    keysFunction: {path: "keys(map)"}
+)
+operation Bar {
+    input: BarInput
+}
+
+structure BarInput {
+    resourceId: String,
+    nested: Nested1,
+    listOfObjects: ListOfObjects,
+    map: Map
+}
+
+list ListOfObjects {
+    member: ObjectMember
+}
+
+structure ObjectMember {
+    key: String,
+}
+
+map Map {
+    key: String,
+    value: String
+}
+
+structure Nested1 {
+    foo: String,
+    nested: Nested2
+}
+
+structure Nested2 {
+    bar: String,
+    recursiveMember: Nested1
+}
+
diff --git a/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/valid/valid-model.smithy b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/valid/valid-model.smithy
index de5b5bc87..9d2e80212 100644
--- a/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/valid/valid-model.smithy
+++ b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/valid/valid-model.smithy
@@ -25,6 +25,10 @@ use smithy.rules#staticContextParams
         "ExtraParameter": {
             "type": "string",
             "documentation": "docs"
+        },
+        "StringArrayParameter": {
+            "type": "stringArray",
+            documentation: "docs"
         }
     },
     "rules": []
@@ -38,7 +42,8 @@ service FizzBuzz {
 
 @staticContextParams(
   "ParameterFoo": {value: "foo"},
-  "ExtraParameter": {value: "someValue"}
+  "ExtraParameter": {value: "someValue"},
+  "StringArrayParameter": {value: ["a", "b", "c"]}
 )
 operation GetResource {
  input: GetResourceInput
diff --git a/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/invalid-rules/invalid-param-type.json5 b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/invalid-rules/invalid-param-type.json5
index a2432b5ca..b4c642aca 100644
--- a/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/invalid-rules/invalid-param-type.json5
+++ b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/invalid-rules/invalid-param-type.json5
@@ -2,7 +2,7 @@
 // while parsing the parameter `RegionName`
 //   at invalid-rules/invalid-param-type.json5:10
 // while parsing the parameter type
-// Unexpected parameter type `notastring`. Expected `String` or `Boolean`.
+// Unexpected parameter type `notastring`. Expected `string`, `boolean`, or `stringArray`.
 //   at invalid-rules/invalid-param-type.json5:11
 {
   "version": "1.2",
diff --git a/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/traits/errorfiles/invalid-static-array-param-value.smithy b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/traits/errorfiles/invalid-static-array-param-value.smithy
index 88a5d8944..de5db2971 100644
--- a/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/traits/errorfiles/invalid-static-array-param-value.smithy
+++ b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/traits/errorfiles/invalid-static-array-param-value.smithy
@@ -4,5 +4,5 @@ namespace smithy.example
 
 use smithy.rules#staticContextParams
 
-@staticContextParams(arrayParam: {value: ["foo", "bar"]})
+@staticContextParams(arrayParam: {value: ["foo", 3]})
 operation OperationArray {}
diff --git a/smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/CreatesTraitTest.java b/smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/CreatesTraitTest.java
index 5cbb22733..0aa918b88 100644
--- a/smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/CreatesTraitTest.java
+++ b/smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/CreatesTraitTest.java
@@ -3,6 +3,7 @@ package software.amazon.smithy.traitcodegen.test;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import com.example.traits.StringTrait;
+import com.example.traits.defaults.StructDefaultsTrait;
 import com.example.traits.documents.DocumentTrait;
 import com.example.traits.documents.StructWithNestedDocumentTrait;
 import com.example.traits.enums.IntEnumTrait;
@@ -136,7 +137,9 @@ public class CreatesTraitTest {
                         SetMember.builder().a("second").b(2).c("more").build().toNode()
                 )),
                 // Strings
-                Arguments.of(StringTrait.ID, Node.from("SPORKZ SPOONS YAY! Utensils."))
+                Arguments.of(StringTrait.ID, Node.from("SPORKZ SPOONS YAY! Utensils.")),
+                // Defaults
+                Arguments.of(StructDefaultsTrait.ID, Node.objectNode())
         );
     }
 
diff --git a/smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/LoadsFromModelTest.java b/smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/LoadsFromModelTest.java
index 98eb7dc2e..377b4953f 100644
--- a/smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/LoadsFromModelTest.java
+++ b/smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/LoadsFromModelTest.java
@@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertIterableEquals;
 
 import com.example.traits.StringTrait;
+import com.example.traits.defaults.StructDefaultsTrait;
 import com.example.traits.documents.DocumentTrait;
 import com.example.traits.documents.StructWithNestedDocumentTrait;
 import com.example.traits.enums.IntEnumTrait;
@@ -210,7 +211,34 @@ public class LoadsFromModelTest {
                                 SetMember.builder().a("second").b(2).c("more").build()))),
                 // Strings
                 Arguments.of("string-trait.smithy", StringTrait.class,
-                        MapUtils.of("getValue","Testing String Trait"))
+                        MapUtils.of("getValue","Testing String Trait")),
+                // Defaults
+                Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
+                        MapUtils.of("getDefaultList", ListUtils.of())),
+                Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
+                        MapUtils.of("getDefaultMap", MapUtils.of())),
+                Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
+                        MapUtils.of("getDefaultBoolean", true)),
+                Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
+                        MapUtils.of("getDefaultString", "default")),
+                Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
+                        MapUtils.of("getDefaultByte", (byte) 1)),
+                Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
+                        MapUtils.of("getDefaultShort", (short) 1)),
+                Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
+                        MapUtils.of("getDefaultInt", 1)),
+                Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
+                        MapUtils.of("getDefaultLong", 1L)),
+                Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
+                        MapUtils.of("getDefaultFloat", 2.2F)),
+                Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
+                        MapUtils.of("getDefaultDouble", 1.1)),
+                Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
+                        MapUtils.of("getDefaultBigInt", new BigInteger("100"))),
+                Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
+                        MapUtils.of("getDefaultBigDecimal", new BigDecimal("100.01"))),
+                Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
+                        MapUtils.of("getDefaultTimestamp", Instant.parse("1985-04-12T23:20:50.52Z")))
         );
     }
 
diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/defaults/defaults.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/defaults/defaults.smithy
new file mode 100644
index 000000000..dbd2c1950
--- /dev/null
+++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/defaults/defaults.smithy
@@ -0,0 +1,9 @@
+$version: "2.0"
+
+namespace test.smithy.traitcodegen
+
+use test.smithy.traitcodegen.defaults#StructDefaults
+
+@StructDefaults
+structure myStruct {
+}
diff --git a/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/TraitCodegenPluginTest.java b/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/TraitCodegenPluginTest.java
index 75820a20e..916faabb0 100644
--- a/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/TraitCodegenPluginTest.java
+++ b/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/TraitCodegenPluginTest.java
@@ -17,6 +17,7 @@ import java.nio.file.Paths;
 import java.util.List;
 import java.util.Optional;
 import java.util.stream.Collectors;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import software.amazon.smithy.build.MockManifest;
 import software.amazon.smithy.build.PluginContext;
@@ -27,15 +28,22 @@ import software.amazon.smithy.model.node.ObjectNode;
 
 
 public class TraitCodegenPluginTest {
-    private static final int EXPECTED_NUMBER_OF_FILES = 55;
+    private static final int EXPECTED_NUMBER_OF_FILES = 56;
 
-    @Test
-    public void generatesExpectedTraitFiles() {
-        MockManifest manifest = new MockManifest();
-        Model model = Model.assembler()
+    private MockManifest manifest;
+    private Model model;
+
+    @BeforeEach
+    void setup() {
+        manifest = new MockManifest();
+        model = Model.assembler()
                 .discoverModels(getClass().getClassLoader())
                 .assemble()
                 .unwrap();
+    }
+
+    @Test
+    public void generatesExpectedTraitFiles() {
         PluginContext context = PluginContext.builder()
                 .fileManifest(manifest)
                 .settings(ObjectNode.builder()
@@ -63,11 +71,6 @@ public class TraitCodegenPluginTest {
 
     @Test
     public void filtersTags() {
-        MockManifest manifest = new MockManifest();
-        Model model = Model.assembler()
-                .discoverModels(getClass().getClassLoader())
-                .assemble()
-                .unwrap();
         PluginContext context = PluginContext.builder()
                 .fileManifest(manifest)
                 .settings(ObjectNode.builder()
@@ -89,11 +92,6 @@ public class TraitCodegenPluginTest {
 
     @Test
     public void addsHeaderLines() {
-        MockManifest manifest = new MockManifest();
-        Model model = Model.assembler()
-                .discoverModels(getClass().getClassLoader())
-                .assemble()
-                .unwrap();
         PluginContext context = PluginContext.builder()
                 .fileManifest(manifest)
                 .settings(ObjectNode.builder()
@@ -118,49 +116,4 @@ public class TraitCodegenPluginTest {
                 " * Header line two\n" +
                 " */"));
     }
-
-    @Test
-    public void doesNotFormatContentInsideHtmlTags() {
-        MockManifest manifest = new MockManifest();
-        Model model = Model.assembler()
-                .discoverModels(getClass().getClassLoader())
-                .assemble()
-                .unwrap();
-        PluginContext context = PluginContext.builder()
-                .fileManifest(manifest)
-                .settings(ObjectNode.builder()
-                        .withMember("package", "com.example.traits")
-                        .withMember("namespace", "test.smithy.traitcodegen")
-                        .withMember("header", ArrayNode.fromStrings("Header line one", "Header line two"))
-                        .build()
-                )
-                .model(model)
-                .build();
-
-        SmithyBuildPlugin plugin = new TraitCodegenPlugin();
-        plugin.execute(context);
-
-        assertFalse(manifest.getFiles().isEmpty());
-        assertEquals(EXPECTED_NUMBER_OF_FILES, manifest.getFiles().size());
-        Optional<String> fileStringOptional = manifest.getFileString(
-                Paths.get("com/example/traits/structures/StructureTrait.java").toString());
-        assertTrue(fileStringOptional.isPresent());
-        String expected = "    /**\n"
-                          + "     * Documentation includes preformatted text that should not be messed with. This sentence should still be partially\n"
-                          + "     * wrapped.\n"
-                          + "     * For example:\n"
-                          + "     * <pre>\n"
-                          + "     * Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n"
-                          + "     * </pre>\n"
-                          + "     *\n"
-                          + "     * <ul>\n"
-                          + "     *     <li> Lorem ipsum dolor sit amet, consectetur adipiscing elit Lorem ipsum dolor sit amet, consectetur adipiscing elit </li>\n"
-                          + "     *     <li> Lorem ipsum dolor sit amet, consectetur adipiscing elit Lorem ipsum dolor sit amet, consectetur adipiscing elit </li>\n"
-                          + "     * </ul>\n"
-                          + "     */\n"
-                          + "    public Optional<List<String>> getFieldD() {\n"
-                          + "        return Optional.ofNullable(fieldD);\n"
-                          + "    }\n";
-        assertTrue(fileStringOptional.get().contains(expected));
-    }
 }
diff --git a/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/integrations/annotations/AnnotationsTest.java b/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/integrations/annotations/AnnotationsTest.java
new file mode 100644
index 000000000..b49af3c5c
--- /dev/null
+++ b/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/integrations/annotations/AnnotationsTest.java
@@ -0,0 +1,136 @@
+package software.amazon.smithy.traitcodegen.integrations.annotations;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.net.URL;
+import java.nio.file.Paths;
+import java.util.Objects;
+import java.util.Optional;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import software.amazon.smithy.build.MockManifest;
+import software.amazon.smithy.build.PluginContext;
+import software.amazon.smithy.build.SmithyBuildPlugin;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.node.ArrayNode;
+import software.amazon.smithy.model.node.ObjectNode;
+import software.amazon.smithy.traitcodegen.TraitCodegenPlugin;
+
+public class AnnotationsTest {
+
+    private static final URL TEST_FILE = Objects.requireNonNull(AnnotationsTest.class.getResource("annotations-test.smithy"));
+    private final MockManifest manifest = new MockManifest();
+
+    @BeforeEach
+    void setup() {
+        Model model = Model.assembler()
+                .addImport(TEST_FILE)
+                .assemble()
+                .unwrap();
+        PluginContext context = PluginContext.builder()
+                .fileManifest(manifest)
+                .settings(
+                        ObjectNode.builder()
+                                .withMember("package", "com.example.traits")
+                                .withMember("namespace", "com.example.annotations")
+                                .withMember("header", ArrayNode.fromStrings("Header line One"))
+                                .build()
+                )
+                .model(model)
+                .build();
+        SmithyBuildPlugin plugin = new TraitCodegenPlugin();
+        plugin.execute(context);
+
+        assertFalse(manifest.getFiles().isEmpty());
+    }
+
+    @Test
+    void hasSmithyGeneratedAnnotationEvenIfNoDocstring() {
+        String fileContents = getFileContentsFromShapeName("HasSmithyGeneratedClass", true);
+        String fileContentsNested = getFileContentsFromShapeName("HasSmithyGeneratedNested", false);
+
+        assertTrue(fileContents.contains("@SmithyGenerated"));
+        assertTrue(fileContentsNested.contains("@SmithyGenerated"));
+    }
+
+    @Test
+    void deprecatedAnnotationOnClass() {
+        String fileContents = getFileContentsFromShapeName("DeprecatedStructure", true);
+        String expected = "@Deprecated\n" +
+                "@SmithyGenerated\n" +
+                "public final class DeprecatedStructureTrait";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void deprecatedAnnotationOnMember() {
+        String fileContents = getFileContentsFromShapeName("DeprecatedStructure", true);
+        String expected = "    @Deprecated\n" +
+                "    public Optional<String> getDeprecatedMember() {";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void deprecatedAnnotationWithDocsOnMember() {
+        String fileContents = getFileContentsFromShapeName("DeprecatedStructure", true);
+        String expected = "    /**\n" +
+                "     * Has docs in addition to deprecated\n" +
+                "     */\n" +
+                "    @Deprecated\n" +
+                "    public Optional<String> getDeprecatedWithDocs() {";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void unstableAnnotationOnClass() {
+        String fileContents = getFileContentsFromShapeName("UnstableStructure", true);
+        String expected = "@SmithyUnstableApi\n" +
+                "@SmithyGenerated\n" +
+                "public final class UnstableStructureTrait extends AbstractTrait";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void unstableAnnotationOnMember() {
+        String fileContents = getFileContentsFromShapeName("UnstableStructure", true);
+        String expected = "    @SmithyUnstableApi\n" +
+                "    public Optional<String> getUnstableMember() {";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void unstableAnnotationWithDocsOnMember() {
+        String fileContents = getFileContentsFromShapeName("UnstableStructure", true);
+        String expected = "    /**\n" +
+                "     * Has docs in addition to unstable\n" +
+                "     */\n" +
+                "    @SmithyUnstableApi\n" +
+                "    public Optional<String> getUnstableWithDocs() {";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void deprecatedAnnotationOnEnumVariant() {
+        String fileContents = getFileContentsFromShapeName("EnumWithAnnotations", true);
+        String expected = "        @Deprecated\n" +
+                "        DEPRECATED(\"DEPRECATED\"),";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void unstableAnnotationOnEnumVariant() {
+        String fileContents = getFileContentsFromShapeName("EnumWithAnnotations", true);
+        String expected = "        @SmithyUnstableApi\n" +
+                "        UNSTABLE(\"UNSTABLE\"),";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    private String getFileContentsFromShapeName(String className, boolean isTrait) {
+        String suffix = isTrait ? "Trait" : "";
+        String path = String.format("com/example/traits/%s%s.java", className, suffix);
+        Optional<String> fileStringOptional = manifest.getFileString(Paths.get(path).toString());
+        assertTrue(fileStringOptional.isPresent());
+        return fileStringOptional.get();
+    }
+}
diff --git a/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/integrations/javadoc/JavadocTest.java b/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/integrations/javadoc/JavadocTest.java
new file mode 100644
index 000000000..c0f641300
--- /dev/null
+++ b/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/integrations/javadoc/JavadocTest.java
@@ -0,0 +1,229 @@
+package software.amazon.smithy.traitcodegen.integrations.javadoc;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.net.URL;
+import java.nio.file.Paths;
+import java.util.Objects;
+import java.util.Optional;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import software.amazon.smithy.build.MockManifest;
+import software.amazon.smithy.build.PluginContext;
+import software.amazon.smithy.build.SmithyBuildPlugin;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.node.ArrayNode;
+import software.amazon.smithy.model.node.ObjectNode;
+import software.amazon.smithy.traitcodegen.TraitCodegenPlugin;
+
+public class JavadocTest {
+
+    private static final URL TEST_FILE = Objects.requireNonNull(JavadocTest.class.getResource("javadoc-test.smithy"));
+    private final MockManifest manifest = new MockManifest();
+
+    @BeforeEach
+    void setup() {
+        Model model = Model.assembler()
+                .addImport(TEST_FILE)
+                .assemble()
+                .unwrap();
+        PluginContext context = PluginContext.builder()
+                .fileManifest(manifest)
+                .settings(
+                        ObjectNode.builder()
+                                .withMember("package", "com.example.traits")
+                                .withMember("namespace", "com.example.javadoc")
+                                .withMember("header", ArrayNode.fromStrings("Header line One"))
+                                .build()
+                )
+                .model(model)
+                .build();
+        SmithyBuildPlugin plugin = new TraitCodegenPlugin();
+        plugin.execute(context);
+
+        assertFalse(manifest.getFiles().isEmpty());
+    }
+
+    @Test
+    void hasClassLevelDocstring() {
+        String fileContents = getFileContentsFromShapeName("DocumentationWrapping", true);
+        String expected = "/**\n" +
+                " * Basic class-level documentation\n" +
+                " */\n" +
+                "@SmithyGenerated\n" +
+                "public final class DocumentationWrappingTrait extends AbstractTrait implements ToSmithyBuilder<DocumentationWrappingTrait> {";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void wrapsBasicTextString() {
+        String fileContents = getFileContentsFromShapeName("DocumentationWrapping", true);
+        String expected = "    /**\n" +
+                "     * This is a long long docstring that should be wrapped. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do\n" +
+                "     * eiusmod tempor incididunt ut labore et dolore magna aliqua.\n" +
+                "     */\n" +
+                "    public Optional<String> getShouldBeWrapped() {\n" +
+                "        return Optional.ofNullable(shouldBeWrapped);\n" +
+                "    }";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void doesNotFormatContentInsideHtmlTags() {
+        String fileContents = getFileContentsFromShapeName("DocumentationWrapping", true);
+        String expected = "    /**\n" +
+                "     * Documentation includes preformatted text that should not be messed with. This sentence should still be partially\n" +
+                "     * wrapped.\n" +
+                "     * For example:\n" +
+                "     * <pre>\n" +
+                "     * Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n" +
+                "     * </pre>\n" +
+                "     * <ul>\n" +
+                "     *     <li> Lorem ipsum dolor sit amet, consectetur adipiscing elit Lorem ipsum dolor sit amet, consectetur adipiscing elit consectetur adipiscing </li>\n" +
+                "     *     <li> Lorem ipsum dolor sit amet, consectetur adipiscing elit Lorem ipsum dolor sit amet, consectetur adipiscing elit consectetur adipiscing </li>\n" +
+                "     * </ul>\n" +
+                "     */\n" +
+                "    public Optional<String> getPreformattedText() {\n" +
+                "        return Optional.ofNullable(preformattedText);\n" +
+                "    }";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void deprecatedAnnotationAndNoteOnClass() {
+        String fileContents = getFileContentsFromShapeName("DeprecatedStructure", true);
+        String expected = "/**\n" +
+                " * @deprecated As of yesterday. A message\n" +
+                " */\n" +
+                "@Deprecated\n" +
+                "@SmithyGenerated\n" +
+                "public final class DeprecatedStructureTrait";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void deprecatedAnnotationAndNoteOnMember() {
+        String fileContents = getFileContentsFromShapeName("DeprecatedStructure", true);
+        String expected = "    /**\n" +
+                "     * @deprecated As of yesterday. A message\n" +
+                "     */\n" +
+                "    @Deprecated\n" +
+                "    public Optional<String> getDeprecatedMember() {";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void deprecatedAnnotationAndNoteWithDocsOnMember() {
+        String fileContents = getFileContentsFromShapeName("DeprecatedStructure", true);
+        String expected = "    /**\n" +
+                "     * Has docs in addition to deprecated\n" +
+                "     *\n" +
+                "     * @deprecated As of yesterday. A message\n" +
+                "     */\n" +
+                "    @Deprecated\n" +
+                "    public Optional<String> getDeprecatedWithDocs() {";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void sinceJavadocTagAddedToClass() {
+        String fileContents = getFileContentsFromShapeName("SinceStructure", true);
+        String expected = "/**\n" +
+                " * @since 1.2\n" +
+                " */\n" +
+                "@SmithyGenerated\n" +
+                "public final class SinceStructureTrait";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void sinceJavaDocOnMember() {
+        String fileContents = getFileContentsFromShapeName("SinceStructure", true);
+        String expected = "    /**\n" +
+                "     * @since 1.2\n" +
+                "     */\n" +
+                "    public Optional<String> getSinceMember() {";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void externalDocumentationOnClass() {
+        String fileContents = getFileContentsFromShapeName("ExternalDocumentation", true);
+        String expected = "/**\n" +
+                " * @see <a href=\"https://example.com\">Example</a>\n" +
+                " * @see <a href=\"https://example.com\">Example2</a>\n" +
+                " */\n" +
+                "@SmithyGenerated\n" +
+                "public final class ExternalDocumentationTrait extends AbstractTrait";
+        assertTrue(fileContents.contains(expected));
+
+    }
+
+    @Test
+    void externalDocumentationOnMember() {
+        String fileContents = getFileContentsFromShapeName("ExternalDocumentation", true);
+        String expected = "    /**\n" +
+                "     * @see <a href=\"https://example.com\">Example</a>\n" +
+                "     */\n" +
+                "    public Optional<String> getMemberWithExternalDocumentation() {";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void allDocumentationIncludedTogetherOnClass() {
+        String fileContents = getFileContentsFromShapeName("Rollup", true);
+        String expected = "/**\n" +
+                " * This structure applies all documentation traits\n" +
+                " *\n" +
+                " * @see <a href=\"https://example.com\">Example</a>\n" +
+                " * @since 4.5\n" +
+                " * @deprecated As of sometime.\n" +
+                " */\n" +
+                "@SmithyUnstableApi\n" +
+                "@Deprecated\n" +
+                "@SmithyGenerated\n" +
+                "public final class RollupTrait extends AbstractTrait";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void allDocumentationIncludedTogetherOnMember() {
+        String fileContents = getFileContentsFromShapeName("Rollup", true);
+        String expected = "    /**\n" +
+                "     * This member applies all documentation traits\n" +
+                "     *\n" +
+                "     * @see <a href=\"https://example.com\">Example</a>\n" +
+                "     * @since 4.5\n" +
+                "     * @deprecated As of sometime.\n" +
+                "     */\n" +
+                "    @SmithyUnstableApi\n" +
+                "    @Deprecated\n" +
+                "    public Optional<String> getRollupMember() {";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void allDocumentationIncludedTogetherEnumVariant() {
+        String fileContents = getFileContentsFromShapeName("EnumVariantsTest", true);
+        String expected = "        /**\n" +
+                "         * Just a documented variant\n" +
+                "         *\n" +
+                "         * @see <a href=\"https://example.com\">Example</a>\n" +
+                "         * @since 4.5\n" +
+                "         * @deprecated Really. Dont use this.\n" +
+                "         */\n" +
+                "        @SmithyUnstableApi\n" +
+                "        @Deprecated\n" +
+                "        A(\"A\"),";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    private String getFileContentsFromShapeName(String className, boolean isTrait) {
+        String suffix = isTrait ? "Trait" : "";
+        String path = String.format("com/example/traits/%s%s.java", className, suffix);
+        Optional<String> fileStringOptional = manifest.getFileString(Paths.get(path).toString());
+        assertTrue(fileStringOptional.isPresent());
+        return fileStringOptional.get();
+    }
+}
diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/defaults/defaults.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/defaults/defaults.smithy
new file mode 100644
index 000000000..ff89a1e17
--- /dev/null
+++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/defaults/defaults.smithy
@@ -0,0 +1,44 @@
+$version: "2"
+
+namespace test.smithy.traitcodegen.defaults
+
+@trait
+structure StructDefaults {
+    @default([])
+    defaultList: StringList
+    @default({})
+    defaultMap: StringMap
+    @default(true)
+    defaultBoolean: Boolean
+    @default("default")
+    defaultString: String
+    @default(1)
+    defaultByte: Byte
+    @default(1)
+    defaultShort: Short
+    @default(1)
+    defaultInt: Integer
+    @default(1)
+    defaultLong: Long
+    @default(2.2)
+    defaultFloat: Float
+    @default(1.1)
+    defaultDouble: Double
+    @default(100)
+    defaultBigInt: BigInteger
+    @default(100.01)
+    defaultBigDecimal: BigDecimal
+    @default("1985-04-12T23:20:50.52Z")
+    defaultTimestamp: Timestamp
+}
+
+@private
+list StringList {
+    member: String
+}
+
+@private
+map StringMap {
+    key: String
+    value: String
+}
diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/manifest b/smithy-trait-codegen/src/test/resources/META-INF/smithy/manifest
index 2dcecb800..e1e57d9ef 100644
--- a/smithy-trait-codegen/src/test/resources/META-INF/smithy/manifest
+++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/manifest
@@ -41,4 +41,5 @@ uniqueitems/number-set-trait.smithy
 uniqueitems/string-set-trait.smithy
 uniqueitems/struct-set-trait.smithy
 filtered-by-tag.smithy
+defaults/defaults.smithy
 
diff --git a/smithy-trait-codegen/src/test/resources/software/amazon/smithy/traitcodegen/integrations/annotations/annotations-test.smithy b/smithy-trait-codegen/src/test/resources/software/amazon/smithy/traitcodegen/integrations/annotations/annotations-test.smithy
new file mode 100644
index 000000000..8ec589625
--- /dev/null
+++ b/smithy-trait-codegen/src/test/resources/software/amazon/smithy/traitcodegen/integrations/annotations/annotations-test.smithy
@@ -0,0 +1,44 @@
+$version: "2"
+
+namespace com.example.annotations
+
+@trait
+structure HasSmithyGeneratedClass {
+    nested: HasSmithyGeneratedNested
+}
+
+structure HasSmithyGeneratedNested {
+    field: String
+}
+
+@trait
+@deprecated
+structure DeprecatedStructure {
+    @deprecated
+    deprecatedMember: String,
+
+    /// Has docs in addition to deprecated
+    @deprecated
+    deprecatedWithDocs: String
+}
+
+@trait
+@unstable
+structure UnstableStructure {
+    @unstable
+    unstableMember: String,
+
+    /// Has docs in addition to unstable
+    @unstable
+    unstableWithDocs: String
+}
+
+@trait
+enum EnumWithAnnotations {
+    @deprecated
+    DEPRECATED,
+    @unstable
+    UNSTABLE
+}
+
+
diff --git a/smithy-trait-codegen/src/test/resources/software/amazon/smithy/traitcodegen/integrations/javadoc/javadoc-test.smithy b/smithy-trait-codegen/src/test/resources/software/amazon/smithy/traitcodegen/integrations/javadoc/javadoc-test.smithy
new file mode 100644
index 000000000..d9f154cc0
--- /dev/null
+++ b/smithy-trait-codegen/src/test/resources/software/amazon/smithy/traitcodegen/integrations/javadoc/javadoc-test.smithy
@@ -0,0 +1,80 @@
+$version: "2"
+
+namespace com.example.javadoc
+
+/// Basic class-level documentation
+@trait
+structure DocumentationWrapping {
+    /// This is a long long docstring that should be wrapped. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+    shouldBeWrapped: String,
+
+    /// Documentation includes preformatted text that should not be messed with. This sentence should still be partially wrapped.
+    /// For example:
+    ///
+    /// <pre>
+    /// Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+    /// </pre>
+    ///
+    /// <ul>
+    ///     <li> Lorem ipsum dolor sit amet, consectetur adipiscing elit Lorem ipsum dolor sit amet, consectetur adipiscing elit consectetur adipiscing </li>
+    ///     <li> Lorem ipsum dolor sit amet, consectetur adipiscing elit Lorem ipsum dolor sit amet, consectetur adipiscing elit consectetur adipiscing </li>
+    /// </ul>
+    preformattedText: String
+}
+
+@trait
+@deprecated(message: "A message", since: "yesterday")
+structure DeprecatedStructure {
+    @deprecated(message: "A message", since: "yesterday")
+    deprecatedMember: String,
+
+    /// Has docs in addition to deprecated
+    @deprecated(message: "A message", since: "yesterday")
+    deprecatedWithDocs: String
+}
+
+@trait
+@since("1.2")
+structure SinceStructure {
+    @since("1.2")
+    sinceMember: String
+}
+
+@trait
+@externalDocumentation(
+    Example: "https://example.com",
+    Example2: "https://example.com"
+)
+structure ExternalDocumentation {
+    @externalDocumentation(Example: "https://example.com")
+    memberWithExternalDocumentation: String
+}
+
+
+/// This structure applies all documentation traits
+@trait
+@unstable
+@deprecated(since: "sometime")
+@externalDocumentation(Example: "https://example.com")
+@since("4.5")
+structure Rollup {
+    /// This member applies all documentation traits
+    @unstable
+    @deprecated(since: "sometime")
+    @externalDocumentation(Example: "https://example.com")
+    @since("4.5")
+    rollupMember: String
+}
+
+/// This enum applies all documentation traits to its variants
+@trait
+enum EnumVariantsTest {
+    /// Just a documented variant
+    @unstable
+    @deprecated(message: "Really. Dont use this.")
+    @externalDocumentation(Example: "https://example.com")
+    @since("4.5")
+    A,
+    B
+}
+