Repository: smithy-lang/smithy
Pull request #2264 (2024-04-29 22:05:38)

Add OperationContextParams + validator
#### Background
* Adds new OperationContextParams trait to support binding of array parameters to nested operation inputs and an associated validator.

#### Testing
* New validator tests (valid/invalid)

---
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-2.0/additional-specs/rules-engine/parameters.rst b/docs/source-2.0/additional-specs/rules-engine/parameters.rst
index 3385dc5f2..d1ee3e931 100644
--- a/docs/source-2.0/additional-specs/rules-engine/parameters.rst
+++ b/docs/source-2.0/additional-specs/rules-engine/parameters.rst
@@ -95,6 +95,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
@@ -240,6 +241,82 @@ operation:
     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 +420,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/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/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/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/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-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
+}
+