Repository: smithy-lang/smithy
Pull request #2445 (2024-11-07 21:17:32)

Add round-trip convertibility of resource schemas
#### Background
- Before these changes, converting a node to resource-schema is lossy
- These changes include the necessary changes to keep round-trips between node and resource-schema lossless
- Property was adapted to incorporate its separate "properties" as extensions to the schema**
  - **only when these "properties" (`insertionOrder`, `dependencies`) is set to a non-default
- Unknown properties are added as extensions to the Schema

#### Testing
- Unit testing

---
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/README.md b/README.md
index 007c0ca27..4e1c42985 100644
--- a/README.md
+++ b/README.md
@@ -83,5 +83,5 @@ This library is licensed under the Apache 2.0 License.
 [Smithy Gradle Plugin]: https://github.com/awslabs/smithy-gradle-plugin/
 [Smithy CLI]: https://smithy.io/2.0/guides/smithy-cli/index.html
 [`smithy-build.json`]: https://smithy.io/2.0/guides/building-models/build-config.html#using-smithy-build-json
-[building]: https://smithy.io/2.0/guides/building-models/index.html
+[building]: https://smithy.io/2.0/guides/smithy-build-json.html
 [awesome-smithy]: https://github.com/smithy-lang/awesome-smithy
diff --git a/docs/requirements.txt b/docs/requirements.txt
index e50794f67..54c82aab9 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -11,3 +11,6 @@ sphinx-tabs>=3.4.4
 
 sphinx_copybutton==0.5.0
 sphinx-autobuild>=2024.4.16
+
+# needs to be added as it is removed in python3.13
+standard-imghdr==3.13.0
diff --git a/docs/source-2.0/guides/building-codegen/using-the-semantic-model.rst b/docs/source-2.0/guides/building-codegen/using-the-semantic-model.rst
index 319bfb30b..90900870c 100644
--- a/docs/source-2.0/guides/building-codegen/using-the-semantic-model.rst
+++ b/docs/source-2.0/guides/building-codegen/using-the-semantic-model.rst
@@ -284,7 +284,7 @@ the model and might rely on protocol-specific information.
     HttpBindingIndex index = HttpBindingIndex.of(model);
 
     String defaultPayloadType = "application/json";
-    String eventStreamType = "application/vnd.amazon.event-stream");
+    String eventStreamType = "application/vnd.amazon.eventstream");
     String contentType = index
         .determineRequestContentType(operation, defaultPayloadType, eventStreamType)
         .orElseNull();
@@ -302,7 +302,7 @@ the model and might rely on protocol-specific information.
    HttpBindingIndex index = HttpBindingIndex.of(model);
 
    String defaultPayloadType = "application/json";
-   String eventStreamType = "application/vnd.amazon.event-stream");
+   String eventStreamType = "application/vnd.amazon.eventstream");
    String contentType = index
        .determineResponseContentType(operation, defaultPayloadType, eventStreamType)
        .orElseNull();
diff --git a/docs/source-2.0/guides/smithy-build-json.rst b/docs/source-2.0/guides/smithy-build-json.rst
index ebdcf35a4..cba39798b 100644
--- a/docs/source-2.0/guides/smithy-build-json.rst
+++ b/docs/source-2.0/guides/smithy-build-json.rst
@@ -1650,6 +1650,61 @@ but keeps the shape if it has any of the provided tags:
         }
     }
 
+.. _removeDeprecatedShapes-transform:
+
+removeDeprecatedShapes
+-----------------------
+
+Removes any shapes that were marked as ``@deprecated`` before the specified version or date.
+
+This transform can be used to filter out shapes with the :ref:`deprecated trait <deprecated-trait>` applied if the
+``since`` property of the trait specifies a version or date. Versions are expected to follow the
+`SemVer Specification`_ and dates are expected to be `ISO 8601`_ calendar dates (YYYY-MM-DD).
+If the value of the ``since`` property is not a valid SemVer version or ISO 8601 calendar date then the
+``@deprecated`` trait will be ignored by this transform.
+
+If the version or date in the ``since`` property of a ``@deprecated`` trait is before the configured version or date, then
+the shape the ``@deprecated`` trait is applied to will be removed from the model. Shapes with the ``@deprecated`` trait,
+but no ``since`` property will be ignored by this transform.
+
+.. list-table::
+    :header-rows: 1
+    :widths: 10 20 70
+
+    * - Property
+      - Type
+      - Description
+    * - relativeDate
+      - ``string``
+      - Date, in `ISO 8601`_ calendar date format (YYYY-MM-DD), to use to filter out deprecated shapes.
+        Any shapes deprecated before this date will be removed from the model.
+    * - relativeVersion
+      - ``string``
+      - Version, following the `SemVer Specification`_, to use to filter out deprecated shapes.
+        Any shapes deprecated in an earlier version will be removed from the model.
+
+The following example removes any deprecated shapes that were deprecated before ``2024-10-10``
+or before version ``1.1.0``.
+
+.. code-block:: json
+
+    {
+        "version": "1.0",
+        "projections": {
+            "exampleProjection": {
+                "transforms": [
+                    {
+                        "name": "removeDeprecatedShapes",
+                        "args": {
+                            "relativeDate": "2024-10-10",
+                            "relativeVersion": "1.1.0"
+                        }
+                    }
+                ]
+            }
+        }
+    }
+
 .. _renameShapes-transform:
 
 renameShapes
@@ -1964,3 +2019,5 @@ Assuming ``hello.sh`` is on the PATH and might look something like:
 .. _Apache Maven: https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html
 .. _Maven Central: https://search.maven.org
 .. _official Maven documentation: https://maven.apache.org/pom.html#dependency-version-requirement-specification
+.. _SemVer Specification: https://semver.org/
+.. _ISO 8601: https://www.iso.org/iso-8601-date-and-time-format.html
diff --git a/docs/source-2.0/spec/documentation-traits.rst b/docs/source-2.0/spec/documentation-traits.rst
index f0748626a..58182ee19 100644
--- a/docs/source-2.0/spec/documentation-traits.rst
+++ b/docs/source-2.0/spec/documentation-traits.rst
@@ -388,9 +388,9 @@ tags trait are arbitrary and up to the model author.
 Summary
     Defines a proper name for a service or resource shape. This title can be
     used in automatically generated documentation and other contexts to
-    provide a user friendly name for services and resources.
+    provide a user friendly name for a shape.
 Trait selector
-    ``:is(service, resource)``
+    ``:not(member)``
 
     *Any service or resource*
 Value type
diff --git a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/Property.java b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/Property.java
index 61ccade1e..919f9f62d 100644
--- a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/Property.java
+++ b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/Property.java
@@ -16,12 +16,14 @@
 package software.amazon.smithy.aws.cloudformation.schema.model;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 import software.amazon.smithy.jsonschema.Schema;
 import software.amazon.smithy.model.node.Node;
 import software.amazon.smithy.model.node.ObjectNode;
+import software.amazon.smithy.model.node.StringNode;
 import software.amazon.smithy.model.node.ToNode;
-import software.amazon.smithy.utils.ListUtils;
 import software.amazon.smithy.utils.SmithyBuilder;
 import software.amazon.smithy.utils.ToSmithyBuilder;
 
