Repository: smithy-lang/smithy
Pull request #2267 (2024-04-30 17:57:01)

Add support for default trait on members
#### Background
Adds support for defaults on trait member values. 

Applying the `@default` trait to a member will cause it to be treated as non-nullable and will set the value as the initial value for the member within the shape's builder.  

#### Testing
A new set of integration tests were added to ensure the defaults for all types of members are set on a structure shape with no value for those default members.

---
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/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-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
+}
+