@@ -32,8 +34,6 @@ import software.amazon.smithy.utils.ToSmithyBuilder;
  * @see <a href="https://github.com/aws-cloudformation/cloudformation-cli/blob/master/src/rpdk/core/data/schema/provider.definition.schema.v1.jsonL74">Resource Type Properties JSON Schema</a>
  */
 public final class Property implements ToNode, ToSmithyBuilder<Property> {
-    private final boolean insertionOrder;
-    private final List<String> dependencies;
     private final Schema schema;
     // Other reserved property names in definition but not in the validation
     // JSON Schema, so not defined in code:
@@ -41,32 +41,45 @@ public final class Property implements ToNode, ToSmithyBuilder<Property> {
     // * writeOnly
 
     private Property(Builder builder) {
-        this.insertionOrder = builder.insertionOrder;
-        this.dependencies = ListUtils.copyOf(builder.dependencies);
-        this.schema = builder.schema;
-    }
+        Schema.Builder schemaBuilder;
 
-    @Override
-    public Node toNode() {
-        ObjectNode.Builder builder = schema.toNode().expectObjectNode().toBuilder();
+        if (builder.schema == null) {
+            schemaBuilder = Schema.builder();
+        } else {
+            schemaBuilder = builder.schema.toBuilder();
+        }
 
-        // Only serialize these properties if set to non-defaults.
-        if (insertionOrder) {
-            builder.withMember("insertionOrder", Node.from(insertionOrder));
+        if (builder.insertionOrder) {
+            schemaBuilder.putExtension("insertionOrder", Node.from(true));
         }
-        if (!dependencies.isEmpty()) {
-            builder.withMember("dependencies", Node.fromStrings(dependencies));
+
+        if (!builder.dependencies.isEmpty()) {
+            schemaBuilder.putExtension("dependencies", Node.fromStrings(builder.dependencies));
         }
 
-        return builder.build();
+        this.schema = schemaBuilder.build();
+    }
+
+    @Override
+    public Node toNode() {
+        return schema.toNode().expectObjectNode();
     }
 
     @Override
     public Builder toBuilder() {
-        return builder()
-                .insertionOrder(insertionOrder)
-                .dependencies(dependencies)
-                .schema(schema);
+        return builder().schema(schema);
+    }
+
+    public static Property fromNode(Node node) {
+        ObjectNode objectNode = node.expectObjectNode();
+        Builder builder = builder();
+        builder.schema(Schema.fromNode(objectNode));
+
+        return builder.build();
+    }
+
+    public static Property fromSchema(Schema schema) {
+        return builder().schema(schema).build();
     }
 
     public static Builder builder() {
@@ -74,11 +87,17 @@ public final class Property implements ToNode, ToSmithyBuilder<Property> {
     }
 
     public boolean isInsertionOrder() {
-        return insertionOrder;
+        Optional<Boolean> insertionOrder = schema.getExtension("insertionOrder")
+                .map(n -> n.toNode().expectBooleanNode().getValue());
+
+        return insertionOrder.orElse(false);
     }
 
     public List<String> getDependencies() {
-        return dependencies;
+        Optional<List<String>> dependencies = schema.getExtension("dependencies")
+                .map(n -> n.toNode().expectArrayNode().getElementsAs(StringNode::getValue));
+
+        return dependencies.orElse(Collections.emptyList());
     }
 
     public Schema getSchema() {
diff --git a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/ResourceSchema.java b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/ResourceSchema.java
index a006ced3b..5268dfb98 100644
--- a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/ResourceSchema.java
+++ b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/ResourceSchema.java
@@ -60,6 +60,7 @@ public final class ResourceSchema implements ToNode, ToSmithyBuilder<ResourceSch
     private final Map<String, Handler> handlers = new TreeMap<>(Comparator.comparing(Handler::getHandlerNameOrder));
     private final Map<String, Remote> remotes = new TreeMap<>();
     private final Tagging tagging;
+    private final Schema additionalProperties;
 
     private ResourceSchema(Builder builder) {
         typeName = SmithyBuilder.requiredState("typeName", builder.typeName);
@@ -84,6 +85,7 @@ public final class ResourceSchema implements ToNode, ToSmithyBuilder<ResourceSch
         handlers.putAll(builder.handlers);
         remotes.putAll(builder.remotes);
         tagging = builder.tagging;
+        additionalProperties = builder.additionalProperties;
     }
 
     @Override
@@ -136,6 +138,9 @@ public final class ResourceSchema implements ToNode, ToSmithyBuilder<ResourceSch
         if (tagging != null) {
             builder.withMember("tagging", mapper.serialize(tagging));
         }
+        if (additionalProperties != null) {
+            builder.withMember("additionalProperties", mapper.serialize(additionalProperties));
+        }
 
         return builder.build();
     }
@@ -161,6 +166,11 @@ public final class ResourceSchema implements ToNode, ToSmithyBuilder<ResourceSch
                 .tagging(tagging);
     }
 
+    public static ResourceSchema fromNode(Node node) {
+        NodeMapper mapper = new NodeMapper();
+        return mapper.deserializeInto(node, ResourceSchema.builder()).build();
+    }
+
     public static Builder builder() {
         return new Builder();
     }
@@ -242,6 +252,7 @@ public final class ResourceSchema implements ToNode, ToSmithyBuilder<ResourceSch
         private final Map<String, Handler> handlers = new TreeMap<>();
         private final Map<String, Remote> remotes = new TreeMap<>();
         private Tagging tagging;
+        private Schema additionalProperties;
 
         private Builder() {}
 
@@ -470,5 +481,10 @@ public final class ResourceSchema implements ToNode, ToSmithyBuilder<ResourceSch
             this.remotes.clear();
             return this;
         }
+
+        public Builder additionalProperties(Schema additionalProperties) {
+            this.additionalProperties = additionalProperties;
+            return this;
+        }
     }
 }
diff --git a/smithy-build/src/main/java/software/amazon/smithy/build/transforms/RemoveDeprecatedShapes.java b/smithy-build/src/main/java/software/amazon/smithy/build/transforms/RemoveDeprecatedShapes.java
new file mode 100644
index 000000000..c0b08ab87
--- /dev/null
+++ b/smithy-build/src/main/java/software/amazon/smithy/build/transforms/RemoveDeprecatedShapes.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.build.transforms;
+
+import software.amazon.smithy.build.TransformContext;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.transform.ModelTransformer;
+
+/**
+ * {@code removeDeprecatedShapes} removes shapes from a model if they have been deprecated before a version or date.
+ */
+public final class RemoveDeprecatedShapes extends ConfigurableProjectionTransformer<RemoveDeprecatedShapes.Config> {
+
+    /**
+     * {@code RemoveDeprecatedShapes} configuration settings.
+     */
+    public static final class Config {
+        private String relativeDate;
+        private String relativeVersion;
+
+        /**
+         * Gets the date used to filter deprecated shapes.
+         *
+         * @return The date used to filter deprecated shapes.
+         */
+        public String getRelativeDate() {
+            return relativeDate;
+        }
+
+        /**
+         * Sets the date used to filter deprecated shapes.
+         *
+         * @param relativeDate The date used to filter deprecated shapes.
+         */
+        public void setRelativeDate(String relativeDate) {
+            this.relativeDate = relativeDate;
+        }
+
+        /**
+         * Gets the version used to filter deprecated shapes.
+         *
+         * @return The version used to filter deprecated shapes.
+         */
+        public String getRelativeVersion() {
+            return relativeVersion;
+        }
+
+        /**
+         * Sets the version used to filter deprecated shapes.
+         *
+         * @param relativeVersion The version used to filter deprecated shapes.
+         */
+        public void setRelativeVersion(String relativeVersion) {
+            this.relativeVersion = relativeVersion;
+        }
+    }
+
+    @Override
+    public String getName() {
+        return "removeDeprecatedShapes";
+    }
+
+    @Override
+    public Class<Config> getConfigType() {
+        return Config.class;
+    }
+
+    @Override
+    protected Model transformWithConfig(TransformContext context, Config config) {
+        Model model = context.getModel();
+        ModelTransformer transformer = context.getTransformer();
+        model = transformer.filterDeprecatedRelativeDate(model, config.getRelativeDate());
+        model = transformer.filterDeprecatedRelativeVersion(model, config.getRelativeVersion());
+        return model;
+    }
+}
diff --git a/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/directed/CodegenDirector.java b/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/directed/CodegenDirector.java
index 795fc54fb..13aedaeb8 100644
--- a/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/directed/CodegenDirector.java
+++ b/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/directed/CodegenDirector.java
@@ -289,6 +289,32 @@ public final class CodegenDirector<
         });
     }
 
+    /**
+     * Removes any shapes deprecated before the specified date.
+     *
+     * @param relativeDate Relative date, in YYYY-MM-DD format, to use to filter out deprecated shapes.
+     * @see ModelTransformer#filterDeprecatedRelativeDate(Model, String)
+     */
+    public void removeShapesDeprecatedBeforeDate(String relativeDate) {
+        transforms.add((model, transformer) -> {
+            LOGGER.finest("Removing shapes deprecated before date: " + relativeDate);
+            return transformer.filterDeprecatedRelativeDate(model, relativeDate);
+        });
+    }
+
+    /**
+     * Removes any shapes deprecated before the specified version.
+     *
+     * @param relativeVersion Version, in SemVer format, to use to filter out deprecated shapes.
+     * @see ModelTransformer#filterDeprecatedRelativeVersion(Model, String)
+     */
+    public void removeShapesDeprecatedBeforeVersion(String relativeVersion) {
+        transforms.add((model, transformer) -> {
+            LOGGER.finest("Removing shapes deprecated before version: " + relativeVersion);
+            return transformer.filterDeprecatedRelativeVersion(model, relativeVersion);
+        });
+    }
+
     /**
      * Changes each compatible string shape with the enum trait to an enum shape.
      *
diff --git a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/Schema.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/Schema.java
index 7d820b970..f051d0dd5 100644
--- a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/Schema.java
+++ b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/Schema.java
@@ -27,8 +27,10 @@ import java.util.Objects;
 import java.util.Optional;
 import java.util.logging.Logger;
 import software.amazon.smithy.model.node.ArrayNode;
+import software.amazon.smithy.model.node.BooleanNode;
 import software.amazon.smithy.model.node.Node;
 import software.amazon.smithy.model.node.ObjectNode;
+import software.amazon.smithy.model.node.StringNode;
 import software.amazon.smithy.model.node.ToNode;
 import software.amazon.smithy.utils.ListUtils;
 import software.amazon.smithy.utils.MapUtils;
@@ -60,6 +62,9 @@ import software.amazon.smithy.utils.ToSmithyBuilder;
 public final class Schema implements ToNode, ToSmithyBuilder<Schema> {
     private static final Logger LOGGER = Logger.getLogger(Schema.class.getName());
 
+    // A schema can be an object OR a boolean - when this value is set (true or false), the schema is represented
+    // as a "trivial boolean schema".
+    private final Boolean trivial;
     private final String ref;
     private final String type;
     private final Collection<String> enumValues;
@@ -112,6 +117,11 @@ public final class Schema implements ToNode, ToSmithyBuilder<Schema> {
     private Node asNode;
 
     private Schema(Builder builder) {
+        trivial = builder.trivial;
+        if (trivial != null) {
+            asNode = Node.from(trivial);
+        }
+
         ref = builder.ref;
         type = builder.type;
         enumValues = Collections.unmodifiableCollection(builder.enumValues);
@@ -577,10 +587,25 @@ public final class Schema implements ToNode, ToSmithyBuilder<Schema> {
         return Objects.hash(ref, type, properties, items);
     }
 
+    public static Schema fromNode(Node node) {
+        if (node.isBooleanNode()) {
+            BooleanNode booleanNode = node.expectBooleanNode();
+            return new Schema.Builder().trivial(booleanNode.getValue()).build();
+        }
+
+        ObjectNode objectNode = node.expectObjectNode();
+        Schema.Builder builder = builder();
+        objectNode.getMembers().forEach((key, val) -> builder.applyNode(key.getValue(), val));
+
+        return builder.build();
+    }
+
     /**
      * Abstract class used to build Schema components.
      */
     public static final class Builder implements SmithyBuilder<Schema> {
+        private Boolean trivial;
+
         private String ref;
         private String type;
         private Collection<String> enumValues = ListUtils.of();
@@ -637,6 +662,11 @@ public final class Schema implements ToNode, ToSmithyBuilder<Schema> {
             return new Schema(this);
         }
 
+        public Builder trivial(Boolean trivial) {
+            this.trivial = trivial;
+            return this;
+        }
+
         public Builder ref(String ref) {
             this.ref = ref;
             return this;
@@ -970,5 +1000,138 @@ public final class Schema implements ToNode, ToSmithyBuilder<Schema> {
                     return this;
             }
         }
+
+        Builder applyNode(String key, Node node) {
+            switch (key) {
+                case "$ref":
+                    this.ref(node.expectStringNode().getValue());
+                    break;
+                case "type":
+                    this.type(node.expectStringNode().getValue());
+                    break;
+                case "enum":
+                    this.enumValues(node.expectArrayNode().getElementsAs(StringNode::getValue));
+                    break;
+                case "intEnum":
+                    this.intEnumValues(
+                            node.expectArrayNode().getElementsAs((e) -> e.expectNumberNode().getValue().intValue()));
+                    break;
+                case "const":
+                    this.constValue(node);
+                    break;
+                case "default":
+                    this.defaultValue(node);
+                    break;
+                case "multipleOf":
+                    this.multipleOf(node.expectNumberNode().getValue());
+                    break;
+                case "maximum":
+                    this.maximum(node.expectNumberNode().getValue());
+                    break;
+                case "exclusiveMaximum":
+                    this.exclusiveMaximum(node.expectNumberNode().getValue());
+                    break;
+                case "minimum":
+                    this.minimum(node.expectNumberNode().getValue());
+                    break;
+                case "exclusiveMinimum":
+                    this.exclusiveMinimum(node.expectNumberNode().getValue());
+                    break;
+                case "maxLength":
+                    this.maxLength(node.expectNumberNode().getValue().longValue());
+                    break;
+                case "minLength":
+                    this.minLength(node.expectNumberNode().getValue().longValue());
+                    break;
+                case "pattern":
+                    this.pattern(node.expectStringNode().getValue());
+                    break;
+                case "items":
+                    this.items(Schema.fromNode(node));
+                    break;
+                case "maxItems":
+                    this.maxItems(node.expectNumberNode().getValue().intValue());
+                    break;
+                case "minItems":
+                    this.minItems(node.expectNumberNode().getValue().intValue());
+                    break;
+                case "uniqueItems":
+                    this.uniqueItems(node.expectBooleanNode().getValue());
+                    break;
+                case "maxProperties":
+                    this.maxProperties(node.expectNumberNode().getValue().intValue());
+                    break;
+                case "minProperties":
+                    this.minProperties(node.expectNumberNode().getValue().intValue());
+                    break;
+                case "required":
+                    this.required(node.expectArrayNode().getElementsAs(StringNode::getValue));
+                    break;
+                case "properties":
+                    node.expectObjectNode()
+                            .getMembers()
+                            .forEach((k, v) -> this.putProperty(k.getValue(), Schema.fromNode(v)));
+                    break;
+                case "additionalProperties":
+                    this.additionalProperties(Schema.fromNode(node));
+                    break;
+                case "propertyNames":
+                    this.propertyNames(Schema.fromNode(node));
+                    break;
+                case "patternProperties":
+                    node.expectObjectNode()
+                            .getMembers()
+                            .forEach((k, v) -> this.putPatternProperty(k.getValue(), Schema.fromNode(v)));
+                    break;
+                case "allOf":
+                    this.allOf(node.expectArrayNode().getElementsAs(Schema::fromNode));
+                    break;
+                case "anyOf":
+                    this.anyOf(node.expectArrayNode().getElementsAs(Schema::fromNode));
+                    break;
+                case "oneOf":
+                    this.oneOf(node.expectArrayNode().getElementsAs(Schema::fromNode));
+                    break;
+                case "not":
+                    this.not(Schema.fromNode(node));
+                    break;
+                case "title":
+                    this.title(node.expectStringNode().getValue());
+                    break;
+                case "description":
+                    this.description(node.expectStringNode().getValue());
+                    break;
+                case "format":
+                    this.format(node.expectStringNode().getValue());
+                    break;
+                case "readOnly":
+                    this.readOnly(node.expectBooleanNode().getValue());
+                    break;
+                case "writeOnly":
+                    this.writeOnly(node.expectBooleanNode().getValue());
+                    break;
+                case "comment":
+                    this.comment(node.expectStringNode().getValue());
+                    break;
+                case "examples":
+                    this.examples(node);
+                    break;
+                case "deprecated":
+                    this.deprecated(node.expectBooleanNode().getValue());
+                    break;
+                case "contentEncoding":
+                    this.contentEncoding(node.expectStringNode().getValue());
+                    break;
+                case "contentMediaType":
+                    this.contentMediaType(node.expectStringNode().getValue());
+                    break;
+                default:
+                    LOGGER.fine("Unknown property will be added to extensions: " + key);
+                    this.putExtension(key, node);
+                    break;
+            }
+
+            return this;
+        }
     }
 }
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/node/DefaultNodeDeserializers.java b/smithy-model/src/main/java/software/amazon/smithy/model/node/DefaultNodeDeserializers.java
index 553213f95..cda85e0ae 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/node/DefaultNodeDeserializers.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/node/DefaultNodeDeserializers.java
@@ -126,6 +126,7 @@ final class DefaultNodeDeserializers {
     private static final Map<Type, Function<Number, Object>> NUMBER_MAPPERS = new HashMap<>();
 
     static {
+        NUMBER_MAPPERS.put(Number.class, n -> n);
         NUMBER_MAPPERS.put(Object.class, n -> n);
         NUMBER_MAPPERS.put(Byte.class, Number::byteValue);
         NUMBER_MAPPERS.put(byte.class, Number::byteValue);
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/transform/FilterDeprecatedRelativeDate.java b/smithy-model/src/main/java/software/amazon/smithy/model/transform/FilterDeprecatedRelativeDate.java
new file mode 100644
index 000000000..49a048183
--- /dev/null
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/transform/FilterDeprecatedRelativeDate.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.model.transform;
+
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.shapes.Shape;
+import software.amazon.smithy.model.traits.DeprecatedTrait;
+
+final class FilterDeprecatedRelativeDate {
+    private static final Logger LOGGER = Logger.getLogger(FilterDeprecatedRelativeDate.class.getName());
+    // YYYY-MM-DD calendar date with optional hyphens.
+    private static final Pattern ISO_8601_DATE_REGEX = Pattern.compile(
+            "^([0-9]{4})-?(1[0-2]|0[1-9])-?(3[01]|0[1-9]|[12][0-9])$"
+    );
+
+    private final String relativeDate;
+
+    FilterDeprecatedRelativeDate(String relativeDate) {
+        if (relativeDate != null && !isIso8601Date(relativeDate)) {
+            throw new IllegalArgumentException("Provided relativeDate: `"
+                    + relativeDate
+                    + "` does not match ISO8601 calendar date format (YYYY-MM-DD)."
+            );
+        }
+        this.relativeDate = relativeDate != null ? relativeDate.replace("-", "") : null;
+    }
+
+    Model transform(ModelTransformer transformer, Model model) {
+        // If there is no filter. Exit without traversing shapes
+        if (relativeDate == null) {
+            return model;
+        }
+
+        Set<Shape> shapesToRemove = new HashSet<>();
+        for (Shape shape : model.getShapesWithTrait(DeprecatedTrait.class)) {
+            Optional<String> sinceOptional = shape.expectTrait(DeprecatedTrait.class).getSince();
+            if (!sinceOptional.isPresent()) {
+                continue;
+            }
+            String since = sinceOptional.get();
+
+            if (isIso8601Date(since)) {
+                // Compare lexicographical ordering without hyphens.
+                if (relativeDate.compareTo(since.replace("-", "")) > 0) {
+                    LOGGER.fine("Filtering deprecated shape: `"
+                            + shape + "`"
+                            + ". Shape was deprecated as of: " + since
+                    );
+                    shapesToRemove.add(shape);
+                }
+            }
+        }
+
+        return transformer.removeShapes(model, shapesToRemove);
+    }
+
+    private static boolean isIso8601Date(String string) {
+        return ISO_8601_DATE_REGEX.matcher(string).matches();
+    }
+}
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/transform/FilterDeprecatedRelativeVersion.java b/smithy-model/src/main/java/software/amazon/smithy/model/transform/FilterDeprecatedRelativeVersion.java
new file mode 100644
index 000000000..ed00b28aa
--- /dev/null
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/transform/FilterDeprecatedRelativeVersion.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.model.transform;
+
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.shapes.Shape;
+import software.amazon.smithy.model.traits.DeprecatedTrait;
+
+final class FilterDeprecatedRelativeVersion {
+    private static final Logger LOGGER = Logger.getLogger(FilterDeprecatedRelativeVersion.class.getName());
+
+    /**
+     * SemVer regex from semantic version spec.
+     * @see <a href="https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string">SemVer</a>
+     */
+    private static final Pattern SEMVER_REGEX = Pattern.compile(
+            "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)"
+            + "(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"
+    );
+
+    private final String relativeVersion;
+
+    FilterDeprecatedRelativeVersion(String relativeVersion) {
+        if (relativeVersion != null && !isSemVer(relativeVersion)) {
+            throw new IllegalArgumentException("Provided relativeDate: `"
+                    + relativeVersion
+                    + "` is not a valid ."
+            );
+        }
+        this.relativeVersion = relativeVersion;
+    }
+
+    Model transform(ModelTransformer transformer, Model model) {
+        // If there are no filters. Exit without traversing shapes
+        if (relativeVersion == null) {
+            return model;
+        }
+
+        Set<Shape> shapesToRemove = new HashSet<>();
+        for (Shape shape : model.getShapesWithTrait(DeprecatedTrait.class)) {
+            Optional<String> sinceOptional = shape.expectTrait(DeprecatedTrait.class).getSince();
+
+            if (!sinceOptional.isPresent()) {
+                continue;
+            }
+
+            String since = sinceOptional.get();
+            // Remove any shapes that were deprecated before the specified version.
+            if (isSemVer(since)) {
+                if (compareSemVer(relativeVersion, since) > 0) {
+                    LOGGER.fine("Filtering deprecated shape: `"
+                            + shape + "`"
+                            + ". Shape was deprecated as of version: " + since
+                    );
+                    shapesToRemove.add(shape);
+                }
+            }
+        }
+
+        return transformer.removeShapes(model, shapesToRemove);
+    }
+
+    private static boolean isSemVer(String string) {
+        return SEMVER_REGEX.matcher(string).matches();
+    }
+
+    static int compareSemVer(String semVer1, String semVer2) {
+        String[] versionComponents1 = semVer1.split("\\.");
+        String[] versionComponents2 = semVer2.split("\\.");
+
+        int maxLength = Math.max(versionComponents1.length, versionComponents2.length);
+        for (int i = 0; i < maxLength; i++) {
+            // Treat all implicit components as 0's
+            String component1 = i >= versionComponents1.length ? "0" : versionComponents1[i];
+            String component2 = i >= versionComponents2.length ? "0" : versionComponents2[i];
+            int comparison = component1.compareTo(component2);
+            if (comparison != 0) {
+                return comparison;
+            }
+        }
+        return 0;
+    }
+}
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java
index 4c6160db4..ed2123608 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java
@@ -687,4 +687,37 @@ public final class ModelTransformer {
     public Model flattenPaginationInfoIntoOperations(Model model, ServiceShape forService) {
         return new FlattenPaginationInfo(forService).transform(this, model);
     }
+
+    /**
+     * Removes any shapes that were deprecated before the specified date.
+     *
+     * <p>The relative date used as a filter should match the ISO 8601 Calendar date format
+     * (i.e. YYYY-MM-DD with optional hyphens).
+     * <p><strong>Note:</strong> Shapes with {@code @deprecated} trait but no {@code since} property are not filtered.
+     *
+     * @param model Model to transform.
+     * @param relativeDate ISO 8601 calendar date to use to filter out deprecated shapes.
+     * @return Returns the transformed model.
+     *
+     * @see <a href="https://www.iso.org/iso-8601-date-and-time-format.html">ISO 8601</a>
+     */
+    public Model filterDeprecatedRelativeDate(Model model, String relativeDate) {
+        return new FilterDeprecatedRelativeDate(relativeDate).transform(this, model);
+    }
+
+    /**
+     * Removes any shapes that were deprecated before the specified version.
+     *
+     * <p>The version used should be a valid Semantic Version (SemVer). For example, {@code 1.2.3.1}.
+     * <p><strong>Note:</strong> Shapes with {@code @deprecated} trait but no {@code since} property are not filtered.
+     *
+     * @param model Model to transform.
+     * @param relativeVersion Semantic Version to use to filter out deprecated shapes.
+     * @return Returns the transformed model.
+     *
+     * @see <a href="https://semver.org/">SemVer</a>
+     */
+    public Model filterDeprecatedRelativeVersion(Model model, String relativeVersion) {
+        return new FilterDeprecatedRelativeVersion(relativeVersion).transform(this, model);
+    }
 }
diff --git a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy
index b8a92aff7..84bd11528 100644
--- a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy
+++ b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy
@@ -668,9 +668,8 @@ list tags {
 /// Defines a proper name for a service or resource shape.
 ///
 /// This title can be used in automatically generated documentation
-/// and other contexts to provide a user friendly name for services
-/// and resources.
-@trait(selector: ":is(service, resource)")
+/// and other contexts to provide a user friendly for shapes.
+@trait(selector: ":not(member)")
 string title
 
 /// Constrains the acceptable values of a string to a fixed set


Test patch:
diff --git a/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/ResourceSchemaTest.java b/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/ResourceSchemaTest.java
new file mode 100644
index 000000000..ce5259be8
--- /dev/null
+++ b/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/ResourceSchemaTest.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *  http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.smithy.aws.cloudformation.schema.fromsmithy;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import software.amazon.smithy.aws.cloudformation.schema.model.ResourceSchema;
+import software.amazon.smithy.model.node.Node;
+import software.amazon.smithy.utils.IoUtils;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class ResourceSchemaTest {
+
+    @ParameterizedTest
+    @MethodSource("resourceSchemaFiles")
+    public void validateResourceSchemaFromNodeToNode(String resourceSchemaFile) {
+        String json = IoUtils.readUtf8File(resourceSchemaFile);
+
+        Node node = Node.parse(json);
+        ResourceSchema schemaFromNode = ResourceSchema.fromNode(node);
+        Node nodeFromSchema = schemaFromNode.toNode();
+
+        Node.assertEquals(nodeFromSchema, node);
+    }
+
+    public static List<String> resourceSchemaFiles() {
+        try {
+            Path definitionPath = Paths.get(ResourceSchemaTest.class.getResource("aws-sagemaker-domain.cfn.json").toURI());
+
+            return Files.walk(Paths.get(definitionPath.getParent().toUri()))
+                    .filter(Files::isRegularFile)
+                    .filter(file -> file.toString().endsWith(".cfn.json"))
+                    .map(Object::toString)
+                    .collect(Collectors.toList());
+        } catch (IOException | URISyntaxException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/aws-sagemaker-domain.cfn.json b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/aws-sagemaker-domain.cfn.json
new file mode 100644
index 000000000..cdb1e347f
--- /dev/null
+++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/aws-sagemaker-domain.cfn.json
@@ -0,0 +1,564 @@
+{
+  "typeName": "AWS::SageMaker::Domain",
+  "description": "Resource Type definition for AWS::SageMaker::Domain",
+  "additionalProperties": false,
+  "properties": {
+    "DomainArn": {
+      "type": "string",
+      "description": "The Amazon Resource Name (ARN) of the created domain.",
+      "maxLength": 256,
+      "pattern": "arn:aws[a-z\\-]*:sagemaker:[a-z0-9\\-]*:[0-9]{12}:domain/.*"
+    },
+    "Url": {
+      "type": "string",
+      "description": "The URL to the created domain.",
+      "maxLength": 1024
+    },
+    "AppNetworkAccessType": {
+      "type": "string",
+      "description": "Specifies the VPC used for non-EFS traffic. The default value is PublicInternetOnly.",
+      "enum": [
+        "PublicInternetOnly",
+        "VpcOnly"
+      ]
+    },
+    "AuthMode": {
+      "type": "string",
+      "description": "The mode of authentication that members use to access the domain.",
+      "enum": [
+        "SSO",
+        "IAM"
+      ]
+    },
+    "DefaultUserSettings": {
+      "$ref": "#/definitions/UserSettings",
+      "description": "The default user settings."
+    },
+    "DefaultSpaceSettings": {
+      "$ref": "#/definitions/DefaultSpaceSettings",
+      "description": "The default space settings."
+    },
+    "DomainName": {
+      "type": "string",
+      "description": "A name for the domain.",
+      "maxLength": 63,
+      "pattern": "^[a-zA-Z0-9](-*[a-zA-Z0-9]){0,62}"
+    },
+    "KmsKeyId": {
+      "type": "string",
+      "description": "SageMaker uses AWS KMS to encrypt the EFS volume attached to the domain with an AWS managed customer master key (CMK) by default.",
+      "maxLength": 2048,
+      "pattern": ".*"
+    },
+    "SubnetIds": {
+      "type": "array",
+      "description": "The VPC subnets that Studio uses for communication.",
+      "uniqueItems": true,
+      "insertionOrder": false,
+      "minItems": 1,
+      "maxItems": 16,
+      "items": {
+        "type": "string",
+        "maxLength": 32,
+        "pattern": "[-0-9a-zA-Z]+"
+      }
+    },
+    "Tags": {
+      "type": "array",
+      "description": "A list of tags to apply to the user profile.",
+      "uniqueItems": true,
+      "insertionOrder": false,
+      "minItems": 0,
+      "maxItems": 50,
+      "items": {
+        "$ref": "#/definitions/Tag"
+      }
+    },
+    "VpcId": {
+      "type": "string",
+      "description": "The ID of the Amazon Virtual Private Cloud (VPC) that Studio uses for communication.",
+      "maxLength": 32,
+      "pattern": "[-0-9a-zA-Z]+"
+    },
+    "DomainId": {
+      "type": "string",
+      "description": "The domain name.",
+      "maxLength": 63,
+      "pattern": "^d-(-*[a-z0-9])+"
+    },
+    "HomeEfsFileSystemId": {
+      "type" : "string",
+      "description" : "The ID of the Amazon Elastic File System (EFS) managed by this Domain.",
+      "maxLength" : 32
+    },
+    "SingleSignOnManagedApplicationInstanceId": {
+      "type" : "string",
+      "description" : "The SSO managed application instance ID.",
+      "maxLength" : 256
+    },
+    "DomainSettings": {
+      "$ref": "#/definitions/DomainSettings"
+    },
+    "AppSecurityGroupManagement": {
+      "type": "string",
+      "description": "The entity that creates and manages the required security groups for inter-app communication in VPCOnly mode. Required when CreateDomain.AppNetworkAccessType is VPCOnly and DomainSettings.RStudioServerProDomainSettings.DomainExecutionRoleArn is provided.",
+      "enum": [
+        "Service",
+        "Customer"
+      ]
+    },
+    "SecurityGroupIdForDomainBoundary": {
+      "type": "string",
+      "description": "The ID of the security group that authorizes traffic between the RSessionGateway apps and the RStudioServerPro app.",
+      "maxLength": 32,
+      "pattern": "[-0-9a-zA-Z]+"
+    }
+  },
+  "definitions": {
+    "UserSettings": {
+      "type": "object",
+      "description": "A collection of settings that apply to users of Amazon SageMaker Studio. These settings are specified when the CreateUserProfile API is called, and as DefaultUserSettings when the CreateDomain API is called.",
+      "additionalProperties": false,
+      "properties": {
+        "ExecutionRole": {
+          "type": "string",
+          "description": "The execution role for the user.",
+          "minLength": 20,
+          "maxLength": 2048,
+          "pattern": "^arn:aws[a-z\\-]*:iam::\\d{12}:role/?[a-zA-Z_0-9+=,.@\\-_/]+$"
+        },
+        "JupyterServerAppSettings": {
+          "$ref": "#/definitions/JupyterServerAppSettings",
+          "description": "The Jupyter server's app settings."
+        },
+        "KernelGatewayAppSettings": {
+          "$ref": "#/definitions/KernelGatewayAppSettings",
+          "description": "The kernel gateway app settings."
+        },
+        "RStudioServerProAppSettings": {
+          "$ref": "#/definitions/RStudioServerProAppSettings"
+        },
+        "RSessionAppSettings": {
+          "$ref": "#/definitions/RSessionAppSettings"
+        },
+        "SecurityGroups": {
+          "type": "array",
+          "description": "The security groups for the Amazon Virtual Private Cloud (VPC) that Studio uses for communication.",
+          "uniqueItems": true,
+          "insertionOrder": false,
+          "minItems": 0,
+          "maxItems": 5,
+          "items": {
+            "type": "string",
+            "maxLength": 32,
+            "pattern": "[-0-9a-zA-Z]+"
+          }
+        },
+        "SharingSettings": {
+          "$ref": "#/definitions/SharingSettings",
+          "description": "The sharing settings."
+        }
+      }
+    },
+    "DefaultSpaceSettings": {
+      "type": "object",
+      "description": "A collection of settings that apply to spaces of Amazon SageMaker Studio. These settings are specified when the Create/Update Domain API is called.",
+      "additionalProperties": false,
+      "properties": {
+        "ExecutionRole": {
+          "type": "string",
+          "description": "The execution role for the space.",
+          "minLength": 20,
+          "maxLength": 2048,
+          "pattern": "^arn:aws[a-z\\-]*:iam::\\d{12}:role/?[a-zA-Z_0-9+=,.@\\-_/]+$"
+        },
+        "JupyterServerAppSettings": {
+          "$ref": "#/definitions/JupyterServerAppSettings",
+          "description": "The Jupyter server's app settings."
+        },
+        "KernelGatewayAppSettings": {
+          "$ref": "#/definitions/KernelGatewayAppSettings",
+          "description": "The kernel gateway app settings."
+        },
+        "SecurityGroups": {
+          "type": "array",
+          "description": "The security groups for the Amazon Virtual Private Cloud (VPC) that Studio uses for communication.",
+          "uniqueItems": true,
+          "insertionOrder": false,
+          "minItems": 0,
+          "maxItems": 5,
+          "items": {
+            "type": "string",
+            "maxLength": 32,
+            "pattern": "[-0-9a-zA-Z]+"
+          }
+        }
+      }
+    },
+    "JupyterServerAppSettings": {
+      "type": "object",
+      "description": "The JupyterServer app settings.",
+      "additionalProperties": false,
+      "properties": {
+        "DefaultResourceSpec": {
+          "$ref": "#/definitions/ResourceSpec"
+        }
+      }
+    },
+    "ResourceSpec": {
+      "type": "object",
+      "additionalProperties": false,
+      "properties": {
+        "InstanceType": {
+          "type": "string",
+          "description": "The instance type that the image version runs on.",
+          "enum": [
+            "system",
+            "ml.t3.micro",
+            "ml.t3.small",
+            "ml.t3.medium",
+            "ml.t3.large",
+            "ml.t3.xlarge",
+            "ml.t3.2xlarge",
+            "ml.m5.large",
+            "ml.m5.xlarge",
+            "ml.m5.2xlarge",
+            "ml.m5.4xlarge",
+            "ml.m5.8xlarge",
+            "ml.m5.12xlarge",
+            "ml.m5.16xlarge",
+            "ml.m5.24xlarge",
+            "ml.c5.large",
+            "ml.c5.xlarge",
+            "ml.c5.2xlarge",
+            "ml.c5.4xlarge",
+            "ml.c5.9xlarge",
+            "ml.c5.12xlarge",
+            "ml.c5.18xlarge",
+            "ml.c5.24xlarge",
+            "ml.p3.2xlarge",
+            "ml.p3.8xlarge",
+            "ml.p3.16xlarge",
+            "ml.g4dn.xlarge",
+            "ml.g4dn.2xlarge",
+            "ml.g4dn.4xlarge",
+            "ml.g4dn.8xlarge",
+            "ml.g4dn.12xlarge",
+            "ml.g4dn.16xlarge",
+            "ml.r5.large",
+            "ml.r5.xlarge",
+            "ml.r5.2xlarge",
+            "ml.r5.4xlarge",
+            "ml.r5.8xlarge",
+            "ml.r5.12xlarge",
+            "ml.r5.16xlarge",
+            "ml.r5.24xlarge",
+            "ml.p3dn.24xlarge",
+            "ml.m5d.large",
+            "ml.m5d.xlarge",
+            "ml.m5d.2xlarge",
+            "ml.m5d.4xlarge",
+            "ml.m5d.8xlarge",
+            "ml.m5d.12xlarge",
+            "ml.m5d.16xlarge",
+            "ml.m5d.24xlarge",
+            "ml.g5.xlarge",
+            "ml.g5.2xlarge",
+            "ml.g5.4xlarge",
+            "ml.g5.8xlarge",
+            "ml.g5.12xlarge",
+            "ml.g5.16xlarge",
+            "ml.g5.24xlarge",
+            "ml.g5.48xlarge"
+          ]
+        },
+        "SageMakerImageArn": {
+          "type": "string",
+          "description": "The Amazon Resource Name (ARN) of the SageMaker image that the image version belongs to.",
+          "maxLength": 256,
+          "pattern": "^arn:aws(-[\\w]+)*:sagemaker:.+:[0-9]{12}:image/[a-z0-9]([-.]?[a-z0-9])*$"
+        },
+        "SageMakerImageVersionArn": {
+          "type": "string",
+          "description": "The Amazon Resource Name (ARN) of the image version created on the instance.",
+          "maxLength": 256,
+          "pattern": "^arn:aws(-[\\w]+)*:sagemaker:.+:[0-9]{12}:image-version/[a-z0-9]([-.]?[a-z0-9])*/[0-9]+$"
+        },
+        "LifecycleConfigArn": {
+          "type": "string",
+          "description": "The Amazon Resource Name (ARN) of the Lifecycle Configuration to attach to the Resource.",
+          "maxLength": 256,
+          "pattern": "arn:aws[a-z\\-]*:sagemaker:[a-z0-9\\-]*:[0-9]{12}:studio-lifecycle-config/.*"
+        }
+      }
+    },
+    "KernelGatewayAppSettings": {
+      "type": "object",
+      "description": "The kernel gateway app settings.",
+      "additionalProperties": false,
+      "properties": {
+        "CustomImages": {
+          "type": "array",
+          "description": "A list of custom SageMaker images that are configured to run as a KernelGateway app.",
+          "uniqueItems": true,
+          "insertionOrder": false,
+          "minItems": 0,
+          "maxItems": 30,
+          "items": {
+            "$ref": "#/definitions/CustomImage"
+          }
+        },
+        "DefaultResourceSpec": {
+          "$ref": "#/definitions/ResourceSpec",
+          "description": "The default instance type and the Amazon Resource Name (ARN) of the default SageMaker image used by the KernelGateway app."
+        }
+      }
+    },
+    "CustomImage": {
+      "type": "object",
+      "description": "A custom SageMaker image.",
+      "additionalProperties": false,
+      "properties": {
+        "AppImageConfigName": {
+          "type": "string",
+          "description": "The Name of the AppImageConfig.",
+          "maxLength": 63,
+          "pattern": "^[a-zA-Z0-9](-*[a-zA-Z0-9]){0,62}"
+        },
+        "ImageName": {
+          "type": "string",
+          "description": "The name of the CustomImage. Must be unique to your account.",
+          "maxLength": 63,
+          "pattern": "^[a-zA-Z0-9]([-.]?[a-zA-Z0-9]){0,62}$"
+        },
+        "ImageVersionNumber": {
+          "type": "integer",
+          "description": "The version number of the CustomImage.",
+          "minimum": 0
+        }
+      },
+      "required": [
+        "AppImageConfigName",
+        "ImageName"
+      ]
+    },
+    "SharingSettings": {
+      "type": "object",
+      "description": "Specifies options when sharing an Amazon SageMaker Studio notebook. These settings are specified as part of DefaultUserSettings when the CreateDomain API is called, and as part of UserSettings when the CreateUserProfile API is called.",
+      "additionalProperties": false,
+      "properties": {
+        "NotebookOutputOption": {
+          "type": "string",
+          "description": "Whether to include the notebook cell output when sharing the notebook. The default is Disabled.",
+          "enum": [
+            "Allowed",
+            "Disabled"
+          ]
+        },
+        "S3KmsKeyId": {
+          "type": "string",
+          "description": "When NotebookOutputOption is Allowed, the AWS Key Management Service (KMS) encryption key ID used to encrypt the notebook cell output in the Amazon S3 bucket.",
+          "maxLength": 2048,
+          "pattern": ".*"
+        },
+        "S3OutputPath": {
+          "type": "string",
+          "description": "When NotebookOutputOption is Allowed, the Amazon S3 bucket used to store the shared notebook snapshots.",
+          "maxLength": 1024,
+          "pattern": "^(https|s3)://([^/]+)/?(.*)$"
+        }
+      }
+    },
+    "Tag": {
+      "type": "object",
+      "additionalProperties": false,
+      "properties": {
+        "Value": {
+          "type": "string",
+          "minLength": 1,
+          "maxLength": 128
+        },
+        "Key": {
+          "type": "string",
+          "minLength": 1,
+          "maxLength": 128
+        }
+      },
+      "required": [
+        "Key",
+        "Value"
+      ]
+    },
+    "DomainSettings": {
+      "type": "object",
+      "description":"A collection of Domain settings.",
+      "additionalProperties": false,
+      "properties": {
+        "SecurityGroupIds": {
+          "type": "array",
+          "description": "The security groups for the Amazon Virtual Private Cloud that the Domain uses for communication between Domain-level apps and user apps.",
+          "uniqueItems": true,
+          "insertionOrder": false,
+          "minItems": 1,
+          "maxItems": 3,
+          "items": {
+            "type": "string",
+            "maxLength": 32,
+            "pattern": "[-0-9a-zA-Z]+"
+          }
+        },
+        "RStudioServerProDomainSettings": {
+          "$ref": "#/definitions/RStudioServerProDomainSettings"
+        }
+      }
+    },
+    "RStudioServerProDomainSettings": {
+      "type": "object",
+      "description": "A collection of settings that update the current configuration for the RStudioServerPro Domain-level app.",
+      "additionalProperties": false,
+      "properties": {
+        "DomainExecutionRoleArn": {
+          "type": "string",
+          "description": "The ARN of the execution role for the RStudioServerPro Domain-level app.",
+          "minLength": 20,
+          "maxLength": 2048,
+          "pattern": "^arn:aws[a-z\\-]*:iam::\\d{12}:role/?[a-zA-Z_0-9+=,.@\\-_/]+$"
+        },
+        "RStudioConnectUrl": {
+          "type": "string",
+          "description": "A URL pointing to an RStudio Connect server.",
+          "pattern": "^(https:|http:|www\\.)\\S*"
+        },
+        "RStudioPackageManagerUrl": {
+          "type": "string",
+          "description": "A URL pointing to an RStudio Package Manager server.",
+          "pattern": "^(https:|http:|www\\.)\\S*"
+        },
+        "DefaultResourceSpec": {
+          "$ref": "#/definitions/ResourceSpec"
+        }
+      },
+      "required":["DomainExecutionRoleArn"]
+    },
+    "RSessionAppSettings": {
+      "type": "object",
+      "description": "A collection of settings that apply to an RSessionGateway app.",
+      "additionalProperties": false,
+      "properties": {
+        "CustomImages": {
+          "type": "array",
+          "description": "A list of custom SageMaker images that are configured to run as a KernelGateway app.",
+          "insertionOrder": false,
+          "uniqueItems": true,
+          "minItems": 0,
+          "maxItems": 30,
+          "items": {
+            "$ref": "#/definitions/CustomImage"
+          }
+        },
+        "DefaultResourceSpec": {
+          "$ref": "#/definitions/ResourceSpec"
+        }
+      }
+    },
+    "RStudioServerProAppSettings": {
+      "type": "object",
+      "description": "A collection of settings that configure user interaction with the RStudioServerPro app.",
+      "additionalProperties": false,
+      "properties": {
+        "AccessStatus": {
+          "type": "string",
+          "description": "Indicates whether the current user has access to the RStudioServerPro app.",
+          "enum": [
+            "ENABLED",
+            "DISABLED"
+          ]
+        },
+        "UserGroup": {
+          "type": "string",
+          "description": "The level of permissions that the user has within the RStudioServerPro app. This value defaults to User. The Admin value allows the user access to the RStudio Administrative Dashboard.",
+          "enum": [
+            "R_STUDIO_ADMIN",
+            "R_STUDIO_USER"
+          ]
+        }
+      }
+    }
+  },
+  "required": [
+    "AuthMode",
+    "DefaultUserSettings",
+    "DomainName",
+    "SubnetIds",
+    "VpcId"
+  ],
+  "createOnlyProperties": [
+    "/properties/AppNetworkAccessType",
+    "/properties/AuthMode",
+    "/properties/DomainName",
+    "/properties/DomainSettings/RStudioServerProDomainSettings/DefaultResourceSpec",
+    "/properties/KmsKeyId",
+    "/properties/SubnetIds",
+    "/properties/Tags",
+    "/properties/VpcId"
+  ],
+  "writeOnlyProperties": [
+    "/properties/Tags"
+  ],
+  "primaryIdentifier": [
+    "/properties/DomainId"
+  ],
+  "readOnlyProperties": [
+    "/properties/DomainArn",
+    "/properties/DomainId",
+    "/properties/HomeEfsFileSystemId",
+    "/properties/SecurityGroupIdForDomainBoundary",
+    "/properties/SingleSignOnManagedApplicationInstanceId",
+    "/properties/Url"
+  ],
+  "handlers": {
+    "create": {
+      "permissions": [
+        "efs:CreateFileSystem",
+        "iam:CreateServiceLinkedRole",
+        "iam:PassRole",
+        "kms:CreateGrant",
+        "kms:Decrypt",
+        "kms:DescribeKey",
+        "kms:GenerateDataKeyWithoutPlainText",
+        "sagemaker:CreateApp",
+        "sagemaker:CreateDomain",
+        "sagemaker:DescribeDomain",
+        "sagemaker:DescribeImage",
+        "sagemaker:DescribeImageVersion"
+      ]
+    },
+    "read": {
+      "permissions": [
+        "sagemaker:DescribeDomain"
+      ]
+    },
+    "update": {
+      "permissions": [
+        "iam:PassRole",
+        "sagemaker:CreateApp",
+        "sagemaker:DescribeDomain",
+        "sagemaker:DescribeImage",
+        "sagemaker:DescribeImageVersion",
+        "sagemaker:UpdateDomain"
+      ]
+    },
+    "delete": {
+      "permissions": [
+        "sagemaker:DeleteApp",
+        "sagemaker:DeleteDomain",
+        "sagemaker:DescribeDomain"
+      ]
+    },
+    "list": {
+      "permissions": [
+        "sagemaker:ListDomains"
+      ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/smithy-build/src/test/java/software/amazon/smithy/build/transforms/RemoveDeprecatedShapesTest.java b/smithy-build/src/test/java/software/amazon/smithy/build/transforms/RemoveDeprecatedShapesTest.java
new file mode 100644
index 000000000..4e9fa5d9f
--- /dev/null
+++ b/smithy-build/src/test/java/software/amazon/smithy/build/transforms/RemoveDeprecatedShapesTest.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.build.transforms;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.file.Paths;
+import org.junit.jupiter.api.Test;
+import software.amazon.smithy.build.TransformContext;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.node.Node;
+import software.amazon.smithy.model.shapes.ShapeId;
+
+public class RemoveDeprecatedShapesTest {
+    @Test
+    public void removesAllDeprecatedShapes() throws Exception {
+        Model model = Model.assembler()
+                .addImport(Paths.get(getClass().getResource("remove-deprecated.smithy").toURI()))
+                .assemble()
+                .unwrap();
+        TransformContext context = TransformContext.builder()
+                .model(model)
+                .settings(Node.objectNode()
+                        .withMember("relativeDate", Node.from("2024-10-10"))
+                        .withMember("relativeVersion", Node.from("1.1.0")))
+                .build();
+        Model result = new RemoveDeprecatedShapes().transform(context);
+
+        // Deprecated by date removed
+        assertFalse(result.getShape(ShapeId.from("smithy.example#FilteredBeforeHyphens")).isPresent());
+        assertFalse(result.getShape(ShapeId.from("smithy.example#FilteredVersionBefore")).isPresent());
+
+        // Equal to the filter retained
+        assertTrue(result.getShape(ShapeId.from("smithy.example#NotFilteredDateEquals")).isPresent());
+        assertTrue(result.getShape(ShapeId.from("smithy.example#NotFilteredVersionEquals")).isPresent());
+
+        // After filter retained
+        assertTrue(result.getShape(ShapeId.from("smithy.example#NotFilteredDateAfter")).isPresent());
+        assertTrue(result.getShape(ShapeId.from("smithy.example#NotFilteredVersionAfter")).isPresent());
+    }
+}
diff --git a/smithy-build/src/test/resources/software/amazon/smithy/build/transforms/remove-deprecated.smithy b/smithy-build/src/test/resources/software/amazon/smithy/build/transforms/remove-deprecated.smithy
new file mode 100644
index 000000000..2e1d85bd2
--- /dev/null
+++ b/smithy-build/src/test/resources/software/amazon/smithy/build/transforms/remove-deprecated.smithy
@@ -0,0 +1,33 @@
+$version: "2.0"
+
+namespace smithy.example
+
+@deprecated(message: "Should be filtered as it is deprecated before date", since: "2024-10-09")
+structure FilteredDateBefore {
+    field: String
+}
+
+@deprecated(message: "Should NOT be filtered as it is deprecated at the same date as the filter", since: "2024-10-10")
+structure NotFilteredDateEquals {
+    field: String
+}
+
+@deprecated(message: "Should NOT be filtered as it is deprecated after the filter date", since: "2024-10-11")
+structure NotFilteredDateAfter {
+    field: String
+}
+
+@deprecated(message: "Should be filtered as it is deprecated before version", since: "1.0.0")
+structure FilteredVersionBefore {
+    field: String
+}
+
+@deprecated(message: "Should NOT be filtered as it is deprecated at the same version as the filter", since: "1.1.0")
+structure NotFilteredVersionEquals {
+    field: String
+}
+
+@deprecated(message: "Should NOT be filtered as it is deprecated after the filter version", since: "1.1.1")
+structure NotFilteredVersionAfter {
+    field: String
+}
diff --git a/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/JsonSchemaConverterTest.java b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/JsonSchemaConverterTest.java
index b3c74ad02..dd0458b1d 100644
--- a/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/JsonSchemaConverterTest.java
+++ b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/JsonSchemaConverterTest.java
@@ -962,4 +962,20 @@ public class JsonSchemaConverterTest {
                 IoUtils.toUtf8String(getClass().getResourceAsStream("member-documentation.jsonschema.json")));
         Node.assertEquals(document.toNode(), expected);
     }
+
+    @Test
+    public void appliesTitlesCorrectly() {
+        Model model = Model.assembler()
+                .addImport(getClass().getResource("title-added.smithy"))
+                .assemble()
+                .unwrap();
+        SchemaDocument document = JsonSchemaConverter.builder()
+                .model(model)
+                .build()
+                .convert();
+
+        Node expected = Node.parse(
+                IoUtils.toUtf8String(getClass().getResourceAsStream("title-added.jsonschema.v07.json")));
+        Node.assertEquals(document.toNode(), expected);
+    }
 }
diff --git a/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/title-added.jsonschema.v07.json b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/title-added.jsonschema.v07.json
new file mode 100644
index 000000000..084201fff
--- /dev/null
+++ b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/title-added.jsonschema.v07.json
@@ -0,0 +1,44 @@
+{
+    "definitions": {
+        "Foo": {
+            "type": "object",
+            "properties": {
+                "bam": {
+                    "type": "array",
+                    "items": {
+                        "type": "string"
+                    },
+                    "title": "A list of strings",
+                    "default": []
+                },
+                "bar": {
+                    "type": "number",
+                    "default": 0
+                },
+                "bat": {
+                    "allOf": [
+                        {
+                            "$ref": "#/definitions/TestEnum"
+                        },
+                        {
+                            "default": "FOO"
+                        }
+                    ]
+                },
+                "baz": {
+                    "type": "string",
+                    "default": ""
+                }
+            },
+            "title": "A structure"
+        },
+        "TestEnum": {
+            "type": "string",
+            "enum": [
+                "FOO",
+                "BAR"
+            ],
+            "title": "A Test Enum!"
+        }
+    }
+}
diff --git a/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/title-added.smithy b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/title-added.smithy
new file mode 100644
index 000000000..338a57168
--- /dev/null
+++ b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/title-added.smithy
@@ -0,0 +1,22 @@
+$version: "2.0"
+
+namespace smithy.example
+
+@title("A structure")
+structure Foo {
+    bar: Integer = 0
+    baz: String = ""
+    bam: StringList = [],
+    bat: TestEnum = "FOO"
+}
+
+@title("A list of strings")
+list StringList {
+    member: String
+}
+
+@title("A Test Enum!")
+enum TestEnum {
+    FOO
+    BAR
+}
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/transform/FilterDeprecatedRelativeDateTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/transform/FilterDeprecatedRelativeDateTest.java
new file mode 100644
index 000000000..6547a4127
--- /dev/null
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/transform/FilterDeprecatedRelativeDateTest.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.model.transform;
+
+import java.util.List;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.node.Node;
+import software.amazon.smithy.model.shapes.ModelSerializer;
+import software.amazon.smithy.utils.ListUtils;
+
+public class FilterDeprecatedRelativeDateTest {
+
+    public static List<String> fileSource() {
+        return ListUtils.of("deprecated-shapes", "deprecated-members", "deprecated-traits");
+    }
+
+    @ParameterizedTest
+    @MethodSource("fileSource")
+    void compareTransform(String prefix) {
+        Model before = Model.assembler()
+                .addImport(FilterDeprecatedRelativeDate.class.getResource("deprecated-date/" + prefix + "-before.smithy"))
+                .assemble()
+                .unwrap();
+        Model actualResult = ModelTransformer.create().filterDeprecatedRelativeDate(before, "2024-10-10");
+        Model expectedResult = Model.assembler()
+                .addImport(FilterDeprecatedRelativeDate.class.getResource("deprecated-date/" + prefix + "-after.smithy"))
+                .assemble()
+                .unwrap();
+
+        Node actualNode = ModelSerializer.builder().build().serialize(actualResult);
+        Node expectedNode = ModelSerializer.builder().build().serialize(expectedResult);
+        Node.assertEquals(actualNode, expectedNode);
+    }
+}
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/transform/FilterDeprecatedRelativeVersionTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/transform/FilterDeprecatedRelativeVersionTest.java
new file mode 100644
index 000000000..e13b2d544
--- /dev/null
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/transform/FilterDeprecatedRelativeVersionTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.model.transform;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.node.Node;
+import software.amazon.smithy.model.shapes.ModelSerializer;
+import software.amazon.smithy.utils.ListUtils;
+
+public class FilterDeprecatedRelativeVersionTest {
+    static List<Arguments> semverSupplier() {
+        return ListUtils.of(
+                Arguments.of("1.0.0", "1.0.0", 0),
+                Arguments.of("1.0.0", "1.0.1", -1),
+                Arguments.of("1.1.0", "1.1.1", -1),
+                Arguments.of("1.1.0", "1.0.1", 1),
+                Arguments.of("1.1.1", "1.1.1.1", -1),
+                Arguments.of("1.0.0.1", "1.0.0", 1),
+                Arguments.of("1.0.0", "1.0", 0),
+                Arguments.of("20.20.0.1", "20.20.1.0", -1),
+                Arguments.of("20.20.1.0", "20.20.1.0-PATCH", -1)
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource("semverSupplier")
+    void testSemverComparison(String semver1, String semver2, int expected) {
+        int result = FilterDeprecatedRelativeVersion.compareSemVer(semver1, semver2);
+        switch (expected) {
+            case 0:
+                assertEquals(result, 0);
+                break;
+            case -1:
+                assertTrue(result < 0);
+                break;
+            case 1:
+                assertTrue(result > 0);
+                break;
+            default:
+                throw new IllegalStateException("Unexpected value: " + expected);
+        }
+    }
+
+    public static List<String> fileSource() {
+        return ListUtils.of("deprecated-shapes", "deprecated-members", "deprecated-traits");
+    }
+
+    @ParameterizedTest
+    @MethodSource("fileSource")
+    void compareTransform(String prefix) {
+        Model before = Model.assembler()
+                .addImport(FilterDeprecatedRelativeDate.class.getResource("deprecated-version/" + prefix + "-before.smithy"))
+                .assemble()
+                .unwrap();
+        Model actualResult = ModelTransformer.create().filterDeprecatedRelativeVersion(before, "1.1.0");
+        Model expectedResult = Model.assembler()
+                .addImport(FilterDeprecatedRelativeDate.class.getResource("deprecated-version/" + prefix + "-after.smithy"))
+                .assemble()
+                .unwrap();
+
+        Node actualNode = ModelSerializer.builder().build().serialize(actualResult);
+        Node expectedNode = ModelSerializer.builder().build().serialize(expectedResult);
+        Node.assertEquals(actualNode, expectedNode);
+    }
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-members-after.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-members-after.smithy
new file mode 100644
index 000000000..ce2ccc52b
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-members-after.smithy
@@ -0,0 +1,11 @@
+$version: "2"
+
+namespace smithy.example
+
+structure MyStruct {
+    @deprecated(message: "Should NOT be filtered as it is deprecated at the same date as the filter", since: "2024-10-10")
+    notFilteredEquals: String
+
+    @deprecated(message: "Should NOT be filtered as it is deprecated after the filter date", since: "2024-10-11")
+    notFilteredAfter: String
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-members-before.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-members-before.smithy
new file mode 100644
index 000000000..b0a8d0b3d
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-members-before.smithy
@@ -0,0 +1,20 @@
+$version: "2"
+
+namespace smithy.example
+
+structure MyStruct {
+    @deprecated(message: "Should be filtered as it is deprecated before date", since: "2024-10-09")
+    filteredBeforeHyphens: String
+
+    @deprecated(message: "Should be filtered as it is deprecated before date", since: "20241009")
+    filteredBeforeNoHyphens: String
+
+    @deprecated(message: "Should be filtered as it is deprecated before date", since: "2024-1009")
+    filteredBeforeMixedHyphens: String
+
+    @deprecated(message: "Should NOT be filtered as it is deprecated at the same date as the filter", since: "2024-10-10")
+    notFilteredEquals: String
+
+    @deprecated(message: "Should NOT be filtered as it is deprecated after the filter date", since: "2024-10-11")
+    notFilteredAfter: String
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-shapes-after.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-shapes-after.smithy
new file mode 100644
index 000000000..7b376f706
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-shapes-after.smithy
@@ -0,0 +1,13 @@
+$version: "2"
+
+namespace smithy.example
+
+@deprecated(message: "Should NOT be filtered as it is deprecated at the same date as the filter", since: "2024-10-10")
+structure NotFilteredEquals {
+    field: String
+}
+
+@deprecated(message: "Should NOT be filtered as it is deprecated after the filter date", since: "2024-10-11")
+structure NotFilteredAfter {
+    field: String
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-shapes-before.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-shapes-before.smithy
new file mode 100644
index 000000000..08e34edd9
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-shapes-before.smithy
@@ -0,0 +1,28 @@
+$version: "2"
+
+namespace smithy.example
+
+@deprecated(message: "Should be filtered as it is deprecated before date", since: "2024-10-09")
+structure FilteredBeforeHyphens {
+    field: String
+}
+
+@deprecated(message: "Should be filtered as it is deprecated before date", since: "20241009")
+structure FilteredBeforeNoHyphens {
+    field: String
+}
+
+@deprecated(message: "Should be filtered as it is deprecated before date", since: "2024-1009")
+structure FilteredBeforeMixedHyphens {
+    field: String
+}
+
+@deprecated(message: "Should NOT be filtered as it is deprecated at the same date as the filter", since: "2024-10-10")
+structure NotFilteredEquals {
+    field: String
+}
+
+@deprecated(message: "Should NOT be filtered as it is deprecated after the filter date", since: "2024-10-11")
+structure NotFilteredAfter {
+    field: String
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-traits-after.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-traits-after.smithy
new file mode 100644
index 000000000..69b7f1828
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-traits-after.smithy
@@ -0,0 +1,15 @@
+$version: "2"
+
+namespace smithy.example
+
+@trait
+@deprecated(message: "Should NOT be filtered as it is deprecated at the same date as the filter", since: "2024-10-10")
+structure NotFilteredEquals {}
+
+@trait
+@deprecated(message: "Should NOT be filtered as it is deprecated after the filter date", since: "2024-10-11")
+structure NotFilteredAfter {}
+
+@NotFilteredEquals
+@NotFilteredAfter
+structure MyStruct {}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-traits-before.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-traits-before.smithy
new file mode 100644
index 000000000..dbae56ddb
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-date/deprecated-traits-before.smithy
@@ -0,0 +1,30 @@
+$version: "2"
+
+namespace smithy.example
+
+@trait
+@deprecated(message: "Should be filtered as it is deprecated before date", since: "2024-10-09")
+structure FilteredBeforeHyphens {}
+
+@trait
+@deprecated(message: "Should be filtered as it is deprecated before date", since: "20241009")
+structure FilteredBeforeNoHyphens {}
+
+@trait
+@deprecated(message: "Should be filtered as it is deprecated before date", since: "2024-1009")
+structure FilteredBeforeMixedHyphens {}
+
+@trait
+@deprecated(message: "Should NOT be filtered as it is deprecated at the same date as the filter", since: "2024-10-10")
+structure NotFilteredEquals {}
+
+@trait
+@deprecated(message: "Should NOT be filtered as it is deprecated after the filter date", since: "2024-10-11")
+structure NotFilteredAfter {}
+
+@FilteredBeforeHyphens
+@FilteredBeforeNoHyphens
+@FilteredBeforeMixedHyphens
+@NotFilteredEquals
+@NotFilteredAfter
+structure MyStruct {}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-members-after.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-members-after.smithy
new file mode 100644
index 000000000..1733842b0
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-members-after.smithy
@@ -0,0 +1,11 @@
+$version: "2"
+
+namespace smithy.example
+
+structure MyStruct {
+    @deprecated(message: "Should NOT be filtered as it is deprecated at the same version as the filter", since: "1.1.0")
+    notFilteredEquals: String
+
+    @deprecated(message: "Should NOT be filtered as it is deprecated after the filter version", since: "1.1.1")
+    notFilteredAfter: String
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-members-before.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-members-before.smithy
new file mode 100644
index 000000000..634ba688c
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-members-before.smithy
@@ -0,0 +1,14 @@
+$version: "2"
+
+namespace smithy.example
+
+structure MyStruct {
+    @deprecated(message: "Should be filtered as it is deprecated before version", since: "1.0.0")
+    filteredBefore: String
+
+    @deprecated(message: "Should NOT be filtered as it is deprecated at the same version as the filter", since: "1.1.0")
+    notFilteredEquals: String
+
+    @deprecated(message: "Should NOT be filtered as it is deprecated after the filter version", since: "1.1.1")
+    notFilteredAfter: String
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-shapes-after.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-shapes-after.smithy
new file mode 100644
index 000000000..378e9911a
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-shapes-after.smithy
@@ -0,0 +1,13 @@
+$version: "2"
+
+namespace smithy.example
+
+@deprecated(message: "Should NOT be filtered as it is deprecated at the same version as the filter", since: "1.1.0")
+structure NotFilteredEquals {
+    field: String
+}
+
+@deprecated(message: "Should NOT be filtered as it is deprecated after the filter version", since: "1.1.1")
+structure NotFilteredAfter {
+    field: String
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-shapes-before.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-shapes-before.smithy
new file mode 100644
index 000000000..c763069dd
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-shapes-before.smithy
@@ -0,0 +1,18 @@
+$version: "2"
+
+namespace smithy.example
+
+@deprecated(message: "Should be filtered as it is deprecated before version", since: "1.0.0")
+structure FilteredBefore {
+    field: String
+}
+
+@deprecated(message: "Should NOT be filtered as it is deprecated at the same version as the filter", since: "1.1.0")
+structure NotFilteredEquals {
+    field: String
+}
+
+@deprecated(message: "Should NOT be filtered as it is deprecated after the filter version", since: "1.1.1")
+structure NotFilteredAfter {
+    field: String
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-traits-after.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-traits-after.smithy
new file mode 100644
index 000000000..528931434
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-traits-after.smithy
@@ -0,0 +1,15 @@
+$version: "2"
+
+namespace smithy.example
+
+@trait
+@deprecated(message: "Should NOT be filtered as it is deprecated at the same version as the filter", since: "1.1.0")
+structure NotFilteredEquals {}
+
+@trait
+@deprecated(message: "Should NOT be filtered as it is deprecated after the filter version", since: "1.1.1")
+structure NotFilteredAfter {}
+
+@NotFilteredEquals
+@NotFilteredAfter
+structure MyStruct {}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-traits-before.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-traits-before.smithy
new file mode 100644
index 000000000..ecbad79cc
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/transform/deprecated-version/deprecated-traits-before.smithy
@@ -0,0 +1,20 @@
+$version: "2"
+
+namespace smithy.example
+
+@trait
+@deprecated(message: "Should be filtered as it is deprecated before version", since: "1.0.0")
+structure FilteredBefore {}
+
+@trait
+@deprecated(message: "Should NOT be filtered as it is deprecated at the same version as the filter", since: "1.1.0")
+structure NotFilteredEquals {}
+
+@trait
+@deprecated(message: "Should NOT be filtered as it is deprecated after the filter version", since: "1.1.1")
+structure NotFilteredAfter {}
+
+@FilteredBefore
+@NotFilteredEquals
+@NotFilteredAfter
+structure MyStruct {}